mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:20:43 +00:00
feat(openai): group codex auth under OpenAI setup
This commit is contained in:
committed by
Val Alexander
parent
5c8c2f48da
commit
17c1e13d32
@@ -13,6 +13,7 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
|
||||
import {
|
||||
OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
hasOpenAICodexCliOAuthCredential,
|
||||
readOpenAICodexCliOAuthProfile,
|
||||
} from "./openai-codex-cli-auth.js";
|
||||
|
||||
@@ -80,6 +81,20 @@ describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
expect(parsed?.credential.expires).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it("detects an existing Codex CLI chatgpt login for setup labeling", () => {
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue(
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: "access-token",
|
||||
refresh_token: "refresh-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(hasOpenAICodexCliOAuthCredential()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not override a locally managed OpenAI Codex profile", () => {
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -66,6 +66,18 @@ function readCodexCliAuthFile(env: NodeJS.ProcessEnv): CodexCliAuthFile | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function hasOpenAICodexCliOAuthCredential(params?: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
const authFile = readCodexCliAuthFile(params?.env ?? process.env);
|
||||
if (!authFile || authFile.auth_mode !== "chatgpt") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
trimNonEmptyString(authFile.tokens?.access_token) &&
|
||||
trimNonEmptyString(authFile.tokens?.refresh_token),
|
||||
);
|
||||
}
|
||||
|
||||
function oauthCredentialMatches(a: OAuthCredential, b: OAuthCredential): boolean {
|
||||
return (
|
||||
a.type === b.type &&
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
|
||||
|
||||
const refreshOpenAICodexTokenMock = vi.hoisted(() => vi.fn());
|
||||
const readOpenAICodexCliOAuthProfileMock = vi.hoisted(() => vi.fn());
|
||||
const hasOpenAICodexCliOAuthCredentialMock = vi.hoisted(() => vi.fn());
|
||||
const loginOpenAICodexDeviceCodeMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./openai-codex-provider.runtime.js", () => ({
|
||||
@@ -15,6 +16,7 @@ vi.mock("./openai-codex-cli-auth.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./openai-codex-cli-auth.js")>();
|
||||
return {
|
||||
...actual,
|
||||
hasOpenAICodexCliOAuthCredential: hasOpenAICodexCliOAuthCredentialMock,
|
||||
readOpenAICodexCliOAuthProfile: readOpenAICodexCliOAuthProfileMock,
|
||||
};
|
||||
});
|
||||
@@ -66,6 +68,8 @@ describe("openai codex provider", () => {
|
||||
beforeEach(() => {
|
||||
refreshOpenAICodexTokenMock.mockReset();
|
||||
readOpenAICodexCliOAuthProfileMock.mockReset();
|
||||
hasOpenAICodexCliOAuthCredentialMock.mockReset();
|
||||
hasOpenAICodexCliOAuthCredentialMock.mockReturnValue(false);
|
||||
loginOpenAICodexDeviceCodeMock.mockReset();
|
||||
});
|
||||
|
||||
@@ -145,7 +149,7 @@ describe("openai codex provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("offers browser, device-code, and one-time Codex CLI import auth methods", () => {
|
||||
it("offers OpenAI menu auth methods for login, import, and device pairing", () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
expect(provider.auth?.map((method) => method.id)).toEqual([
|
||||
@@ -153,18 +157,47 @@ describe("openai codex provider", () => {
|
||||
"device-code",
|
||||
"import-codex-cli",
|
||||
]);
|
||||
expect(provider.auth?.find((method) => method.id === "oauth")).toMatchObject({
|
||||
label: "OpenAI Codex Login",
|
||||
hint: "Browser sign-in",
|
||||
wizard: {
|
||||
choiceId: "openai-codex",
|
||||
choiceLabel: "OpenAI Codex Login",
|
||||
assistantPriority: -30,
|
||||
},
|
||||
});
|
||||
expect(provider.auth?.find((method) => method.id === "device-code")).toMatchObject({
|
||||
label: "ChatGPT device code",
|
||||
hint: "Browser device-code sign-in",
|
||||
label: "OpenAI Codex Device Pairing",
|
||||
hint: "Pair in browser with a device code",
|
||||
kind: "device_code",
|
||||
wizard: {
|
||||
choiceId: "openai-codex-device-code",
|
||||
choiceLabel: "OpenAI Codex Device Pairing",
|
||||
assistantPriority: -10,
|
||||
},
|
||||
});
|
||||
expect(provider.auth?.find((method) => method.id === "import-codex-cli")).toMatchObject({
|
||||
label: "Import Codex CLI login",
|
||||
hint: "Use existing .codex auth once",
|
||||
label: "OpenAI Codex",
|
||||
hint: "Import existing ~/.codex login once",
|
||||
kind: "oauth",
|
||||
wizard: {
|
||||
choiceId: "openai-codex-import",
|
||||
choiceLabel: "OpenAI Codex",
|
||||
assistantPriority: -20,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("annotates the import option when ~/.codex auth is detected", () => {
|
||||
hasOpenAICodexCliOAuthCredentialMock.mockReturnValueOnce(true);
|
||||
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
expect(provider.auth?.find((method) => method.id === "import-codex-cli")).toMatchObject({
|
||||
label: "OpenAI Codex (~/.codex existing key detected)",
|
||||
wizard: {
|
||||
choiceLabel: "OpenAI Codex (~/.codex existing key detected)",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,30 +15,44 @@
|
||||
"method": "oauth",
|
||||
"choiceId": "openai-codex",
|
||||
"deprecatedChoiceIds": ["codex-cli"],
|
||||
"choiceLabel": "OpenAI Codex (ChatGPT OAuth)",
|
||||
"choiceLabel": "OpenAI Codex Login",
|
||||
"choiceHint": "Browser sign-in",
|
||||
"assistantPriority": -30,
|
||||
"groupId": "openai",
|
||||
"groupLabel": "OpenAI",
|
||||
"groupHint": "Codex OAuth + API key"
|
||||
"groupHint": "API key + Codex auth"
|
||||
},
|
||||
{
|
||||
"provider": "openai-codex",
|
||||
"method": "device-code",
|
||||
"choiceId": "openai-codex-device-code",
|
||||
"choiceLabel": "OpenAI Codex (device code)",
|
||||
"choiceHint": "Browser device-code sign-in",
|
||||
"choiceLabel": "OpenAI Codex Device Pairing",
|
||||
"choiceHint": "Pair in browser with a device code",
|
||||
"assistantPriority": -10,
|
||||
"groupId": "openai",
|
||||
"groupLabel": "OpenAI",
|
||||
"groupHint": "Codex OAuth + API key"
|
||||
"groupHint": "API key + Codex auth"
|
||||
},
|
||||
{
|
||||
"provider": "openai-codex",
|
||||
"method": "import-codex-cli",
|
||||
"choiceId": "openai-codex-import",
|
||||
"choiceLabel": "OpenAI Codex",
|
||||
"choiceHint": "Import existing ~/.codex login once",
|
||||
"assistantPriority": -20,
|
||||
"groupId": "openai",
|
||||
"groupLabel": "OpenAI",
|
||||
"groupHint": "API key + Codex auth"
|
||||
},
|
||||
{
|
||||
"provider": "openai",
|
||||
"method": "api-key",
|
||||
"choiceId": "openai-api-key",
|
||||
"choiceLabel": "OpenAI API key",
|
||||
"assistantPriority": -40,
|
||||
"groupId": "openai",
|
||||
"groupLabel": "OpenAI",
|
||||
"groupHint": "Codex OAuth + API key",
|
||||
"groupHint": "API key + Codex auth",
|
||||
"optionKey": "openaiApiKey",
|
||||
"cliFlag": "--openai-api-key",
|
||||
"cliOption": "--openai-api-key <key>",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
const noopAuth = async () => ({ profiles: [] });
|
||||
const OPENAI_WIZARD_GROUP = {
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
groupHint: "API key + Codex auth",
|
||||
} as const;
|
||||
|
||||
export function createOpenAICodexProvider(): ProviderPlugin {
|
||||
return {
|
||||
@@ -11,31 +16,43 @@ export function createOpenAICodexProvider(): ProviderPlugin {
|
||||
{
|
||||
id: "oauth",
|
||||
kind: "oauth",
|
||||
label: "ChatGPT OAuth",
|
||||
label: "OpenAI Codex Login",
|
||||
hint: "Browser sign-in",
|
||||
run: noopAuth,
|
||||
wizard: {
|
||||
choiceId: "openai-codex",
|
||||
choiceLabel: "OpenAI Codex (ChatGPT OAuth)",
|
||||
choiceLabel: "OpenAI Codex Login",
|
||||
choiceHint: "Browser sign-in",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
groupHint: "Codex OAuth + API key",
|
||||
assistantPriority: -30,
|
||||
...OPENAI_WIZARD_GROUP,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "device-code",
|
||||
kind: "device_code",
|
||||
label: "ChatGPT device code",
|
||||
hint: "Browser device-code sign-in",
|
||||
label: "OpenAI Codex Device Pairing",
|
||||
hint: "Pair in browser with a device code",
|
||||
run: noopAuth,
|
||||
wizard: {
|
||||
choiceId: "openai-codex-device-code",
|
||||
choiceLabel: "OpenAI Codex (device code)",
|
||||
choiceHint: "Browser device-code sign-in",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
groupHint: "Codex OAuth + API key",
|
||||
choiceLabel: "OpenAI Codex Device Pairing",
|
||||
choiceHint: "Pair in browser with a device code",
|
||||
assistantPriority: -10,
|
||||
...OPENAI_WIZARD_GROUP,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "import-codex-cli",
|
||||
kind: "oauth",
|
||||
label: "OpenAI Codex",
|
||||
hint: "Import existing ~/.codex login once",
|
||||
run: noopAuth,
|
||||
wizard: {
|
||||
choiceId: "openai-codex-import",
|
||||
choiceLabel: "OpenAI Codex",
|
||||
choiceHint: "Import existing ~/.codex login once",
|
||||
assistantPriority: -20,
|
||||
...OPENAI_WIZARD_GROUP,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -59,9 +76,8 @@ export function createOpenAIProvider(): ProviderPlugin {
|
||||
wizard: {
|
||||
choiceId: "openai-api-key",
|
||||
choiceLabel: "OpenAI API key",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
groupHint: "Codex OAuth + API key",
|
||||
assistantPriority: -40,
|
||||
...OPENAI_WIZARD_GROUP,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -447,6 +447,53 @@ describe("buildAuthChoiceOptions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("orders OpenAI auth methods as api key, login, import, then device pairing", () => {
|
||||
resolveProviderWizardOptions.mockReturnValue([
|
||||
{
|
||||
value: "openai-api-key",
|
||||
label: "OpenAI API key",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
assistantPriority: -40,
|
||||
},
|
||||
{
|
||||
value: "openai-codex",
|
||||
label: "OpenAI Codex Login",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
assistantPriority: -30,
|
||||
},
|
||||
{
|
||||
value: "openai-codex-import",
|
||||
label: "OpenAI Codex (~/.codex existing key detected)",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
assistantPriority: -20,
|
||||
},
|
||||
{
|
||||
value: "openai-codex-device-code",
|
||||
label: "OpenAI Codex Device Pairing",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
assistantPriority: -10,
|
||||
},
|
||||
]);
|
||||
|
||||
const { groups } = buildAuthChoiceGroups({
|
||||
store: EMPTY_STORE,
|
||||
includeSkip: false,
|
||||
});
|
||||
const openAIGroup = groups.find((group) => group.value === "openai");
|
||||
|
||||
expect(openAIGroup).toBeDefined();
|
||||
expect(openAIGroup?.options.map((option) => option.value)).toEqual([
|
||||
"openai-api-key",
|
||||
"openai-codex",
|
||||
"openai-codex-import",
|
||||
"openai-codex-device-code",
|
||||
]);
|
||||
});
|
||||
|
||||
it("groups OpenCode Zen and Go under one OpenCode entry", () => {
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user