From 49e9382cc05a33d12a79cfbe9f00a6d31908f689 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 16 May 2026 01:02:33 +0100 Subject: [PATCH] fix(configure): unify OpenAI auth provider picker (#82324) --- CHANGELOG.md | 1 + extensions/openai/auth-choice-copy.ts | 14 ++++--- .../openai/openai-codex-provider.test.ts | 31 ++++++++------- extensions/openai/openai-codex-provider.ts | 2 + extensions/openai/openai-provider.test.ts | 2 +- extensions/openai/openclaw.plugin.json | 31 ++++++++------- extensions/openai/openclaw.plugin.test.ts | 33 +++++++++------- extensions/openai/provider-contract-api.ts | 1 + extensions/openai/setup-api.test.ts | 1 + extensions/openai/setup-api.ts | 4 ++ src/commands/auth-choice-options.test.ts | 30 +++++++++++--- ...re.gateway-auth.prompt-auth-config.test.ts | 39 +++++++++++++++++++ src/commands/configure.gateway-auth.ts | 10 ++++- src/commands/model-picker.test.ts | 33 ++++++++++++++++ src/flows/model-picker.ts | 3 +- src/plugins/provider-validation.test.ts | 4 ++ src/plugins/provider-validation.ts | 1 + src/plugins/provider-wizard.ts | 2 + src/plugins/types.ts | 1 + src/wizard/setup.test.ts | 4 +- 20 files changed, 187 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6175f81c5..d7e6067a4f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Providers: reject malformed successful Runway, BytePlus, and Ollama embedding responses with provider-owned errors instead of raw parser/type failures, silent bad vectors, or long bogus polling. - Trajectory export: skip and report malformed session/runtime JSONL rows in `manifest.json` instead of letting wrong-shaped session rows crash support bundle export. - Config/doctor: copy fallback-enabled channel `allowFrom` entries into explicit `groupAllowFrom` allowlists during `openclaw doctor --fix`, preserving current group access without adding runtime fallback-transition flags. +- Configure: show one OpenAI provider entry with ChatGPT/Codex sign-in and API key choices, and keep browsed Codex models in the saved `/model` picker allowlist. - Hooks: raise bounded gateway lifecycle hook wait budgets to 5 seconds for shutdown and 10 seconds for pre-restart, giving short restart notification handlers time to finish before shutdown continues. (#82273) Thanks @bryanbaer. - Plugin releases: require external package compatibility metadata in the npm plugin publish plan, matching the ClawHub package contract before packages ship. - Agents/OpenAI-compatible: honor per-model `max_completion_tokens`/`max_tokens` params in embedded OpenAI-completions runs so high-token Kimi-style routes keep their configured completion cap. Fixes #82230. Thanks @albert-zen. diff --git a/extensions/openai/auth-choice-copy.ts b/extensions/openai/auth-choice-copy.ts index c5cf57db2a7..bfcf049ccc5 100644 --- a/extensions/openai/auth-choice-copy.ts +++ b/extensions/openai/auth-choice-copy.ts @@ -7,11 +7,13 @@ export const OPENAI_CHATGPT_DEVICE_PAIRING_HINT = export const OPENAI_CODEX_API_KEY_BACKUP_LABEL = "OpenAI API Key Backup"; export const OPENAI_CODEX_API_KEY_BACKUP_HINT = "Use an OpenAI API key when your Codex subscription is unavailable"; -export const OPENAI_CODEX_LOGIN_LABEL = "OpenAI Codex Browser Login"; +export const OPENAI_CODEX_LOGIN_LABEL = "ChatGPT/Codex Browser Login"; export const OPENAI_CODEX_LOGIN_HINT = "Sign in with OpenAI in your browser"; -export const OPENAI_CODEX_DEVICE_PAIRING_LABEL = "OpenAI Codex Device Pairing"; +export const OPENAI_CODEX_DEVICE_PAIRING_LABEL = "ChatGPT/Codex Device Pairing"; export const OPENAI_CODEX_DEVICE_PAIRING_HINT = "Pair in browser with a device code"; +const OPENAI_UNIFIED_GROUP_HINT = "ChatGPT/Codex sign-in or API key"; + export const OPENAI_API_KEY_WIZARD_GROUP = { groupId: "openai", groupLabel: "OpenAI", @@ -21,11 +23,11 @@ export const OPENAI_API_KEY_WIZARD_GROUP = { export const OPENAI_ACCOUNT_WIZARD_GROUP = { groupId: "openai", groupLabel: "OpenAI", - groupHint: "ChatGPT subscription or API key", + groupHint: OPENAI_UNIFIED_GROUP_HINT, } as const; export const OPENAI_CODEX_WIZARD_GROUP = { - groupId: "openai-codex", - groupLabel: "OpenAI Codex", - groupHint: "ChatGPT/Codex sign-in", + groupId: "openai", + groupLabel: "OpenAI", + groupHint: OPENAI_UNIFIED_GROUP_HINT, } as const; diff --git a/extensions/openai/openai-codex-provider.test.ts b/extensions/openai/openai-codex-provider.test.ts index 0d8e48a786b..4f86fa18efe 100644 --- a/extensions/openai/openai-codex-provider.test.ts +++ b/extensions/openai/openai-codex-provider.test.ts @@ -170,23 +170,23 @@ describe("openai codex provider", () => { const apiKey = requireAuthMethod(provider, "api-key"); expectRecordFields(oauth.wizard, "oauth wizard", { - choiceLabel: "OpenAI Codex Browser Login", - groupId: "openai-codex", - groupLabel: "OpenAI Codex", - groupHint: "ChatGPT/Codex sign-in", + choiceLabel: "ChatGPT/Codex Browser Login", + groupId: "openai", + groupLabel: "OpenAI", + groupHint: "ChatGPT/Codex sign-in or API key", }); expectRecordFields(deviceCode.wizard, "device-code wizard", { - choiceLabel: "OpenAI Codex Device Pairing", - groupId: "openai-codex", - groupLabel: "OpenAI Codex", - groupHint: "ChatGPT/Codex sign-in", + choiceLabel: "ChatGPT/Codex Device Pairing", + groupId: "openai", + groupLabel: "OpenAI", + groupHint: "ChatGPT/Codex sign-in or API key", }); expectRecordFields(apiKey.wizard, "api-key wizard", { choiceLabel: "OpenAI API Key Backup", choiceHint: "Use an OpenAI API key when your Codex subscription is unavailable", - groupId: "openai-codex", - groupLabel: "OpenAI Codex", - groupHint: "ChatGPT/Codex sign-in", + groupId: "openai", + groupLabel: "OpenAI", + groupHint: "ChatGPT/Codex sign-in or API key", }); }); @@ -222,19 +222,20 @@ describe("openai codex provider", () => { const deviceCode = requireAuthMethod(provider, "device-code"); expect(provider.auth?.map((method) => method.id)).toEqual(["oauth", "device-code", "api-key"]); - expect(oauth.label).toBe("OpenAI Codex Browser Login"); + expect(oauth.label).toBe("ChatGPT/Codex Browser Login"); expect(oauth.hint).toBe("Sign in with OpenAI in your browser"); expectRecordFields(oauth.wizard, "oauth wizard", { choiceId: "openai-codex", - choiceLabel: "OpenAI Codex Browser Login", + choiceLabel: "ChatGPT/Codex Browser Login", assistantPriority: -30, + onboardingFeatured: true, }); - expect(deviceCode.label).toBe("OpenAI Codex Device Pairing"); + expect(deviceCode.label).toBe("ChatGPT/Codex Device Pairing"); expect(deviceCode.hint).toBe("Pair in browser with a device code"); expect(deviceCode.kind).toBe("device_code"); expectRecordFields(deviceCode.wizard, "device-code wizard", { choiceId: "openai-codex-device-code", - choiceLabel: "OpenAI Codex Device Pairing", + choiceLabel: "ChatGPT/Codex Device Pairing", assistantPriority: -10, }); }); diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index ac6dc852dee..bf678d686bb 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -489,6 +489,7 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { choiceLabel: OPENAI_CODEX_LOGIN_LABEL, choiceHint: OPENAI_CODEX_LOGIN_HINT, assistantPriority: OPENAI_CODEX_LOGIN_ASSISTANT_PRIORITY, + onboardingFeatured: true, ...OPENAI_CODEX_WIZARD_GROUP, }, run: async (ctx) => await runOpenAICodexOAuth(ctx), @@ -525,6 +526,7 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { choiceLabel: OPENAI_CODEX_API_KEY_BACKUP_LABEL, choiceHint: OPENAI_CODEX_API_KEY_BACKUP_HINT, assistantPriority: 5, + assistantVisibility: "manual-only", ...OPENAI_CODEX_WIZARD_GROUP, }, }), diff --git a/extensions/openai/openai-provider.test.ts b/extensions/openai/openai-provider.test.ts index e2e92974eea..44bdb9de73f 100644 --- a/extensions/openai/openai-provider.test.ts +++ b/extensions/openai/openai-provider.test.ts @@ -132,7 +132,7 @@ describe("buildOpenAIProvider", () => { choiceHint: "Use your OpenAI API key directly", groupId: "openai", groupLabel: "OpenAI", - groupHint: "ChatGPT subscription or API key", + groupHint: "ChatGPT/Codex sign-in or API key", }); }); diff --git a/extensions/openai/openclaw.plugin.json b/extensions/openai/openclaw.plugin.json index 1ed34b5211d..2f68cc732bf 100644 --- a/extensions/openai/openclaw.plugin.json +++ b/extensions/openai/openclaw.plugin.json @@ -767,9 +767,10 @@ "choiceLabel": "ChatGPT Login", "choiceHint": "Sign in with your ChatGPT or Codex subscription", "assistantPriority": -40, + "assistantVisibility": "manual-only", "groupId": "openai", "groupLabel": "OpenAI", - "groupHint": "ChatGPT subscription or API key" + "groupHint": "ChatGPT/Codex sign-in or API key" }, { "provider": "openai", @@ -778,32 +779,34 @@ "choiceLabel": "ChatGPT Device Pairing", "choiceHint": "Pair your ChatGPT account in browser with a device code", "assistantPriority": -10, + "assistantVisibility": "manual-only", "groupId": "openai", "groupLabel": "OpenAI", - "groupHint": "ChatGPT subscription or API key" + "groupHint": "ChatGPT/Codex sign-in or API key" }, { "provider": "openai-codex", "method": "oauth", "choiceId": "openai-codex", "deprecatedChoiceIds": ["codex-cli", "openai-codex-import"], - "choiceLabel": "OpenAI Codex Browser Login", + "choiceLabel": "ChatGPT/Codex Browser Login", "choiceHint": "Sign in with OpenAI in your browser", "assistantPriority": -30, - "groupId": "openai-codex", - "groupLabel": "OpenAI Codex", - "groupHint": "ChatGPT/Codex sign-in" + "groupId": "openai", + "groupLabel": "OpenAI", + "groupHint": "ChatGPT/Codex sign-in or API key", + "onboardingFeatured": true }, { "provider": "openai-codex", "method": "device-code", "choiceId": "openai-codex-device-code", - "choiceLabel": "OpenAI Codex Device Pairing", + "choiceLabel": "ChatGPT/Codex Device Pairing", "choiceHint": "Pair in browser with a device code", "assistantPriority": -10, - "groupId": "openai-codex", - "groupLabel": "OpenAI Codex", - "groupHint": "ChatGPT/Codex sign-in" + "groupId": "openai", + "groupLabel": "OpenAI", + "groupHint": "ChatGPT/Codex sign-in or API key" }, { "provider": "openai-codex", @@ -813,9 +816,9 @@ "choiceHint": "Use an OpenAI API key when your Codex subscription is unavailable", "assistantPriority": 5, "assistantVisibility": "manual-only", - "groupId": "openai-codex", - "groupLabel": "OpenAI Codex", - "groupHint": "ChatGPT/Codex sign-in", + "groupId": "openai", + "groupLabel": "OpenAI", + "groupHint": "ChatGPT/Codex sign-in or API key", "optionKey": "openaiApiKey", "cliFlag": "--openai-api-key", "cliOption": "--openai-api-key ", @@ -830,7 +833,7 @@ "assistantPriority": 5, "groupId": "openai", "groupLabel": "OpenAI", - "groupHint": "ChatGPT subscription or API key", + "groupHint": "ChatGPT/Codex sign-in or API key", "onboardingFeatured": true, "optionKey": "openaiApiKey", "cliFlag": "--openai-api-key", diff --git a/extensions/openai/openclaw.plugin.test.ts b/extensions/openai/openclaw.plugin.test.ts index dfbf2fae3a1..f9b7c39b81b 100644 --- a/extensions/openai/openclaw.plugin.test.ts +++ b/extensions/openai/openclaw.plugin.test.ts @@ -22,6 +22,7 @@ const manifest = JSON.parse( choiceHint?: string; choiceId?: string; deprecatedChoiceIds?: string[]; + assistantVisibility?: string; groupId?: string; groupLabel?: string; groupHint?: string; @@ -38,6 +39,7 @@ function manifestComparableWizardFields(choice: { choiceId?: string; choiceLabel?: string; choiceHint?: string; + assistantVisibility?: string; groupId?: string; groupLabel?: string; groupHint?: string; @@ -47,6 +49,7 @@ function manifestComparableWizardFields(choice: { choiceId: choice.choiceId, choiceLabel: choice.choiceLabel, choiceHint: choice.choiceHint, + assistantVisibility: choice.assistantVisibility, groupId: choice.groupId, groupLabel: choice.groupLabel, groupHint: choice.groupHint, @@ -125,38 +128,40 @@ describe("OpenAI plugin manifest", () => { expect(openAiLogin?.choiceLabel).toBe("ChatGPT Login"); expect(openAiLogin?.choiceHint).toBe("Sign in with your ChatGPT or Codex subscription"); + expect(openAiLogin?.assistantVisibility).toBe("manual-only"); expect(openAiLogin?.groupId).toBe("openai"); expect(openAiLogin?.groupLabel).toBe("OpenAI"); - expect(openAiLogin?.groupHint).toBe("ChatGPT subscription or API key"); + expect(openAiLogin?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); expect(openAiDeviceCode?.choiceLabel).toBe("ChatGPT Device Pairing"); expect(openAiDeviceCode?.choiceHint).toBe( "Pair your ChatGPT account in browser with a device code", ); + expect(openAiDeviceCode?.assistantVisibility).toBe("manual-only"); expect(openAiDeviceCode?.groupId).toBe("openai"); expect(openAiDeviceCode?.groupLabel).toBe("OpenAI"); - expect(openAiDeviceCode?.groupHint).toBe("ChatGPT subscription or API key"); - expect(codexBrowserLogin?.choiceLabel).toBe("OpenAI Codex Browser Login"); + expect(openAiDeviceCode?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); + expect(codexBrowserLogin?.choiceLabel).toBe("ChatGPT/Codex Browser Login"); expect(codexBrowserLogin?.choiceHint).toBe("Sign in with OpenAI in your browser"); - expect(codexBrowserLogin?.groupId).toBe("openai-codex"); - expect(codexBrowserLogin?.groupLabel).toBe("OpenAI Codex"); - expect(codexBrowserLogin?.groupHint).toBe("ChatGPT/Codex sign-in"); - expect(codexDeviceCode?.choiceLabel).toBe("OpenAI Codex Device Pairing"); + expect(codexBrowserLogin?.groupId).toBe("openai"); + expect(codexBrowserLogin?.groupLabel).toBe("OpenAI"); + expect(codexBrowserLogin?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); + expect(codexDeviceCode?.choiceLabel).toBe("ChatGPT/Codex Device Pairing"); expect(codexDeviceCode?.choiceHint).toBe("Pair in browser with a device code"); - expect(codexDeviceCode?.groupId).toBe("openai-codex"); - expect(codexDeviceCode?.groupLabel).toBe("OpenAI Codex"); - expect(codexDeviceCode?.groupHint).toBe("ChatGPT/Codex sign-in"); + expect(codexDeviceCode?.groupId).toBe("openai"); + expect(codexDeviceCode?.groupLabel).toBe("OpenAI"); + expect(codexDeviceCode?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); expect(apiKey?.choiceLabel).toBe("OpenAI API Key"); expect(apiKey?.choiceHint).toBe("Use your OpenAI API key directly"); expect(apiKey?.groupId).toBe("openai"); expect(apiKey?.groupLabel).toBe("OpenAI"); - expect(apiKey?.groupHint).toBe("ChatGPT subscription or API key"); + expect(apiKey?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); expect(codexApiKey?.choiceLabel).toBe("OpenAI API Key Backup"); expect(codexApiKey?.choiceHint).toBe( "Use an OpenAI API key when your Codex subscription is unavailable", ); - expect(codexApiKey?.groupId).toBe("openai-codex"); - expect(codexApiKey?.groupLabel).toBe("OpenAI Codex"); - expect(codexApiKey?.groupHint).toBe("ChatGPT/Codex sign-in"); + expect(codexApiKey?.groupId).toBe("openai"); + expect(codexApiKey?.groupLabel).toBe("OpenAI"); + expect(codexApiKey?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); expect(choices.map((choice) => choice.choiceLabel)).not.toContain( "OpenAI Codex (ChatGPT OAuth)", ); diff --git a/extensions/openai/provider-contract-api.ts b/extensions/openai/provider-contract-api.ts index 11dd6f64ecc..6b4f2e2395a 100644 --- a/extensions/openai/provider-contract-api.ts +++ b/extensions/openai/provider-contract-api.ts @@ -34,6 +34,7 @@ export function createOpenAICodexProvider(): ProviderPlugin { choiceLabel: OPENAI_CODEX_LOGIN_LABEL, choiceHint: OPENAI_CODEX_LOGIN_HINT, assistantPriority: -30, + onboardingFeatured: true, ...OPENAI_CODEX_WIZARD_GROUP, }, }, diff --git a/extensions/openai/setup-api.test.ts b/extensions/openai/setup-api.test.ts index f3d62e7ac40..1e295938fcf 100644 --- a/extensions/openai/setup-api.test.ts +++ b/extensions/openai/setup-api.test.ts @@ -15,6 +15,7 @@ describe("OpenAI setup auth provider", () => { expect(authMethodIds(provider)).toEqual(["oauth", "device-code", "api-key"]); expect(oauth?.label).toBe("ChatGPT Login"); expect(oauth?.wizard?.choiceId).toBe("openai"); + expect(oauth?.wizard?.assistantVisibility).toBe("manual-only"); expect(apiKey?.label).toBe("OpenAI API Key"); expect(apiKey?.wizard?.choiceId).toBe("openai-api-key"); }); diff --git a/extensions/openai/setup-api.ts b/extensions/openai/setup-api.ts index 10bbd7328db..342c6b2428a 100644 --- a/extensions/openai/setup-api.ts +++ b/extensions/openai/setup-api.ts @@ -53,6 +53,7 @@ export function buildOpenAISetupProvider(): ProviderPlugin { choiceLabel: OPENAI_CHATGPT_LOGIN_LABEL, choiceHint: OPENAI_CHATGPT_LOGIN_HINT, assistantPriority: -40, + assistantVisibility: "manual-only", ...OPENAI_ACCOUNT_WIZARD_GROUP, }, run: async (ctx) => runOpenAICodexProviderAuthMethod("oauth", ctx), @@ -68,6 +69,7 @@ export function buildOpenAISetupProvider(): ProviderPlugin { choiceLabel: OPENAI_CHATGPT_DEVICE_PAIRING_LABEL, choiceHint: OPENAI_CHATGPT_DEVICE_PAIRING_HINT, assistantPriority: -10, + assistantVisibility: "manual-only", ...OPENAI_ACCOUNT_WIZARD_GROUP, }, run: async (ctx) => runOpenAICodexProviderAuthMethod("device-code", ctx), @@ -108,6 +110,7 @@ export function buildOpenAICodexSetupProvider(): ProviderPlugin { choiceLabel: OPENAI_CODEX_LOGIN_LABEL, choiceHint: OPENAI_CODEX_LOGIN_HINT, assistantPriority: -30, + onboardingFeatured: true, ...OPENAI_CODEX_WIZARD_GROUP, }, run: async (ctx) => runOpenAICodexProviderAuthMethod("oauth", ctx), @@ -138,6 +141,7 @@ export function buildOpenAICodexSetupProvider(): ProviderPlugin { choiceLabel: OPENAI_CODEX_API_KEY_BACKUP_LABEL, choiceHint: OPENAI_CODEX_API_KEY_BACKUP_HINT, assistantPriority: 5, + assistantVisibility: "manual-only", ...OPENAI_CODEX_WIZARD_GROUP, }, run: async (ctx) => runOpenAICodexProviderAuthMethod("api-key", ctx), diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index c82fbbfedab..d8d9274aef2 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -55,6 +55,7 @@ vi.mock("../flows/provider-flow.js", () => ({ ...(choice.assistantVisibility ? { assistantVisibility: choice.assistantVisibility } : {}), + ...(choice.onboardingFeatured ? { onboardingFeatured: true } : {}), }, })), ...resolveProviderWizardOptions() @@ -75,6 +76,7 @@ vi.mock("../flows/provider-flow.js", () => ({ ...(option.assistantVisibility ? { assistantVisibility: option.assistantVisibility } : {}), + ...(option.onboardingFeatured ? { onboardingFeatured: true } : {}), }, })), ]; @@ -455,25 +457,42 @@ describe("buildAuthChoiceOptions", () => { ]); }); - it("orders OpenAI auth methods as api key, browser login, then device pairing", () => { + it("groups OpenAI auth methods under one provider entry", () => { resolveProviderWizardOptions.mockReturnValue([ + { + value: "openai", + label: "ChatGPT Login", + groupId: "openai", + groupLabel: "OpenAI", + assistantPriority: -40, + assistantVisibility: "manual-only", + }, + { + value: "openai-device-code", + label: "ChatGPT Device Pairing", + groupId: "openai", + groupLabel: "OpenAI", + assistantPriority: -10, + assistantVisibility: "manual-only", + }, { value: "openai-api-key", label: "OpenAI API Key", groupId: "openai", groupLabel: "OpenAI", - assistantPriority: -40, + assistantPriority: 5, }, { value: "openai-codex", - label: "OpenAI Codex Browser Login", + label: "ChatGPT/Codex Browser Login", groupId: "openai", groupLabel: "OpenAI", assistantPriority: -30, + onboardingFeatured: true, }, { value: "openai-codex-device-code", - label: "OpenAI Codex Device Pairing", + label: "ChatGPT/Codex Device Pairing", groupId: "openai", groupLabel: "OpenAI", assistantPriority: -10, @@ -487,10 +506,11 @@ describe("buildAuthChoiceOptions", () => { const openAIGroup = requireChoiceGroup(groups, "openai"); expect(openAIGroup.options.map((option) => option.value)).toEqual([ - "openai-api-key", "openai-codex", "openai-codex-device-code", + "openai-api-key", ]); + expect(openAIGroup.options[0]?.onboardingFeatured).toBe(true); }); it("groups OpenCode Zen and Go under one OpenCode entry", () => { diff --git a/src/commands/configure.gateway-auth.prompt-auth-config.test.ts b/src/commands/configure.gateway-auth.prompt-auth-config.test.ts index 68b93fbce12..1f7eb281e76 100644 --- a/src/commands/configure.gateway-auth.prompt-auth-config.test.ts +++ b/src/commands/configure.gateway-auth.prompt-auth-config.test.ts @@ -222,6 +222,16 @@ function promptModelAllowlistOptions(index = 0) { | undefined; } +function promptDefaultModelOptions(index = 0) { + return mocks.promptDefaultModel.mock.calls[index]?.[0] as + | { + browseCatalogOnDemand?: boolean; + loadCatalog?: boolean; + preferredProvider?: string; + } + | undefined; +} + const noopPrompter = {} as WizardPrompter; function createKilocodeProvider() { @@ -631,6 +641,35 @@ describe("promptAuthConfig", () => { expect(call?.loadCatalog).toBe(true); }); + it("lets skip-auth model browsing scope the allowlist to the selected model provider", async () => { + vi.clearAllMocks(); + mocks.promptAuthChoiceGrouped.mockResolvedValue("skip"); + mocks.promptDefaultModel.mockResolvedValue({ model: "openai-codex/gpt-5.5" }); + mocks.promptModelAllowlist.mockResolvedValue({ + models: ["openai-codex/gpt-5.5"], + scopeKeys: ["openai-codex/gpt-5.5", "openai-codex/gpt-5.5-pro"], + }); + mocks.resolveProviderPluginChoice.mockReturnValue(null); + + const result = await promptAuthConfig( + { + agents: { + defaults: { + model: { primary: "fleet-router/qwen3.6:latest" }, + }, + }, + }, + makeRuntime(), + noopPrompter, + ); + + expect(promptDefaultModelOptions()?.loadCatalog).toBe(true); + expect(promptDefaultModelOptions()?.browseCatalogOnDemand).toBe(true); + expect(promptModelAllowlistOptions()?.preferredProvider).toBe("openai-codex"); + expect(result.agents?.defaults?.model).toEqual({ primary: "openai-codex/gpt-5.5" }); + expect(Object.keys(result.agents?.defaults?.models ?? {})).toEqual(["openai-codex/gpt-5.5"]); + }); + it("returns to auth selection when plugin install onboarding asks for a retry", async () => { vi.clearAllMocks(); mocks.promptAuthChoiceGrouped diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts index b1fec347593..79bb278999e 100644 --- a/src/commands/configure.gateway-auth.ts +++ b/src/commands/configure.gateway-auth.ts @@ -104,6 +104,12 @@ function resolveSingleConfiguredProvider(cfg: OpenClawConfig): string | undefine return configuredProviders.length === 1 ? configuredProviders[0] : undefined; } +function resolveProviderFromModelRef(model: string | undefined): string | undefined { + const trimmed = model?.trim(); + const slashIndex = trimmed?.indexOf("/") ?? -1; + return slashIndex > 0 ? trimmed?.slice(0, slashIndex) : undefined; +} + function resolveConfiguredProviderFromAuthChange(params: { before: OpenClawConfig; after: OpenClawConfig; @@ -210,7 +216,8 @@ export async function promptAuthConfig( allowKeep: true, ignoreAllowlist: true, includeProviderPluginSetups: false, - loadCatalog: false, + loadCatalog: true, + browseCatalogOnDemand: true, preferredProvider, workspaceDir: resolveDefaultAgentWorkspaceDir(), runtime, @@ -220,6 +227,7 @@ export async function promptAuthConfig( } if (modelSelection.model) { next = applyPrimaryModel(next, modelSelection.model); + preferredProvider = resolveProviderFromModelRef(modelSelection.model) ?? preferredProvider; } break; } diff --git a/src/commands/model-picker.test.ts b/src/commands/model-picker.test.ts index 2205ea390dc..437975bce4c 100644 --- a/src/commands/model-picker.test.ts +++ b/src/commands/model-picker.test.ts @@ -574,6 +574,39 @@ describe("promptDefaultModel", () => { ]); }); + it("keeps the full catalog cold until browsing when no provider is preferred", async () => { + const select = vi.fn(async (params) => params.initialValue as never); + const prompter = makePrompter({ select }); + const config = { + agents: { + defaults: { + model: "fleet-router/qwen3.6:latest", + }, + }, + } as OpenClawConfig; + + const result = await promptDefaultModel({ + config, + prompter, + allowKeep: true, + includeManual: true, + ignoreAllowlist: true, + browseCatalogOnDemand: true, + loadCatalog: true, + }); + + expect(result).toStrictEqual({}); + expect(loadModelCatalog).not.toHaveBeenCalled(); + const params = pickerParams(select as MockCallSource); + expect(params.searchable).toBe(false); + expect(params.initialValue).toBe("__keep__"); + expect(optionValues(pickerOptions(select as MockCallSource))).toEqual([ + "__keep__", + "__manual__", + "__browse__", + ]); + }); + it("loads the full model catalog when the user chooses to browse", async () => { loadModelCatalog.mockResolvedValue([ { diff --git a/src/flows/model-picker.ts b/src/flows/model-picker.ts index b1cb0a084c4..6562b5d9072 100644 --- a/src/flows/model-picker.ts +++ b/src/flows/model-picker.ts @@ -611,9 +611,8 @@ export async function promptDefaultModel( if ( loadCatalog && browseCatalogOnDemand && - preferredProvider && allowKeep && - normalizeProviderId(resolved.provider) === preferredProvider + (!preferredProvider || normalizeProviderId(resolved.provider) === preferredProvider) ) { const configuredLabel = await resolveConfiguredDisplayLabel(); const options: WizardSelectOption[] = [ diff --git a/src/plugins/provider-validation.test.ts b/src/plugins/provider-validation.test.ts index 6778e57b9ad..62412b44021 100644 --- a/src/plugins/provider-validation.test.ts +++ b/src/plugins/provider-validation.test.ts @@ -101,6 +101,7 @@ describe("normalizeRegisteredProvider", () => { kind: "custom", wizard: { choiceId: " demo-primary ", + onboardingFeatured: true, modelAllowlist: { allowedKeys: [" demo/model ", "demo/model"], initialSelections: [" demo/model "], @@ -121,6 +122,7 @@ describe("normalizeRegisteredProvider", () => { wizard: { setup: { choiceId: " demo-choice ", + onboardingFeatured: true, methodId: " missing ", }, modelPicker: { @@ -142,6 +144,7 @@ describe("normalizeRegisteredProvider", () => { kind: "custom", wizard: { choiceId: "demo-primary", + onboardingFeatured: true, modelAllowlist: { allowedKeys: ["demo/model"], initialSelections: ["demo/model"], @@ -155,6 +158,7 @@ describe("normalizeRegisteredProvider", () => { wizard: { setup: { choiceId: "demo-choice", + onboardingFeatured: true, }, modelPicker: { label: "Demo models", diff --git a/src/plugins/provider-validation.ts b/src/plugins/provider-validation.ts index a1c16b606a3..9db93ae44b0 100644 --- a/src/plugins/provider-validation.ts +++ b/src/plugins/provider-validation.ts @@ -121,6 +121,7 @@ function buildNormalizedWizardSetup(params: { params.setup.assistantVisibility === "visible" ? { assistantVisibility: params.setup.assistantVisibility } : {}), + ...(params.setup.onboardingFeatured === true ? { onboardingFeatured: true } : {}), ...(groupId ? { groupId } : {}), ...(groupLabel ? { groupLabel } : {}), ...(groupHint ? { groupHint } : {}), diff --git a/src/plugins/provider-wizard.ts b/src/plugins/provider-wizard.ts index c3f4a55231e..61d16a0b930 100644 --- a/src/plugins/provider-wizard.ts +++ b/src/plugins/provider-wizard.ts @@ -27,6 +27,7 @@ export type ProviderWizardOption = { onboardingScopes?: Array<"text-inference" | "image-generation">; assistantPriority?: number; assistantVisibility?: "visible" | "manual-only"; + onboardingFeatured?: boolean; }; export type ProviderModelPickerEntry = { @@ -119,6 +120,7 @@ function buildSetupOptionForMethod(params: { ...(params.wizard.assistantVisibility ? { assistantVisibility: params.wizard.assistantVisibility } : {}), + ...(params.wizard.onboardingFeatured ? { onboardingFeatured: true } : {}), }; } diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 8dbec0b9b59..21ac8750909 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -1120,6 +1120,7 @@ export type ProviderPluginWizardSetup = { choiceHint?: string; assistantPriority?: number; assistantVisibility?: "visible" | "manual-only"; + onboardingFeatured?: boolean; groupId?: string; groupLabel?: string; groupHint?: string; diff --git a/src/wizard/setup.test.ts b/src/wizard/setup.test.ts index a3203388c2c..a3b63aab483 100644 --- a/src/wizard/setup.test.ts +++ b/src/wizard/setup.test.ts @@ -1158,7 +1158,7 @@ describe("runSetupWizard", () => { providerId: "openai-codex", methodId: "oauth", choiceId: "openai-codex", - choiceLabel: "OpenAI Codex Browser Login", + choiceLabel: "ChatGPT/Codex Browser Login", }); resolvePluginSetupProvider.mockReturnValue({ id: "openai-codex", @@ -1166,7 +1166,7 @@ describe("runSetupWizard", () => { auth: [ { id: "oauth", - label: "OpenAI Codex Browser Login", + label: "ChatGPT/Codex Browser Login", kind: "oauth", wizard: { modelSelection: {