feat(openai): group codex auth under OpenAI setup

This commit is contained in:
Vincent Koc
2026-04-20 20:10:58 -07:00
committed by Val Alexander
parent 5c8c2f48da
commit 17c1e13d32
6 changed files with 163 additions and 26 deletions

View File

@@ -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({

View File

@@ -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 &&

View File

@@ -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)",
},
});
});

View File

@@ -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>",

View File

@@ -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,
},
},
],

View File

@@ -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([
{