From 8c567306ba05b6895a5d073ae73669b2fc89cf71 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 12:29:36 -0700 Subject: [PATCH] fix(openai): remove duplicate GPT-5.6 picker entry (#122178) * fix(openai): remove duplicate GPT-5.6 picker entry * test(gateway): split OpenAI model picker coverage * test(openai): align canonical model fixtures * chore(plugin-sdk): refresh agent harness API baseline --- .../agent-harness.json | 2 +- docs/concepts/model-providers.md | 15 ++-- docs/concepts/models.md | 6 +- docs/help/testing-live.md | 4 +- docs/providers/openai.md | 25 ++++--- docs/reference/wizard.md | 2 +- docs/start/wizard-cli-reference.md | 3 +- docs/start/wizard.md | 8 +-- extensions/openai/default-models.ts | 2 +- extensions/openai/openai-provider.test.ts | 14 ++-- extensions/openai/openclaw.plugin.json | 21 +----- extensions/openai/openclaw.plugin.test.ts | 9 ++- .../e2e/lib/codex-on-demand/assertions.mjs | 4 +- .../shared/legacy-config-migrate.test.ts | 71 +++++++++++++++++++ ...-config-migrations.runtime.models.codex.ts | 6 +- ...y-config-migrations.runtime.models.refs.ts | 13 ++-- ...y-config-migrations.runtime.models.test.ts | 2 +- ...legacy-config-migrations.runtime.models.ts | 6 +- src/commands/onboard-inference-ambient.ts | 2 +- src/commands/onboard-inference.test.ts | 4 +- .../gateway-models.profiles.live.test.ts | 2 +- .../models-list-result.openai-picker.test.ts | 42 +++++++++++ ...-list-result.openai-routes.test-support.ts | 68 ++++++++++++++++++ .../models-list-result.openai-routes.test.ts | 68 ++---------------- .../setup-inference-detection.test.ts | 2 +- src/system-agent/setup-inference.test.ts | 3 +- test/openai-onboarding.live.test.ts | 2 +- test/scripts/codex-install-assertions.test.ts | 2 +- 28 files changed, 263 insertions(+), 145 deletions(-) create mode 100644 src/gateway/server-methods/models-list-result.openai-picker.test.ts create mode 100644 src/gateway/server-methods/models-list-result.openai-routes.test-support.ts diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json index dba4a4794528..c37ea0235685 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json @@ -1 +1 @@ -{"contentHash":"8aef71ef2bd4a5984c9a0ddea4c4f1e6d86b9bf74504b50f8ec6a25ad65401cb","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} +{"contentHash":"48813fa984919c8790905b995f4033d19fc9178e6eed7f2e5148ea9729c5eba3","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 5f644479a81b..c482366570bf 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -39,11 +39,10 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram) Plugin auto-enable follows the same boundary: an implicitly Codex-compatible effective route can enable the Codex plugin, while explicit provider/model `agentRuntime.id: "codex"` or legacy `codex/` refs require it. An `openai/*` prefix by itself does not. - Fresh OpenAI setup uses a route-specific GPT-5.6 ref: API-key setup selects - `openai/gpt-5.6` (the bare direct-API id resolves to Sol), while - ChatGPT/Codex OAuth selects exact `openai/gpt-5.6-sol` for the native Codex - catalog. Existing explicit primaries, including `openai/gpt-5.5`, are - preserved when OpenAI auth is added or refreshed. GPT-5.5 remains available + Fresh OpenAI API-key and ChatGPT/Codex OAuth setup select the canonical + `openai/gpt-5.6-sol` ref. The bare direct-API `openai/gpt-5.6` alias remains + supported and resolves to Sol. Existing explicit primaries, including + `openai/gpt-5.5`, are preserved when OpenAI auth is added or refreshed. GPT-5.5 remains available through either runtime as an explicit recovery choice for accounts without GPT-5.6 access. @@ -105,8 +104,8 @@ Official provider plugins publish their own model catalog rows. These providers - Provider: `openai` - Auth: `OPENAI_API_KEY` - Optional rotation: `OPENAI_API_KEYS`, `OPENAI_API_KEY_1`, `OPENAI_API_KEY_2`, plus `OPENCLAW_LIVE_OPENAI_KEY` (single override) -- Fresh setup default: `openai/gpt-5.6`; on the direct API, the bare id resolves to Sol. -- Example models: `openai/gpt-5.6`, `openai/gpt-5.6-terra`, `openai/gpt-5.6-luna`, `openai/gpt-5.5` +- Fresh setup default: `openai/gpt-5.6-sol`. +- Example models: `openai/gpt-5.6-sol`, `openai/gpt-5.6-terra`, `openai/gpt-5.6-luna`, `openai/gpt-5.5`; the bare direct-API `openai/gpt-5.6` alias remains supported. - Verify account/model availability with `openclaw models list --provider openai` if a specific install or API key behaves differently. - CLI: `openclaw onboard --auth-choice openai-api-key` - Default transport is `auto`; OpenClaw passes the transport choice to the shared model runtime. @@ -120,7 +119,7 @@ Official provider plugins publish their own model catalog rows. These providers ```json5 { - agents: { defaults: { model: { primary: "openai/gpt-5.6" } } }, + agents: { defaults: { model: { primary: "openai/gpt-5.6-sol" } } }, } ``` diff --git a/docs/concepts/models.md b/docs/concepts/models.md index b6ac754a1608..cbcad3bc27af 100644 --- a/docs/concepts/models.md +++ b/docs/concepts/models.md @@ -99,9 +99,9 @@ openclaw onboard Sets up model and auth for common providers without hand-editing config, including OpenAI Codex subscription OAuth and Anthropic (API key or Claude CLI reuse). -With no primary model configured, fresh OpenAI API-key setup selects -`openai/gpt-5.6`; the bare direct-API id resolves to the Sol tier. Fresh -ChatGPT/Codex OAuth setup selects the exact `openai/gpt-5.6-sol` catalog ref. +With no primary model configured, fresh OpenAI API-key and ChatGPT/Codex OAuth +setup select the exact `openai/gpt-5.6-sol` catalog ref. The bare direct-API +`openai/gpt-5.6` alias remains supported and resolves to the Sol tier. Reauthentication preserves an existing explicit primary model, including `openai/gpt-5.5`. If GPT-5.6 is unavailable to the account, select `openai/gpt-5.5` explicitly; OpenClaw does not silently downgrade it. diff --git a/docs/help/testing-live.md b/docs/help/testing-live.md index 4d33ad5a226a..56c650d3ea8c 100644 --- a/docs/help/testing-live.md +++ b/docs/help/testing-live.md @@ -316,7 +316,7 @@ Docker notes: - Test: `src/gateway/gateway-codex-harness.live.test.ts` - Enable: `OPENCLAW_LIVE_CODEX_HARNESS=1` - Harness baseline model: `openai/gpt-5.6-luna` -- Fresh OpenAI API-key selection default: `openai/gpt-5.6` +- Fresh OpenAI API-key selection default: `openai/gpt-5.6-sol` - Default thinking: `low` - Model override: `OPENCLAW_LIVE_CODEX_HARNESS_MODEL=openai/` - Thinking override: `OPENCLAW_LIVE_CODEX_HARNESS_THINKING=` @@ -509,7 +509,7 @@ OPENCLAW_LIVE_GATEWAY_OPENAI_API_DEFAULT=1 \ ``` This proof leaves `OPENCLAW_LIVE_GATEWAY_MODELS` unset, resolves the model through -the fresh onboarding inference-selection seam, asserts `openai/gpt-5.6`, and then +the fresh onboarding inference-selection seam, asserts `openai/gpt-5.6-sol`, and then runs a real gateway turn with that resolved model. GPT-5.6 embedded OpenClaw matrix: diff --git a/docs/providers/openai.md b/docs/providers/openai.md index df83595bf639..b808f95100bc 100644 --- a/docs/providers/openai.md +++ b/docs/providers/openai.md @@ -48,7 +48,7 @@ changing config. | Goal | Use | Notes | | ------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------- | | ChatGPT/Codex subscription, native Codex runtime | `openai/gpt-5.6-sol` | Fresh subscription setup; sign in with Codex auth. | -| Direct API-key billing for agent turns | `openai/gpt-5.6` plus an ordered API-key auth profile | Fresh API-key setup; the bare direct-API id resolves to Sol. | +| Direct API-key billing for agent turns | `openai/gpt-5.6-sol` plus an ordered API-key auth profile | Fresh API-key setup uses the explicit Sol id. | | Choose an exact GPT-5.6 tier | `openai/gpt-5.6-sol`, `-terra`, or `-luna` | Check `models list` for the tiers available to this account. | | Account without GPT-5.6 access | `openai/gpt-5.5` | Explicit recovery choice; OpenClaw does not silently downgrade. | | Direct API-key billing, explicit OpenClaw runtime | `openai/gpt-5.6` plus provider/model `agentRuntime.id: "openclaw"` | Select a normal `openai` API-key profile. | @@ -116,11 +116,13 @@ lower-cost tier. See the [GPT-5.6 launch announcement](https://openai.com/index/previewing-gpt-5-6-sol/) and [access guide](https://help.openai.com/en/articles/20001325-a-preview-of-gpt-5-6-sol-terra-and-luna). -With direct OpenAI API-key auth, the bare `openai/gpt-5.6` id is an alias for -Sol and is the fresh setup default. The native Codex catalog does not apply -that direct-API alias client-side; depending on workspace access, it can show -the exact Sol, Terra, and Luna ids. Fresh ChatGPT/Codex OAuth setup therefore -uses `openai/gpt-5.6-sol`. Check the current account with: +OpenAI's [GPT-5.6 Sol model page](https://developers.openai.com/api/docs/models/gpt-5.6-sol) +documents the bare `openai/gpt-5.6` id as a supported alias for Sol. Fresh +API-key and ChatGPT/Codex OAuth setup use the canonical `openai/gpt-5.6-sol` +ref so model pickers do not show both names for the same tier. Run +`openclaw doctor --fix` to rewrite persisted bare OpenAI refs to that canonical +identity. The native Codex catalog can show the exact Sol, Terra, and Luna ids depending on +workspace access. Check the current account with: ```bash openclaw models list --provider openai @@ -266,13 +268,13 @@ for the full example. ```json5 { env: { vars: { OPENAI_API_KEY: "example-openai-key-not-real" } }, - agents: { defaults: { model: { primary: "openai/gpt-5.6" } } }, + agents: { defaults: { model: { primary: "openai/gpt-5.6-sol" } } }, } ``` - The bare direct-API `gpt-5.6` id resolves to the Sol tier. If this API - organization does not expose GPT-5.6, set the primary to - `openai/gpt-5.5` explicitly. + The bare direct-API `gpt-5.6` alias is also accepted and resolves to the + Sol tier. If this API organization does not expose GPT-5.6, set the primary + to `openai/gpt-5.5` explicitly. To try ChatGPT's current Instant model from the OpenAI API, set the model to `openai/chat-latest`: @@ -285,7 +287,8 @@ for the full example. ``` `chat-latest` is a moving alias. Fresh OpenAI API-key setup instead uses - `openai/gpt-5.6`, whose bare direct-API id resolves to Sol. Existing + `openai/gpt-5.6-sol`. The bare direct-API `openai/gpt-5.6` alias remains + supported and resolves to Sol. Existing explicit primaries, including `openai/gpt-5.5`, remain unchanged. The `chat-latest` alias only accepts `medium` text verbosity; OpenClaw forces any other requested verbosity to `medium` for this model. diff --git a/docs/reference/wizard.md b/docs/reference/wizard.md index f85ba4ad6e8b..86a59eb6222b 100644 --- a/docs/reference/wizard.md +++ b/docs/reference/wizard.md @@ -44,7 +44,7 @@ behavior and outputs, see [CLI setup reference](/start/wizard-cli-reference). - **OpenAI Code (Codex) subscription (device pairing)**: browser pairing flow with a short-lived device code. - On a fresh setup with no primary model, sets `agents.defaults.model` to `openai/gpt-5.6-sol` through the Codex runtime. - **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then stores it in auth profiles. - - On a fresh setup with no primary model, sets `agents.defaults.model` to `openai/gpt-5.6`; the bare direct-API model id resolves to the Sol tier. + - On a fresh setup with no primary model, sets `agents.defaults.model` to `openai/gpt-5.6-sol`. The bare direct-API `openai/gpt-5.6` alias remains supported and resolves to the same tier. - Adding or reauthenticating OpenAI preserves an existing explicit primary model, including `openai/gpt-5.5`. If the account does not expose GPT-5.6, select `openai/gpt-5.5` explicitly; OpenClaw does not silently downgrade the model. - **xAI OAuth**: device-code browser sign-in with no localhost callback required, so it works over SSH/Docker/VPS too (`--auth-choice xai-oauth`). - **xAI API key**: prompts for `XAI_API_KEY` (`--auth-choice xai-api-key`). diff --git a/docs/start/wizard-cli-reference.md b/docs/start/wizard-cli-reference.md index c5a0e9107de3..c08e61b4a90b 100644 --- a/docs/start/wizard-cli-reference.md +++ b/docs/start/wizard-cli-reference.md @@ -189,7 +189,8 @@ instead of exiting. Explicit `--auth-choice` runs still fail fast for automation Uses `OPENAI_API_KEY` if present or prompts for a key, then stores the credential in auth profiles. On a fresh setup with no primary model, sets `agents.defaults.model` to - `openai/gpt-5.6`; the bare direct-API model id resolves to the Sol tier. + `openai/gpt-5.6-sol`. The bare direct-API `openai/gpt-5.6` alias remains + supported and resolves to the same tier. Adding or reauthenticating OpenAI preserves an existing explicit primary model, including `openai/gpt-5.5`. If the account does not expose GPT-5.6, diff --git a/docs/start/wizard.md b/docs/start/wizard.md index f71bd9e09a93..31fd64412e9a 100644 --- a/docs/start/wizard.md +++ b/docs/start/wizard.md @@ -151,10 +151,10 @@ Local mode (default) walks through these steps: provider-specific manual auth), including Custom Provider (OpenAI-compatible, OpenAI Responses-compatible, Anthropic-compatible, or Unknown auto-detect). Pick a default model. - Fresh OpenAI API-key setup defaults to `openai/gpt-5.6` (the bare direct-API - id resolves to Sol); fresh ChatGPT/Codex setup defaults to - `openai/gpt-5.6-sol`. Re-running setup preserves an existing explicit model, - including `openai/gpt-5.5`. Select `openai/gpt-5.5` explicitly if the + Fresh OpenAI API-key and ChatGPT/Codex setup default to + `openai/gpt-5.6-sol`. The bare direct-API `openai/gpt-5.6` alias remains + supported and resolves to Sol. Re-running setup preserves an existing + explicit model, including `openai/gpt-5.5`. Select `openai/gpt-5.5` explicitly if the account does not expose GPT-5.6. Security note: if this agent will run tools or process webhook/hook content, prefer the strongest latest-generation model available and keep diff --git a/extensions/openai/default-models.ts b/extensions/openai/default-models.ts index 8b187e5293b0..976c0651d5f3 100644 --- a/extensions/openai/default-models.ts +++ b/extensions/openai/default-models.ts @@ -6,7 +6,7 @@ import { type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; -export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.6"; +export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.6-sol"; export const OPENAI_CODEX_DEFAULT_MODEL = "openai/gpt-5.6-sol"; export const OPENAI_DEFAULT_IMAGE_MODEL = "gpt-image-2"; export const OPENAI_DEFAULT_TTS_MODEL = "gpt-4o-mini-tts"; diff --git a/extensions/openai/openai-provider.test.ts b/extensions/openai/openai-provider.test.ts index d93d718a7c01..d5191ee1b04e 100644 --- a/extensions/openai/openai-provider.test.ts +++ b/extensions/openai/openai-provider.test.ts @@ -290,7 +290,7 @@ describe("buildOpenAIProvider", () => { groupLabel: "OpenAI", groupHint: "ChatGPT/Codex sign-in or API key", }); - expect(apiKey?.starterModel).toBe("openai/gpt-5.6"); + expect(apiKey?.starterModel).toBe("openai/gpt-5.6-sol"); }); it("preserves existing model selection during non-interactive API key setup", async () => { @@ -378,7 +378,7 @@ describe("buildOpenAIProvider", () => { } }); - it("keeps a network-free OpenAI static catalog", async () => { + it("keeps a network-free OpenAI static catalog without the duplicate GPT-5.6 alias", async () => { const provider = buildOpenAIProvider(); const result = await provider.staticCatalog?.run({ @@ -406,22 +406,20 @@ describe("buildOpenAIProvider", () => { image: { maxSidePx: 6000, preferredSidePx: 2048, tokenMode: "detail" }, }); expect(gpt56Models?.map((model) => model.id)).toEqual([ - "gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", ]); expect(gpt56Models?.map((model) => model.contextWindow)).toEqual([ - 1_050_000, 1_050_000, 1_050_000, 1_050_000, + 1_050_000, 1_050_000, 1_050_000, ]); expect(gpt56Models?.map((model) => model.thinkingLevelMap?.off)).toEqual([ "none", "none", "none", - "none", ]); expect(gpt56Models?.map((model) => model.compat?.supportedReasoningEfforts)).toEqual( - Array.from({ length: 4 }, () => ["none", "low", "medium", "high", "xhigh", "max"]), + Array.from({ length: 3 }, () => ["none", "low", "medium", "high", "xhigh", "max"]), ); expect(gpt54Models).toMatchObject([ { @@ -461,7 +459,7 @@ describe("buildOpenAIProvider", () => { cost: { input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0 }, }, ]); - expect(OPENAI_DEFAULT_MODEL).toBe("openai/gpt-5.6"); + expect(OPENAI_DEFAULT_MODEL).toBe("openai/gpt-5.6-sol"); expect(OPENAI_CODEX_DEFAULT_MODEL).toBe("openai/gpt-5.6-sol"); }); @@ -570,7 +568,7 @@ describe("buildOpenAIProvider", () => { }); expect(provider.apiKey).toBe("sk-openai"); - expect(provider.models.map((model) => model.id)).toContain("gpt-5.6"); + expect(provider.models.map((model) => model.id)).not.toContain("gpt-5.6"); expect(provider.models.map((model) => model.id)).toContain("gpt-5.5"); expect(provider.models.map((model) => model.id)).toEqual( expect.arrayContaining([ diff --git a/extensions/openai/openclaw.plugin.json b/extensions/openai/openclaw.plugin.json index b3a3feedfb3d..2c2f40d27991 100644 --- a/extensions/openai/openclaw.plugin.json +++ b/extensions/openai/openclaw.plugin.json @@ -40,23 +40,6 @@ "api": "openai-responses", "defaultUtilityModel": "gpt-5.6-luna", "models": [ - { - "id": "gpt-5.6", - "name": "GPT-5.6", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 1050000, - "contextTokens": 272000, - "maxTokens": 128000, - "cost": { "input": 5, "output": 30, "cacheRead": 0.5, "cacheWrite": 6.25 }, - "thinkingLevelMap": { "off": "none", "xhigh": "xhigh", "max": "max" }, - "compat": { - "supportsReasoningEffort": true, - "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"], - "supportsTemperature": false, - "codeMode": "preferred" - } - }, { "id": "gpt-5.6-sol", "name": "GPT-5.6 Sol", @@ -112,7 +95,7 @@ "id": "gpt-5.5", "name": "GPT-5.5", "status": "deprecated", - "replacedBy": "gpt-5.6", + "replacedBy": "gpt-5.6-sol", "reasoning": true, "input": ["text", "image"], "mediaInput": { @@ -128,7 +111,7 @@ "id": "gpt-5.5-pro", "name": "gpt-5.5-pro", "status": "deprecated", - "replacedBy": "gpt-5.6", + "replacedBy": "gpt-5.6-sol", "reasoning": true, "input": ["text", "image"], "mediaInput": { diff --git a/extensions/openai/openclaw.plugin.test.ts b/extensions/openai/openclaw.plugin.test.ts index 4350decfbbd1..ae4a4e9e3adb 100644 --- a/extensions/openai/openclaw.plugin.test.ts +++ b/extensions/openai/openclaw.plugin.test.ts @@ -4,7 +4,6 @@ import { describe, expect, it } from "vitest"; import { OPENAI_GPT_55_MODEL_ID, OPENAI_GPT_55_PRO_MODEL_ID, - OPENAI_GPT_56_MODEL_ID, OPENAI_GPT_56_VARIANT_MODEL_IDS, } from "./model-route-contract.js"; import { buildOpenAIProvider } from "./openai-provider.js"; @@ -115,7 +114,6 @@ describe("OpenAI plugin manifest", () => { it("keeps million-token OpenAI models on the ordinary runtime budget by default", () => { const models = manifest.modelCatalog?.providers?.openai?.models ?? []; for (const id of [ - OPENAI_GPT_56_MODEL_ID, ...OPENAI_GPT_56_VARIANT_MODEL_IDS, OPENAI_GPT_55_MODEL_ID, OPENAI_GPT_55_PRO_MODEL_ID, @@ -130,6 +128,13 @@ describe("OpenAI plugin manifest", () => { } }); + it("replaces deprecated GPT-5.5 models with canonical GPT-5.6 Sol", () => { + const models = manifest.modelCatalog?.providers?.openai?.models ?? []; + for (const id of [OPENAI_GPT_55_MODEL_ID, OPENAI_GPT_55_PRO_MODEL_ID]) { + expect(models.find((model) => model.id === id)?.replacedBy, id).toBe("gpt-5.6-sol"); + } + }); + it("labels OpenAI API key and Codex auth choices without stale mixed OAuth wording", () => { const choices = manifest.providerAuthChoices ?? []; const openAiLogin = choices.find((choice) => choice.choiceId === "openai"); diff --git a/scripts/e2e/lib/codex-on-demand/assertions.mjs b/scripts/e2e/lib/codex-on-demand/assertions.mjs index 19c389cc388b..d6633c5fd927 100644 --- a/scripts/e2e/lib/codex-on-demand/assertions.mjs +++ b/scripts/e2e/lib/codex-on-demand/assertions.mjs @@ -117,8 +117,8 @@ if (!hasHarness) { } const primaryModel = cfg.agents?.defaults?.model?.primary; -if (primaryModel !== "openai/gpt-5.6") { - throw new Error(`expected OpenAI onboarding model openai/gpt-5.6, got ${primaryModel}`); +if (primaryModel !== "openai/gpt-5.6-sol") { + throw new Error(`expected OpenAI onboarding model openai/gpt-5.6-sol, got ${primaryModel}`); } const providerRuntime = cfg.models?.providers?.openai?.agentRuntime?.id; if (providerRuntime && providerRuntime !== "codex") { diff --git a/src/commands/doctor/shared/legacy-config-migrate.test.ts b/src/commands/doctor/shared/legacy-config-migrate.test.ts index 187040b3d25d..a802b5ab8abd 100644 --- a/src/commands/doctor/shared/legacy-config-migrate.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrate.test.ts @@ -3666,6 +3666,77 @@ describe("legacy model compat migrate", () => { expect(res.config?.models?.providers?.openai?.models?.[0]?.id).toBe("gpt-5.5"); }); + it("canonicalizes persisted OpenAI GPT-5.6 aliases without affecting GitHub Copilot", () => { + const legacy = "openai/gpt-5.6"; + const canonical = "openai/gpt-5.6-sol"; + const res = migrateLegacyConfigForTest({ + agents: { + defaults: { + model: { + primary: `${legacy}@openai:work`, + fallbacks: [legacy, "github-copilot/gpt-5.6"], + }, + modelPolicy: { allow: [legacy, "github-copilot/gpt-5.6"] }, + models: { + [legacy]: { + alias: "GPT", + agentRuntime: { id: "openclaw" }, + params: { temperature: 0.2, nested: { fromAlias: true } }, + }, + [canonical]: { + params: { serviceTier: "priority", nested: { fromCanonical: true } }, + }, + "github-copilot/gpt-5.6": { alias: "Copilot GPT" }, + }, + }, + }, + models: { + providers: { + openai: { + models: [ + { id: "gpt-5.6", name: "GPT alias", maxTokens: 64_000 }, + { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextWindow: 1_050_000 }, + ], + }, + "github-copilot": { models: [{ id: "gpt-5.6", name: "Copilot GPT" }] }, + }, + }, + }); + + expect(res.config?.agents?.defaults).toMatchObject({ + model: { + primary: `${canonical}@openai:work`, + fallbacks: [canonical, "github-copilot/gpt-5.6"], + }, + modelPolicy: { allow: [canonical, "github-copilot/gpt-5.6"] }, + models: { + [canonical]: { + alias: "GPT", + agentRuntime: { id: "openclaw" }, + params: { + serviceTier: "priority", + temperature: 0.2, + nested: { fromAlias: true, fromCanonical: true }, + }, + }, + "github-copilot/gpt-5.6": { alias: "Copilot GPT" }, + }, + }); + expect(res.config?.agents?.defaults?.models).not.toHaveProperty(legacy); + expect(res.config?.models?.providers?.openai?.models).toEqual([ + { + id: "gpt-5.6-sol", + name: "GPT-5.6 Sol", + contextWindow: 1_050_000, + maxTokens: 64_000, + }, + ]); + expect(res.config?.models?.providers?.["github-copilot"]?.models).toEqual([ + { id: "gpt-5.6", name: "Copilot GPT" }, + ]); + expect(migrateLegacyConfigForTest(res.config)).toEqual({ config: null, changes: [] }); + }); + it("merges provider catalog rows that normalize to an explicitly canonical id", () => { const res = migrateLegacyConfigForTest({ models: { diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.codex.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.codex.ts index f8cc3d014d71..77825e4812c7 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.codex.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.codex.ts @@ -9,7 +9,7 @@ import { type LegacyCodexModelIdentity, } from "./codex-route-model-ref.js"; import { - RETIRED_MODEL_REF_MESSAGE, + MODEL_REF_CANONICALIZATION_MESSAGE, hasOwnDefinedProperty, scanKnownModelRefs, } from "./legacy-config-migrations.runtime.models.refs.js"; @@ -592,7 +592,7 @@ export function migrateLegacyOpenAICodexProvider( } } -export const RETIRED_MODEL_REF_RULES: LegacyConfigRule[] = [ +export const MODEL_REF_CANONICALIZATION_RULES: LegacyConfigRule[] = [ "agents", "plugins", "messages", @@ -602,6 +602,6 @@ export const RETIRED_MODEL_REF_RULES: LegacyConfigRule[] = [ "models", ].map((section) => ({ path: [section], - message: RETIRED_MODEL_REF_MESSAGE, + message: MODEL_REF_CANONICALIZATION_MESSAGE, match: (value) => scanKnownModelRefs(value), })); diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.refs.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.refs.ts index 3043a32025b1..362f138672ba 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.refs.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.refs.ts @@ -215,7 +215,7 @@ function upgradeOldClaudeModelPart(model: string, provider: string | undefined): return upgradeOldClaudeToken(model, separator, provider); } -function upgradeRetiredModelRef(value: string): string | null { +function canonicalizeKnownModelRef(value: string): string | null { const trimmed = value.trim(); if (!trimmed) { return null; @@ -227,6 +227,9 @@ function upgradeRetiredModelRef(value: string): string | null { const model = slash > 0 ? modelRef.slice(slash + 1).trim() : modelRef; const normalizedProvider = normalizeString(provider); const normalizedModel = normalizeString(model); + if (normalizedProvider === "openai" && normalizedModel === "gpt-5.6") { + return `${provider}/gpt-5.6-sol${split.profile ? `@${split.profile}` : ""}`; + } const retiredOwnerModel = normalizedProvider === "groq" ? applyRetiredModelTable(model, RETIRED_GROQ_MODELS) @@ -279,7 +282,7 @@ function normalizeKnownModelRef(value: string): string | null { ? normalizeAgentModelRefForConfig(split.model) : split.model; const normalized = `${normalizedModel}${split.profile ? `@${split.profile}` : ""}`; - return upgradeRetiredModelRef(normalized) ?? (normalized === value ? null : normalized); + return canonicalizeKnownModelRef(normalized) ?? (normalized === value ? null : normalized); } const MODEL_REF_STRING_KEYS = new Set([ @@ -333,7 +336,7 @@ function normalizeProviderCatalogModelId(provider: string, modelId: string): str normalizeString(trimmed).startsWith("google/") ? normalizeConfiguredProviderCatalogModelId(provider, trimmed) : trimmed; - const upgradedRef = upgradeRetiredModelRef(`${provider}/${normalized}`); + const upgradedRef = canonicalizeKnownModelRef(`${provider}/${normalized}`); if (!upgradedRef) { return normalized; } @@ -734,5 +737,5 @@ export function rewriteKnownModelRefs( return { value: changed ? next : value, changed }; } -export const RETIRED_MODEL_REF_MESSAGE = - 'Configured retired model refs are no longer in the bundled catalogs; run "openclaw doctor --fix" to upgrade them.'; +export const MODEL_REF_CANONICALIZATION_MESSAGE = + 'Configured retired or noncanonical model refs are no longer in the bundled catalogs; run "openclaw doctor --fix" to upgrade them.'; diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.test.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.test.ts index 269e1bc9f110..d4b06b730768 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.test.ts @@ -48,7 +48,7 @@ describe("model compat catalog ownership migration", () => { baseUrl: "https://api.openai.com/v1", models: [ { - id: "gpt-5.6", + id: "gpt-5.6-sol", compat: { supportsReasoningEffort: true, supportsTemperature: true, diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts index a666469381e8..3398cf52f7a6 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts @@ -100,9 +100,9 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS = [ apply: codex.migrateLegacyOpenAICodexProvider, }), defineLegacyConfigMigration({ - id: "models.retired-model-refs", - describe: "Upgrade retired model refs to current catalog entries", - legacyRules: codex.RETIRED_MODEL_REF_RULES, + id: "models.canonical-model-refs", + describe: "Canonicalize retired and noncanonical model refs", + legacyRules: codex.MODEL_REF_CANONICALIZATION_RULES, apply: (raw, changes) => { const rewritten = refs.rewriteKnownModelRefs(raw, "config", changes); const rewrittenRecord = getRecord(rewritten.value); diff --git a/src/commands/onboard-inference-ambient.ts b/src/commands/onboard-inference-ambient.ts index 9963ddbc8e81..f871d921d0e3 100644 --- a/src/commands/onboard-inference-ambient.ts +++ b/src/commands/onboard-inference-ambient.ts @@ -1,4 +1,4 @@ -export const OPENAI_API_DEFAULT_MODEL_REF = "openai/gpt-5.6"; +export const OPENAI_API_DEFAULT_MODEL_REF = "openai/gpt-5.6-sol"; export const ANTHROPIC_API_DEFAULT_MODEL_REF = "anthropic/claude-opus-5"; export const CLAUDE_CLI_DEFAULT_MODEL_REF = "claude-cli/claude-opus-5"; export const CODEX_APP_SERVER_DEFAULT_MODEL_REF = "openai/gpt-5.6-sol"; diff --git a/src/commands/onboard-inference.test.ts b/src/commands/onboard-inference.test.ts index 1132e05de898..6f9d424a79b3 100644 --- a/src/commands/onboard-inference.test.ts +++ b/src/commands/onboard-inference.test.ts @@ -17,8 +17,8 @@ function probeDeps(found: Record) { } describe("detectInferenceBackends", () => { - it("uses route-specific GPT-5.6 defaults for direct API and Codex", () => { - expect(OPENAI_API_DEFAULT_MODEL_REF).toBe("openai/gpt-5.6"); + it("uses canonical GPT-5.6 Sol defaults for direct API and Codex", () => { + expect(OPENAI_API_DEFAULT_MODEL_REF).toBe("openai/gpt-5.6-sol"); expect(CODEX_APP_SERVER_DEFAULT_MODEL_REF).toBe("openai/gpt-5.6-sol"); }); diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index 63e1a3d6b000..b9930b2e20d0 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -4392,7 +4392,7 @@ async function resolveGatewayLiveRequestedModels(): Promise if (!selected) { throw new Error("fresh OpenAI API-key inference selection returned no candidate"); } - expect(selected.modelRef).toBe("openai/gpt-5.6"); + expect(selected.modelRef).toBe("openai/gpt-5.6-sol"); return selected.modelRef; } diff --git a/src/gateway/server-methods/models-list-result.openai-picker.test.ts b/src/gateway/server-methods/models-list-result.openai-picker.test.ts new file mode 100644 index 000000000000..85b0cf51edfc --- /dev/null +++ b/src/gateway/server-methods/models-list-result.openai-picker.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { migrateLegacyConfig } from "../../commands/doctor/shared/legacy-config-migrate.js"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { withEnvAsync } from "../../test-utils/env.js"; +import { + catalogEntry, + listModels, + WITHOUT_OPENAI_ENV_AUTH, +} from "./models-list-result.openai-routes.test-support.js"; + +describe("models.list OpenAI picker", () => { + it("does not expose a configured GPT-5.6 alias beside named variants after doctor normalization", async () => { + const staleConfig = { + agents: { + defaults: { + model: { primary: "openai/gpt-5.6" }, + models: { + "openai/gpt-5.6": { alias: "GPT" }, + "openai/gpt-5.6-sol": {}, + "openai/gpt-5.6-terra": {}, + "openai/gpt-5.6-luna": {}, + }, + }, + }, + } as OpenClawConfig; + const cfg = migrateLegacyConfig(staleConfig).config ?? staleConfig; + const catalog = [ + { ...catalogEntry("gpt-5.6-sol", "openai-responses"), providerOrder: 0 }, + { ...catalogEntry("gpt-5.6-terra", "openai-responses"), providerOrder: 1 }, + { ...catalogEntry("gpt-5.6-luna", "openai-responses"), providerOrder: 2 }, + ]; + + await withEnvAsync({ ...WITHOUT_OPENAI_ENV_AUTH, OPENAI_API_KEY: "test-key" }, async () => { + const result = await listModels({ catalog, cfg, view: "configured" }); + expect(result.models.map((entry) => entry.id)).toEqual([ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + ]); + }); + }); +}); diff --git a/src/gateway/server-methods/models-list-result.openai-routes.test-support.ts b/src/gateway/server-methods/models-list-result.openai-routes.test-support.ts new file mode 100644 index 000000000000..5c6ce850411a --- /dev/null +++ b/src/gateway/server-methods/models-list-result.openai-routes.test-support.ts @@ -0,0 +1,68 @@ +import { vi } from "vitest"; +import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js"; +import type { createOpenAIModelRoutesResolver } from "../../agents/openai-model-routes.js"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { buildModelsListResult } from "./models-list-result.js"; +import type { GatewayRequestContext } from "./types.js"; + +export const WITHOUT_OPENAI_ENV_AUTH = { + CODEX_API_KEY: undefined, + CODEX_HOME: "/__openclaw_models_list_test__/codex", + OPENAI_API_KEY: undefined, + OPENAI_BASE_URL: undefined, + OPENAI_OAUTH_TOKEN: undefined, + CHATGPT_OAUTH_TOKEN: undefined, +} as const; + +export function catalogEntry(id: string, api: ModelCatalogEntry["api"]): ModelCatalogEntry { + return { id, name: id, provider: "openai", api }; +} + +export function providerCatalogEntry(provider: string, id: string): ModelCatalogEntry { + return { ...catalogEntry(id, "openai-completions"), provider }; +} + +export async function listModels(params: { + catalog: ModelCatalogEntry[]; + cfg?: OpenClawConfig; + discoveryModes?: Record; + routeResolverFactory?: typeof createOpenAIModelRoutesResolver; + view?: "all" | "configured" | "provider-config" | "default"; +}) { + const config = params.cfg ?? ({} as OpenClawConfig); + const context = { + getRuntimeConfig: () => config, + loadGatewayModelCatalog: vi.fn(() => Promise.resolve(params.catalog)), + loadGatewayModelCatalogSnapshot: vi.fn(() => + Promise.resolve({ + agentId: "main", + agentDir: "/tmp/models-list-openai-agent", + config, + entries: params.catalog, + routeVariants: params.catalog, + }), + ), + logGateway: { debug: vi.fn() }, + } as unknown as GatewayRequestContext; + return await buildModelsListResult({ + context, + params: { view: params.view ?? "all" }, + ...(params.discoveryModes + ? { + preloadedCatalog: { + agentId: "main", + config, + snapshot: { entries: params.catalog, routeVariants: params.catalog }, + }, + catalogProjector: { + metadataSnapshot: { + plugins: [ + { id: "test-provider", modelCatalog: { discovery: params.discoveryModes } }, + ], + }, + } as never, + } + : {}), + ...(params.routeResolverFactory ? { routeResolverFactory: params.routeResolverFactory } : {}), + }); +} diff --git a/src/gateway/server-methods/models-list-result.openai-routes.test.ts b/src/gateway/server-methods/models-list-result.openai-routes.test.ts index a581f3363aa6..6e92cf926708 100644 --- a/src/gateway/server-methods/models-list-result.openai-routes.test.ts +++ b/src/gateway/server-methods/models-list-result.openai-routes.test.ts @@ -1,76 +1,20 @@ import { describe, expect, it, vi } from "vitest"; import type { ModelCatalogEntry, ModelCatalogSnapshot } from "../../agents/model-catalog.types.js"; -import type { createOpenAIModelRoutesResolver } from "../../agents/openai-model-routes.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { withEnvAsync } from "../../test-utils/env.js"; import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; import { buildModelsListResult } from "./models-list-result.js"; +import { + catalogEntry, + listModels, + providerCatalogEntry, + WITHOUT_OPENAI_ENV_AUTH, +} from "./models-list-result.openai-routes.test-support.js"; import type { GatewayRequestContext } from "./types.js"; -const WITHOUT_OPENAI_ENV_AUTH = { - CODEX_API_KEY: undefined, - CODEX_HOME: "/__openclaw_models_list_test__/codex", - OPENAI_API_KEY: undefined, - OPENAI_BASE_URL: undefined, - OPENAI_OAUTH_TOKEN: undefined, - CHATGPT_OAUTH_TOKEN: undefined, -} as const; const IMPLICIT_CODEX_RUNTIME = { id: "codex", source: "implicit" } as const; const IMPLICIT_OPENCLAW_RUNTIME = { id: "openclaw", source: "implicit" } as const; -function catalogEntry(id: string, api: ModelCatalogEntry["api"]): ModelCatalogEntry { - return { id, name: id, provider: "openai", api }; -} - -function providerCatalogEntry(provider: string, id: string): ModelCatalogEntry { - return { ...catalogEntry(id, "openai-completions"), provider }; -} - -async function listModels(params: { - catalog: ModelCatalogEntry[]; - cfg?: OpenClawConfig; - discoveryModes?: Record; - routeResolverFactory?: typeof createOpenAIModelRoutesResolver; - view?: "all" | "configured" | "provider-config" | "default"; -}) { - const config = params.cfg ?? ({} as OpenClawConfig); - const context = { - getRuntimeConfig: () => config, - loadGatewayModelCatalog: vi.fn(() => Promise.resolve(params.catalog)), - loadGatewayModelCatalogSnapshot: vi.fn(() => - Promise.resolve({ - agentId: "main", - agentDir: "/tmp/models-list-openai-agent", - config, - entries: params.catalog, - routeVariants: params.catalog, - }), - ), - logGateway: { debug: vi.fn() }, - } as unknown as GatewayRequestContext; - return await buildModelsListResult({ - context, - params: { view: params.view ?? "all" }, - ...(params.discoveryModes - ? { - preloadedCatalog: { - agentId: "main", - config, - snapshot: { entries: params.catalog, routeVariants: params.catalog }, - }, - catalogProjector: { - metadataSnapshot: { - plugins: [ - { id: "test-provider", modelCatalog: { discovery: params.discoveryModes } }, - ], - }, - } as never, - } - : {}), - ...(params.routeResolverFactory ? { routeResolverFactory: params.routeResolverFactory } : {}), - }); -} - describe("models.list OpenAI routes", () => { it("does not reuse a preloaded catalog owned by another agent", async () => { const config = { diff --git a/src/system-agent/setup-inference-detection.test.ts b/src/system-agent/setup-inference-detection.test.ts index 4e1858cb083e..060f18a74d62 100644 --- a/src/system-agent/setup-inference-detection.test.ts +++ b/src/system-agent/setup-inference-detection.test.ts @@ -201,7 +201,7 @@ describe("isolated setup inference detection", () => { { kind: "openai-api-key", brandId: "openai", - modelRef: "openai/gpt-5.6", + modelRef: "openai/gpt-5.6-sol", label: "OpenAI API key", detail: "OPENAI_API_KEY set", credentials: true, diff --git a/src/system-agent/setup-inference.test.ts b/src/system-agent/setup-inference.test.ts index 7c8a946efd25..ff6fa5e015d1 100644 --- a/src/system-agent/setup-inference.test.ts +++ b/src/system-agent/setup-inference.test.ts @@ -2074,6 +2074,7 @@ describe("activateSetupInference", () => { const result = await activateSetupInference({ kind: "openai-api-key", + modelRef: "openai/gpt-5.6", deps: { readConfigFileSnapshot: mockConfigSnapshot(sourceConfig, { runtimeConfig }), runEmbeddedAgent: runEmbeddedAgent as never, @@ -2124,7 +2125,7 @@ describe("activateSetupInference", () => { const result = await activateSetupInference({ kind: "openai-api-key", deps: { - runEmbeddedAgent: vi.fn(successfulRunner("openai", "gpt-5.6")) as never, + runEmbeddedAgent: vi.fn(successfulRunner("openai", "gpt-5.6-sol")) as never, transformConfigWithPendingPluginInstalls: configHarness.transform as never, // The real revalidation throws when the current route owner no longer // matches the probe credential (e.g. a Codex-imported OAuth profile diff --git a/test/openai-onboarding.live.test.ts b/test/openai-onboarding.live.test.ts index 3561de147a70..9eba4f9faeff 100644 --- a/test/openai-onboarding.live.test.ts +++ b/test/openai-onboarding.live.test.ts @@ -241,7 +241,7 @@ describeLive("fresh OpenAI onboarding live", () => { agents?: { defaults?: { model?: { primary?: string }; workspace?: string } }; gateway?: { mode?: string; auth?: { mode?: string; token?: string } }; }; - expect(config.agents?.defaults?.model?.primary).toBe("openai/gpt-5.6"); + expect(config.agents?.defaults?.model?.primary).toBe("openai/gpt-5.6-sol"); expect(config.agents?.defaults?.workspace).toBe( path.join(state.home, ".openclaw", "workspace"), ); diff --git a/test/scripts/codex-install-assertions.test.ts b/test/scripts/codex-install-assertions.test.ts index d43fbd2d8648..9d85e9b34723 100644 --- a/test/scripts/codex-install-assertions.test.ts +++ b/test/scripts/codex-install-assertions.test.ts @@ -548,7 +548,7 @@ function createCodexInstallFixture(root: string) { }); chmodSync(codexBin, 0o755); writeJson(path.join(stateDir, "openclaw.json"), { - agents: { defaults: { model: { primary: "openai/gpt-5.6" } } }, + agents: { defaults: { model: { primary: "openai/gpt-5.6-sol" } } }, models: { providers: { openai: { agentRuntime: { id: "codex" } } } }, }); writePluginInstallIndexForE2E(