From ab5d143d59dd0b8764cea67bb6edfa58fb2d5f8f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 10 Jul 2026 10:22:58 +0100 Subject: [PATCH] feat(openai): default new setups to GPT-5.6 (#103581) * feat(openai): default fresh setup to GPT-5.6 * test(crestodian): expect GPT-5.6 Codex defaults * test(crestodian): expect GPT-5.6 bootstrap default --- CHANGELOG.md | 1 + docs/cli/crestodian.md | 20 +-- docs/concepts/model-providers.md | 29 ++-- docs/concepts/models.md | 9 +- docs/help/faq-first-run.md | 26 ++-- docs/providers/openai.md | 130 +++++++++++------- docs/reference/wizard.md | 7 +- docs/start/wizard-cli-reference.md | 13 +- docs/start/wizard.md | 5 + extensions/codex/src/migration/auth.ts | 2 +- .../codex/src/migration/provider.test.ts | 8 +- extensions/migrate-hermes/auth.ts | 15 +- extensions/migrate-hermes/secrets.test.ts | 12 ++ extensions/openai/default-models.test.ts | 60 +++++++- extensions/openai/default-models.ts | 29 +++- .../openai/openai-chatgpt-provider.test.ts | 17 +++ .../openai-provider-missing-auth.test.ts | 12 +- extensions/openai/openai-provider.test.ts | 111 +++++++++++++-- extensions/openai/openai-provider.ts | 68 +++++++-- extensions/openai/openclaw.plugin.json | 32 +++-- .../provider-catalog.contract-test-support.ts | 2 +- .../src/model-selection.runtime.test.ts | 4 +- .../src/providers/live-frontier/index.ts | 2 +- .../providers/openai-reasoning-effort.test.ts | 5 +- .../src/providers/openai-reasoning-effort.ts | 5 +- .../e2e/lib/codex-on-demand/assertions.mjs | 4 +- src/agents/live-model-filter.ts | 10 +- src/agents/model-catalog-visibility.test.ts | 19 ++- src/agents/model-catalog-visibility.ts | 3 + src/agents/model-compat.test.ts | 26 +++- src/commands/onboard-inference.test.ts | 5 + src/commands/onboard-inference.ts | 5 +- src/crestodian/assistant.test.ts | 8 +- src/crestodian/operations.test.ts | 10 +- src/crestodian/setup-inference.test.ts | 30 ++-- src/plugins/provider-api-key-auth.ts | 15 +- .../provider-auth-choice-helpers.test.ts | 32 +++++ src/plugins/provider-auth-choice-helpers.ts | 38 ++++- test/scripts/codex-install-assertions.test.ts | 2 +- 39 files changed, 652 insertions(+), 179 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b0349937ba..aeaada54f7a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- **OpenAI GPT-5.6 defaults:** use `openai/gpt-5.6` (Sol alias) for fresh API-key setup and exact `openai/gpt-5.6-sol` for fresh Codex/OAuth setup, while preserving existing primaries, fallbacks, aliases, and explicit GPT-5.5 selections. (#103234) - **Meta provider:** add bundled `muse-spark-1.1` model support with Responses API streaming, tool calls, encrypted reasoning replay, onboarding, and standalone npm/ClawHub distribution. (#102873) Thanks @HamidShojanazeri. - **Android chat agent selector:** switch the active agent directly from the live chat screen while keeping chat, Talk mode, and home canvas on the same canonical session. (#80422) Thanks @bcperry. - **Gateway host status:** show the connected Gateway's host, network address, OS, runtime, uptime, CPU, memory, and disk details in Control UI Settings. (#100478) diff --git a/docs/cli/crestodian.md b/docs/cli/crestodian.md index 67ad6777b714..be7b0a477471 100644 --- a/docs/cli/crestodian.md +++ b/docs/cli/crestodian.md @@ -50,8 +50,8 @@ openclaw crestodian openclaw crestodian --json openclaw crestodian --message "models" openclaw crestodian --message "validate config" -openclaw crestodian --message "setup workspace ~/Projects/work model openai/gpt-5.5" --yes -openclaw crestodian --message "set default model openai/gpt-5.5" --yes +openclaw crestodian --message "setup workspace ~/Projects/work model openai/gpt-5.6" --yes +openclaw crestodian --message "set default model openai/gpt-5.6" --yes openclaw onboard --modern ``` @@ -64,7 +64,7 @@ doctor doctor fix validate config setup -setup workspace ~/Projects/work model openai/gpt-5.5 +setup workspace ~/Projects/work model openai/gpt-5.6 config set gateway.port 19001 config set-ref gateway.auth.token env OPENCLAW_GATEWAY_TOKEN gateway status @@ -73,7 +73,7 @@ agents create agent work workspace ~/Projects/work models configure model provider -set default model openai/gpt-5.5 +set default model openai/gpt-5.6 channels channel info slack connect slack @@ -138,18 +138,22 @@ gateway/app chat that supports sensitive replies hosts the same steps inline. ```text setup setup workspace ~/Projects/work -setup workspace ~/Projects/work model openai/gpt-5.5 +setup workspace ~/Projects/work model openai/gpt-5.6 ``` When no model is configured, setup picks the first usable backend in this order and tells you what it chose: 1. Existing explicit model, if already configured. -2. `OPENAI_API_KEY` -> `openai/gpt-5.5` +2. `OPENAI_API_KEY` -> `openai/gpt-5.6` (the bare direct-API id resolves to Sol) 3. `ANTHROPIC_API_KEY` -> `anthropic/claude-opus-4-8` 4. Claude Code CLI -> `claude-cli/claude-opus-4-8` -5. Codex -> `openai/gpt-5.5` through the Codex app-server harness +5. Codex -> `openai/gpt-5.6-sol` through the Codex app-server harness 6. Gemini CLI -> `google-gemini-cli/gemini-3.1-pro-preview` +The first existing explicit model always wins, so setup preserves an existing +`openai/gpt-5.5` primary. If an OpenAI account does not expose GPT-5.6, set +`openai/gpt-5.5` explicitly instead; Crestodian does not silently downgrade. + If none are available, setup still writes the workspace and Gateway configuration, then asks whether to configure a model provider. Accepting opens the normal onboarding provider/auth and default-model steps. Declining leaves Crestodian in deterministic mode; exact setup and repair commands still work, but the normal agent cannot answer until a provider and default model are configured. Run `configure model provider` later to reopen the provider flow. The macOS app drives the same ladder through the `crestodian.setup.detect` and `crestodian.setup.activate` gateway methods: detect lists every reusable backend it finds, activate live-tests one candidate (a real "reply with OK" completion) and only persists the model, workspace, and gateway defaults after the test passes. A failing candidate never changes config; the app automatically walks down the ladder and finally offers a manual key/token step populated from the Gateway's active text-inference provider plugins. The selected provider owns its starter model and config, and the credential is verified the same way before it is saved. @@ -159,7 +163,7 @@ The macOS app drives the same ladder through the `crestodian.setup.detect` and ` Interactive Crestodian is AI-only: every message — including ones that look like typed commands — runs through the same embedded agent loop as regular OpenClaw agents, restricted to one ring-zero `crestodian` tool that wraps the typed operations. Read actions run freely, mutations require your conversational approval for that exact operation (see Operations and approval), and every applied write is audited and re-validated. The agent session persists, so the custodian has real multi-turn memory. It first uses the configured OpenClaw model; with no usable model it falls back to a local runtime already present on the machine, in setup-ladder order: - Claude Code CLI: `claude-cli/claude-opus-4-8` (agent loop; the ring-zero tool is served over MCP, see the trust model below) -- Codex app-server harness: `openai/gpt-5.5` (agent loop with an enforced single-tool allow-list) +- Codex app-server harness: `openai/gpt-5.6-sol` (agent loop with an enforced single-tool allow-list) - Gemini CLI: `google-gemini-cli/gemini-3.1-pro-preview` (agent loop; ring-zero tool over MCP) When the agent loop is unavailable, Crestodian degrades to a bounded single-turn planner, and only without any usable model at all to deterministic typed commands. The planner cannot mutate config directly; it must translate the request into one of Crestodian's typed commands, and normal approval/audit rules apply. Crestodian prints the model it used and the interpreted command before running anything. Fallback planner turns are temporary, tool-disabled where the runtime supports it, and use a temporary workspace/session. diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 73048218e8c7..69d7e4a7bf2d 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -37,7 +37,13 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram) Plugin auto-enable follows the same boundary: `openai/*` agent refs enable the Codex plugin for the default route, and explicit provider/model `agentRuntime.id: "codex"` or legacy `codex/` refs also require it. - GPT-5.5 is available through the native Codex app-server harness by default on `openai/gpt-5.5`, and through the OpenClaw runtime when provider/model runtime policy explicitly selects `openclaw`. + 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 + through either runtime as an explicit recovery choice for accounts without + GPT-5.6 access. @@ -89,7 +95,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) -- Example models: `openai/gpt-5.5`, `openai/gpt-5.4-mini` +- 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` - 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. @@ -103,10 +110,15 @@ Official provider plugins publish their own model catalog rows. These providers ```json5 { - agents: { defaults: { model: { primary: "openai/gpt-5.5" } } }, + agents: { defaults: { model: { primary: "openai/gpt-5.6" } } }, } ``` +If the API organization does not expose GPT-5.6, set +`openai/gpt-5.5` explicitly. Normal onboarding and reauthentication preserve an +existing explicit primary model; `models auth login --set-default` and +`models set` are the intentional replacement paths. + ### Anthropic - Provider: `anthropic` @@ -134,7 +146,7 @@ Claude CLI reuse (`claude -p`) is a sanctioned OpenClaw integration path. Anthro - Provider: `openai` - Auth: OAuth (ChatGPT) -- Native Codex app-server harness ref: `openai/gpt-5.5` +- Fresh native Codex app-server harness ref: `openai/gpt-5.6-sol` - Native Codex app-server harness docs: [Codex harness](/plugins/codex-harness) - Legacy model refs: `codex/gpt-*` - Plugin boundary: `openai/*` loads the OpenAI plugin; the native Codex app-server plugin is selected by the Codex harness runtime. @@ -144,17 +156,18 @@ Claude CLI reuse (`claude -p`) is a sanctioned OpenClaw integration path. Anthro - `params.serviceTier` is also forwarded on native Codex Responses requests (`chatgpt.com/backend-api`) - Hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`) are only attached on native Codex traffic to `chatgpt.com/backend-api`, not generic OpenAI-compatible proxies - Shares the same `/fast` toggle and `params.fastMode` config as direct `openai/*`; OpenClaw maps that to `service_tier=priority` +- The native Codex catalog can expose exact `openai/gpt-5.6-sol`, `openai/gpt-5.6-terra`, and `openai/gpt-5.6-luna` refs according to account access. It does not apply the direct API's bare `gpt-5.6` alias client-side. - `openai/gpt-5.5` uses the Codex catalog native `contextWindow = 400000` and default runtime `contextTokens = 272000`; override the runtime cap with `models.providers.openai.models[].contextTokens` -- Sign in with `openai` auth and configure `openai/gpt-5.5` for the standard subscription plus native Codex runtime route; OpenAI agent turns select Codex by default. -- Use provider/model `agentRuntime.id: "openclaw"` only when you want the built-in OpenClaw route; otherwise keep `openai/gpt-5.5` on the default Codex harness. -- Legacy Codex GPT refs are legacy state, not a live provider route. Use `openai/gpt-5.5` on the native Codex runtime for new agent config, and run `openclaw doctor --fix` to migrate old legacy Codex model refs to canonical `openai/*` refs. +- Sign in with `openai` auth and use `openai/gpt-5.6-sol` for a fresh subscription-backed setup. Select `openai/gpt-5.5` explicitly if that Codex workspace does not expose GPT-5.6. +- Use provider/model `agentRuntime.id: "openclaw"` only when you want the built-in OpenClaw route; otherwise keep the selected `openai/*` model on the default Codex harness. +- Legacy Codex GPT refs are legacy state, not a live provider route. Use canonical `openai/*` refs for new agent config, and run `openclaw doctor --fix` to migrate old legacy Codex model refs without upgrading an existing explicit `openai/gpt-5.5` selection. ```json5 { plugins: { entries: { codex: { enabled: true } } }, agents: { defaults: { - model: { primary: "openai/gpt-5.5" }, + model: { primary: "openai/gpt-5.6-sol" }, }, }, } diff --git a/docs/concepts/models.md b/docs/concepts/models.md index 7a59435fce43..d3b60bac06d9 100644 --- a/docs/concepts/models.md +++ b/docs/concepts/models.md @@ -23,7 +23,7 @@ sidebarTitle: "Models CLI" -A model ref (`provider/model`) chooses a provider and model. It does not usually choose the low-level agent runtime. OpenAI is the main exception: `openai/gpt-5.5` runs through the Codex app-server runtime by default on the official OpenAI provider. Subscription Copilot refs (`github-copilot/*`) can be opted into the external GitHub Copilot agent runtime plugin, but that path is always explicit (never selected by `auto`). Runtime overrides belong on provider/model policy, not on the whole agent or session. In Codex runtime mode, `openai/gpt-*` does not imply API-key billing; auth can come from a Codex account or an `openai` OAuth profile. See [Agent runtimes](/concepts/agent-runtimes) and [GitHub Copilot agent runtime](/plugins/copilot). +A model ref (`provider/model`) chooses a provider and model. It does not usually choose the low-level agent runtime. OpenAI is the main exception: official `openai/gpt-*` agent refs run through the Codex app-server runtime by default. Subscription Copilot refs (`github-copilot/*`) can be opted into the external GitHub Copilot agent runtime plugin, but that path is always explicit (never selected by `auto`). Runtime overrides belong on provider/model policy, not on the whole agent or session. In Codex runtime mode, `openai/gpt-*` does not imply API-key billing; auth can come from a Codex account or an `openai` OAuth profile. See [Agent runtimes](/concepts/agent-runtimes) and [GitHub Copilot agent runtime](/plugins/copilot). ## Selection order @@ -83,6 +83,13 @@ 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. +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. + ## "Model is not allowed" (and why replies stop) If `agents.defaults.models` is set, it becomes the allowlist for `/model` and session overrides. Selecting a model outside that allowlist returns, before any normal reply is generated: diff --git a/docs/help/faq-first-run.md b/docs/help/faq-first-run.md index b81d39deaf1f..89a9667a0752 100644 --- a/docs/help/faq-first-run.md +++ b/docs/help/faq-first-run.md @@ -558,12 +558,17 @@ and troubleshooting see the main [FAQ](/help/faq). - OpenClaw supports **OpenAI Codex** via OAuth (ChatGPT sign-in). Use `openai/gpt-5.5` - for the default setup: ChatGPT/Codex subscription auth plus native Codex app-server - execution. Legacy Codex-prefixed model refs are legacy config repaired by - `openclaw doctor --fix`. Direct OpenAI API-key access remains available for non-agent - OpenAI API surfaces and, through an ordered `openai` API-key profile, for agent models - too. See [Model providers](/concepts/model-providers) and [Onboarding (CLI)](/start/wizard). + OpenClaw supports **OpenAI Codex** via OAuth (ChatGPT sign-in). A fresh + setup with no primary model uses exact `openai/gpt-5.6-sol` for + ChatGPT/Codex subscription auth plus native Codex app-server execution. + Reauthentication preserves an existing explicit model, including + `openai/gpt-5.5`. If the Codex workspace does not expose GPT-5.6, select + `openai/gpt-5.5` explicitly; OpenClaw does not silently downgrade. Legacy + Codex-prefixed model refs are legacy config repaired by `openclaw doctor + --fix`. Direct OpenAI API-key access remains available for non-agent OpenAI + API surfaces and, through an ordered `openai` API-key profile, for agent + models too. See [Model providers](/concepts/model-providers) and + [Onboarding (CLI)](/start/wizard). @@ -571,14 +576,17 @@ and troubleshooting see the main [FAQ](/help/faq). ChatGPT/Codex OAuth - OpenAI Codex is folded into it. You may still see a legacy `openai-codex` prefix in older config and migration warnings: - - `openai/gpt-5.5` = ChatGPT/Codex subscription auth with native Codex runtime for agent turns. + - `openai/gpt-5.6-sol` = fresh ChatGPT/Codex subscription setup with the native Codex runtime for agent turns. + - `openai/gpt-5.5` = explicit supported selection for existing config or accounts without GPT-5.6 access. - Legacy `openai-codex/*` model refs = legacy route repaired by `openclaw doctor --fix`. - `openai/gpt-5.5` plus an ordered `openai` API-key profile = API-key auth for an OpenAI agent model. - Legacy `openai-codex` auth profile ids = legacy ids migrated by `openclaw doctor --fix`. Want direct OpenAI Platform billing? Set `OPENAI_API_KEY`. Want ChatGPT/Codex - subscription auth? Run `openclaw models auth login --provider openai`. Keep the model - ref as `openai/gpt-5.5`; legacy Codex-prefixed refs are what `openclaw doctor --fix` rewrites. + subscription auth? Run `openclaw models auth login --provider openai`. Keep + model refs under the canonical `openai/*` provider. Fresh subscription + setup uses exact `openai/gpt-5.6-sol`; doctor repairs legacy Codex-prefixed + refs without upgrading an explicit `openai/gpt-5.5` selection. diff --git a/docs/providers/openai.md b/docs/providers/openai.md index 528e53ea9718..19a81395f835 100644 --- a/docs/providers/openai.md +++ b/docs/providers/openai.md @@ -44,15 +44,16 @@ changing config. ## Quick choice -| Goal | Use | Notes | -| ------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------- | -| ChatGPT/Codex subscription, native Codex runtime | `openai/gpt-5.5` | Default setup. Sign in with Codex auth. | -| GPT-5.6 limited preview | `openai/gpt-5.6-sol`, `-terra`, or `-luna` | Needs an OpenAI-approved API org or Codex workspace allowlist entry. | -| Direct API-key billing for agent turns | `openai/gpt-5.5` plus an ordered API-key auth profile | Set `auth.order.openai` to put the key profile after subscription auth. | -| Direct API-key billing, explicit OpenClaw runtime | `openai/gpt-5.5` plus provider/model `agentRuntime.id: "openclaw"` | Select a normal `openai` API-key profile. | -| Latest ChatGPT Instant model alias | `openai/chat-latest` | Direct API-key only; moving alias, not the stable default. | -| Image generation or editing | `openai/gpt-image-2` | Works with `OPENAI_API_KEY` or Codex OAuth. | -| Transparent-background images | `openai/gpt-image-1.5` | Set `outputFormat` to `png` or `webp` and `background=transparent`. | +| 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. | +| 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. | +| Latest ChatGPT Instant model alias | `openai/chat-latest` | Direct API-key only; moving alias, not the stable default. | +| Image generation or editing | `openai/gpt-image-2` | Works with `OPENAI_API_KEY` or Codex OAuth. | +| Transparent-background images | `openai/gpt-image-1.5` | Set `outputFormat` to `png` or `webp` and `background=transparent`. | ## Naming map @@ -69,27 +70,42 @@ profile ids, and legacy Codex auth-order entries to the canonical `openai` route. Use `auth.order.openai` for new auth-order config. -GPT-5.5 is available through both direct OpenAI Platform API-key access and -subscription/OAuth routes. For ChatGPT/Codex subscription with native Codex -execution, use `openai/gpt-5.5` and leave runtime config unset; that already -selects the Codex harness. Use an API-key auth profile only when you want -direct API-key auth for an agent model. +Fresh OpenAI setup applies a GPT-5.6 primary only when no primary model is +configured. Adding or refreshing OpenAI auth preserves an existing explicit +selection, including `openai/gpt-5.5`, unless you explicitly use +`models auth login --set-default` or `models set`. Use an API-key auth profile +only when you want API-key auth for an agent model. ## GPT-5.6 limited preview -OpenClaw recognizes three public GPT-5.6 model ids: `openai/gpt-5.6-sol`, -`openai/gpt-5.6-terra`, and `openai/gpt-5.6-luna`. All three expose `xhigh` and -`max` reasoning in the current catalog. OpenAI describes Sol as the flagship -tier, Terra as the balanced tier, and Luna as the fast, lower-cost tier. See -the [GPT-5.6 launch announcement](https://openai.com/index/previewing-gpt-5-6-sol/) -and [preview access guide](https://help.openai.com/en/articles/20001325-a-preview-of-gpt-5-6-sol-terra-and-luna). +OpenClaw recognizes the exact `openai/gpt-5.6-sol`, +`openai/gpt-5.6-terra`, and `openai/gpt-5.6-luna` model ids. All three expose +`xhigh` and `max` reasoning in the current catalog. OpenAI describes Sol as +the flagship tier, Terra as the balanced tier, and Luna as the fast, +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). -Access is allowlisted during the preview and can be granted separately for the -API and Codex; a paid ChatGPT plan alone does not grant access. OpenClaw keeps -`openai/gpt-5.5` as the default and does not special-case the access error, so -selecting a GPT-5.6 ref without access surfaces the upstream error directly -instead of falling back silently. +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: + +```bash +openclaw models list --provider openai +``` + +API organization and Codex workspace access can differ. If GPT-5.6 is not +available, select GPT-5.5 explicitly: + +```bash +openclaw models set openai/gpt-5.5 +``` + +OpenClaw surfaces the upstream access error and does not silently replace a +GPT-5.6 selection with GPT-5.5. Agent model turns on `openai/*` require the bundled Codex app-server plugin by @@ -190,11 +206,12 @@ for the full example. ### Route summary - | Model ref | Runtime config | Route | Auth | - | ----------------------- | ------------------------------------------------------ | --------------------------- | ------------------------------------ | - | `openai/gpt-5.5` | unset, or provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Ordered API-key auth profile | - | `openai/gpt-5.4-mini` | unset, or provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Ordered API-key auth profile | - | `openai/gpt-5.5` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime | Selected `openai` API-key profile | + | Model ref | Runtime config | Route | Auth | + | --------------------- | --------------------------------------------------- | ------------------------ | -------------------------------- | + | `openai/gpt-5.6` | unset, or provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Ordered API-key auth profile | + | `openai/gpt-5.6` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime | Selected `openai` API-key profile | + | `openai/gpt-5.5` | either runtime | Selected agent runtime | Selected OpenAI API-key profile | + | `openai/gpt-5.4-mini` | unset, or provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Ordered API-key auth profile | Agent turns on `openai/*` use the Codex app-server harness by default. For @@ -209,10 +226,14 @@ for the full example. ```json5 { env: { OPENAI_API_KEY: "example-openai-key-not-real" }, - agents: { defaults: { model: { primary: "openai/gpt-5.5" } } }, + agents: { defaults: { model: { primary: "openai/gpt-5.6" } } }, } ``` + 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. + To try ChatGPT's current Instant model from the OpenAI API, set the model to `openai/chat-latest`: @@ -223,10 +244,11 @@ for the full example. } ``` - `chat-latest` is a moving alias. OpenAI recommends `gpt-5.5` for production - API usage, so keep `openai/gpt-5.5` as the stable default unless you want - that alias behavior. The alias only accepts `medium` text verbosity; - OpenClaw forces any other requested verbosity to `medium` for this model. + `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 + 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. OpenClaw does **not** expose `gpt-5.3-codex-spark` on the direct OpenAI @@ -263,7 +285,7 @@ for the full example. ```bash - openclaw config set agents.defaults.model.primary openai/gpt-5.5 + openclaw config set agents.defaults.model.primary openai/gpt-5.6-sol ``` No runtime config is required for the default path. OpenAI agent @@ -283,19 +305,25 @@ for the full example. ### Route summary - | Model ref | Runtime config | Route | Auth | - | -------------------------- | ------------------------------------------------ | --------------------------------------------------------- | -------------------------------------------------- | - | `openai/gpt-5.5` | unset, or provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in, or an ordered `openai` auth profile | - | `openai/gpt-5.5` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime, internal Codex-auth transport | Selected `openai` OAuth profile | - | Legacy Codex GPT-5.5 ref | repaired by doctor | Rewritten to `openai/gpt-5.5` | Migrated OpenAI OAuth profile | - | `codex-cli/gpt-5.5` | repaired by doctor | Rewritten to `openai/gpt-5.5` | Codex app-server auth | + | Model ref | Runtime config | Route | Auth | + | ------------------------- | --------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------- | + | `openai/gpt-5.6-sol` | unset, or provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in, or an ordered `openai` auth profile | + | `openai/gpt-5.6-terra` | unset, or provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in when the catalog exposes Terra | + | `openai/gpt-5.6-luna` | unset, or provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in when the catalog exposes Luna | + | `openai/gpt-5.6-sol` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime, internal Codex-auth transport | Selected `openai` OAuth profile | + | `openai/gpt-5.5` | either runtime | Selected agent runtime | Selected OpenAI auth profile | + | Legacy Codex GPT-5.5 ref | repaired by doctor | Rewritten to `openai/gpt-5.5` | Migrated OpenAI OAuth profile | + | `codex-cli/gpt-5.5` | repaired by doctor | Rewritten to `openai/gpt-5.5` | Codex app-server auth | - Prefer `openai/gpt-5.5` for new subscription-backed agent config. Older + Fresh subscription-backed setup uses exact `openai/gpt-5.6-sol`; the + native Codex catalog may also expose exact Terra or Luna refs. If the + account does not expose GPT-5.6, select `openai/gpt-5.5` explicitly. Older Codex GPT refs are legacy OpenClaw routes, not the native Codex runtime - path; run `openclaw doctor --fix` to migrate them. `gpt-5.3-codex-spark` - stays limited to accounts whose Codex subscription catalog advertises it; - direct OpenAI API-key and Azure refs for it stay suppressed. + path; run `openclaw doctor --fix` to migrate them without upgrading an + existing explicit GPT-5.5 selection. `gpt-5.3-codex-spark` stays limited + to accounts whose Codex subscription catalog advertises it; direct OpenAI + API-key and Azure refs for it stay suppressed. @@ -310,22 +338,22 @@ for the full example. plugins: { entries: { codex: { enabled: true } } }, agents: { defaults: { - model: { primary: "openai/gpt-5.5" }, + model: { primary: "openai/gpt-5.6-sol" }, }, }, } ``` - With an API-key backup, keep the model on `openai/gpt-5.5` and put the - auth order under `openai`. OpenClaw tries the subscription first, then the - API key, while staying on the Codex harness: + With an API-key backup, keep the selected model under `openai/*` and put + the auth order under `openai`. OpenClaw tries the subscription first, then + the API key, while staying on the Codex harness: ```json5 { plugins: { entries: { codex: { enabled: true } } }, agents: { defaults: { - model: { primary: "openai/gpt-5.5" }, + model: { primary: "openai/gpt-5.6-sol" }, }, }, auth: { diff --git a/docs/reference/wizard.md b/docs/reference/wizard.md index cac83a36d9a8..6e558760de7e 100644 --- a/docs/reference/wizard.md +++ b/docs/reference/wizard.md @@ -40,11 +40,12 @@ behavior and outputs, see [CLI setup reference](/start/wizard-cli-reference). - **Anthropic API key**: uses `ANTHROPIC_API_KEY` if present or prompts for a key, then saves it for daemon use. - **Anthropic Claude CLI**: preferred local path when a Claude CLI sign-in already exists; OpenClaw still supports Anthropic setup-token auth as an alternative. - **OpenAI Code (Codex) subscription (OAuth)**: browser flow; paste the `code#state`. - - Sets `agents.defaults.model` to `openai/gpt-5.5` through the Codex runtime when model is unset or already OpenAI-family. + - On a fresh setup with no primary model, sets `agents.defaults.model` to `openai/gpt-5.6-sol` through the Codex runtime. - **OpenAI Code (Codex) subscription (device pairing)**: browser pairing flow with a short-lived device code. - - Sets `agents.defaults.model` to `openai/gpt-5.5` through the Codex runtime when model is unset or already OpenAI-family. + - 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. - - Sets `agents.defaults.model` to `openai/gpt-5.5` when model is unset, `openai/*`, or legacy Codex model refs. + - 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. + - 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`). - `--auth-choice xai-device-code` still works as a manual-only compatibility alias for the same xAI OAuth device-code flow; use `xai-oauth` for new scripts. diff --git a/docs/start/wizard-cli-reference.md b/docs/start/wizard-cli-reference.md index 90ae9bd083b5..c498d4d32f58 100644 --- a/docs/start/wizard-cli-reference.md +++ b/docs/start/wizard-cli-reference.md @@ -171,19 +171,26 @@ instead of exiting. Explicit `--auth-choice` runs still fail fast for automation Browser flow; paste `code#state`. - Sets `agents.defaults.model` to `openai/gpt-5.5` through the Codex runtime when model is unset or already OpenAI-family. + On a fresh setup with no primary model, sets `agents.defaults.model` to + `openai/gpt-5.6-sol` through the Codex runtime. Browser pairing flow with a short-lived device code. - Sets `agents.defaults.model` to `openai/gpt-5.5` through the Codex runtime when model is unset or already OpenAI-family. + On a fresh setup with no primary model, sets `agents.defaults.model` to + `openai/gpt-5.6-sol` through the Codex runtime. Uses `OPENAI_API_KEY` if present or prompts for a key, then stores the credential in auth profiles. - Sets `agents.defaults.model` to `openai/gpt-5.5` when model is unset, `openai/*`, or legacy Codex model refs. + 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. + + 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 it. diff --git a/docs/start/wizard.md b/docs/start/wizard.md index 14d6bc6f0254..fa2d4ad3662d 100644 --- a/docs/start/wizard.md +++ b/docs/start/wizard.md @@ -124,6 +124,11 @@ 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 + 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 tool policy strict - weaker or older tiers are easier to prompt-inject. diff --git a/extensions/codex/src/migration/auth.ts b/extensions/codex/src/migration/auth.ts index 5adda52f7064..1cd593011848 100644 --- a/extensions/codex/src/migration/auth.ts +++ b/extensions/codex/src/migration/auth.ts @@ -30,7 +30,7 @@ import type { CodexSource } from "./source.js"; import type { resolveCodexMigrationTargets } from "./targets.js"; const OPENAI_PROVIDER_ID = "openai"; -const OPENAI_CODEX_DEFAULT_MODEL = "openai/gpt-5.5"; +const OPENAI_CODEX_DEFAULT_MODEL = "openai/gpt-5.6-sol"; const CODEX_IMPORT_DISPLAY_NAME = "Codex import"; const CODEX_REASON_AUTH_NOT_SELECTED = "auth credential migration not selected"; const CODEX_REASON_AUTH_PROFILE_EXISTS = "auth profile exists"; diff --git a/extensions/codex/src/migration/provider.test.ts b/extensions/codex/src/migration/provider.test.ts index 6fd46bb82dae..624d0d103e8c 100644 --- a/extensions/codex/src/migration/provider.test.ts +++ b/extensions/codex/src/migration/provider.test.ts @@ -405,9 +405,11 @@ describe("buildCodexMigrationProvider", () => { }), ); expect(configState.agents?.defaults?.models?.["openai/gpt-5.4-mini"]).toEqual({}); + expect(configState.agents?.defaults?.models?.["openai/gpt-5.5"]).toEqual({}); + expect(configState.agents?.defaults?.models?.["openai/gpt-5.6-sol"]).toEqual({}); expect(configState.agents?.defaults?.model).toEqual({ fallbacks: [], - primary: "openai/gpt-5.5", + primary: "openai/gpt-5.6-sol", }); }); @@ -820,9 +822,11 @@ describe("buildCodexMigrationProvider", () => { details: expect.objectContaining({ path: ["agents", "defaults"], value: expect.objectContaining({ - model: { primary: "openai/gpt-5.5" }, + model: { primary: "openai/gpt-5.6-sol" }, models: expect.objectContaining({ "openai/gpt-5.4-mini": {}, + "openai/gpt-5.5": {}, + "openai/gpt-5.6-sol": {}, }), }), }), diff --git a/extensions/migrate-hermes/auth.ts b/extensions/migrate-hermes/auth.ts index ada937a82509..e2c5c1b79b7c 100644 --- a/extensions/migrate-hermes/auth.ts +++ b/extensions/migrate-hermes/auth.ts @@ -24,6 +24,10 @@ import { type OpenClawConfig, type ProviderAuthResult, } from "openclaw/plugin-sdk/provider-auth"; +import { + applyAgentDefaultModelPrimary, + resolveAgentModelPrimaryValue, +} from "openclaw/plugin-sdk/provider-onboard"; import { applyAuthProfileConfigWithConflictCheck, hasAuthProfileConfigConflict, @@ -43,7 +47,7 @@ import type { HermesSource } from "./source.js"; import type { PlannedTargets } from "./targets.js"; const OPENAI_PROVIDER_ID = "openai"; -const OPENAI_DEFAULT_MODEL = "openai/gpt-5.5"; +const OPENAI_CODEX_DEFAULT_MODEL = "openai/gpt-5.6-sol"; const HERMES_AUTH_DISPLAY_NAME = "Hermes import"; type AgentDefaultModelConfigs = NonNullable< @@ -172,7 +176,7 @@ function buildAuthResult( }); return buildOauthProviderAuthResult({ providerId: OPENAI_PROVIDER_ID, - defaultModel: OPENAI_DEFAULT_MODEL, + defaultModel: OPENAI_CODEX_DEFAULT_MODEL, access: candidate.access, refresh: candidate.refresh, expires: resolveOpenAICodexAccessTokenExpiry(candidate.access), @@ -188,7 +192,7 @@ function readProviderAuthModelConfigs(result: ProviderAuthResult): AgentDefaultM if (isRecord(models)) { return { ...models }; } - const defaultModel = readString(result.defaultModel) ?? OPENAI_DEFAULT_MODEL; + const defaultModel = readString(result.defaultModel) ?? OPENAI_CODEX_DEFAULT_MODEL; return { [defaultModel]: {} }; } @@ -503,7 +507,10 @@ export async function applyAuthItem( ctx, profile: configProfile, applyConfigPatch(config) { - return applyOAuthModelConfigsToConfig(config, profile.result); + const next = applyOAuthModelConfigsToConfig(config, profile.result); + return resolveAgentModelPrimaryValue(next.agents?.defaults?.model) === undefined + ? applyAgentDefaultModelPrimary(next, OPENAI_CODEX_DEFAULT_MODEL) + : next; }, }); if (configResult === "conflict") { diff --git a/extensions/migrate-hermes/secrets.test.ts b/extensions/migrate-hermes/secrets.test.ts index 098c51ad3b35..811006eb6381 100644 --- a/extensions/migrate-hermes/secrets.test.ts +++ b/extensions/migrate-hermes/secrets.test.ts @@ -807,6 +807,10 @@ describe("Hermes migration secret items", () => { refresh: "openai-refresh-token", }), ); + expect(config.agents?.defaults?.model).toEqual({ + primary: "openai/gpt-5.6-sol", + }); + expect(config.agents?.defaults?.models?.["openai/gpt-5.6-sol"]).toEqual({}); }); it("does not apply a planned OpenCode OpenAI OAuth credential after the source token changes", async () => { @@ -957,6 +961,10 @@ describe("Hermes migration secret items", () => { agents: { defaults: { workspace: workspaceDir, + model: { + primary: "anthropic/claude-opus-4-8", + fallbacks: ["openai/gpt-5.5"], + }, }, }, } as OpenClawConfig; @@ -1026,5 +1034,9 @@ describe("Hermes migration secret items", () => { email: sharedEmail, }), ); + expect(config.agents?.defaults?.model).toEqual({ + primary: "anthropic/claude-opus-4-8", + fallbacks: ["openai/gpt-5.5"], + }); }); }); diff --git a/extensions/openai/default-models.test.ts b/extensions/openai/default-models.test.ts index 68a2bbc6ca0b..ec4007b45413 100644 --- a/extensions/openai/default-models.test.ts +++ b/extensions/openai/default-models.test.ts @@ -23,15 +23,71 @@ describe("openai default models", () => { expect(next.agents?.defaults?.models?.[OPENAI_DEFAULT_MODEL]?.alias).toBe("My GPT"); }); + it("does not move the GPT alias from an existing model", () => { + const next = applyOpenAIProviderConfig({ + agents: { + defaults: { + models: { + "openai/gpt-5.5": { alias: "GPT" }, + "custom/model": { alias: "Custom" }, + }, + }, + }, + }); + + expect(next.agents?.defaults?.models).toEqual({ + "openai/gpt-5.5": { alias: "GPT" }, + "custom/model": { alias: "Custom" }, + [OPENAI_DEFAULT_MODEL]: {}, + }); + }); + + it("does not duplicate a case-insensitive custom GPT alias", () => { + const next = applyOpenAIProviderConfig({ + agents: { + defaults: { + models: { + "custom/model": { alias: "gPt" }, + }, + }, + }, + }); + + expect(next.agents?.defaults?.models?.["custom/model"]?.alias).toBe("gPt"); + expect(next.agents?.defaults?.models?.[OPENAI_DEFAULT_MODEL]).toEqual({}); + }); + it("sets the default model when it is unset", () => { const next = applyOpenAIConfig({}); expect(next.agents?.defaults?.model).toEqual({ primary: OPENAI_DEFAULT_MODEL }); }); - it("overrides model.primary while preserving fallbacks", () => { + it("preserves an explicit primary and its fallbacks", () => { const next = applyOpenAIConfig({ agents: { defaults: { model: { primary: "anthropic/claude-opus-4-6", fallbacks: [] } } }, } as OpenClawConfig); - expect(next.agents?.defaults?.model).toEqual({ primary: OPENAI_DEFAULT_MODEL, fallbacks: [] }); + expect(next.agents?.defaults?.model).toEqual({ + primary: "anthropic/claude-opus-4-6", + fallbacks: [], + }); + expect(next.agents?.defaults?.models).toEqual({ + "anthropic/claude-opus-4-6": {}, + [OPENAI_DEFAULT_MODEL]: { alias: "GPT" }, + }); + }); + + it("fills a missing object-form primary while preserving fallbacks", () => { + const next = applyOpenAIConfig({ + agents: { defaults: { model: { fallbacks: ["anthropic/claude-opus-4-6"] } } }, + } as OpenClawConfig); + + expect(next.agents?.defaults?.model).toEqual({ + primary: OPENAI_DEFAULT_MODEL, + fallbacks: ["anthropic/claude-opus-4-6"], + }); + expect(next.agents?.defaults?.models).toEqual({ + "anthropic/claude-opus-4-6": {}, + [OPENAI_DEFAULT_MODEL]: { alias: "GPT" }, + }); }); }); diff --git a/extensions/openai/default-models.ts b/extensions/openai/default-models.ts index a7b1bb1f755a..8b187e5293b0 100644 --- a/extensions/openai/default-models.ts +++ b/extensions/openai/default-models.ts @@ -2,11 +2,12 @@ import { ensureModelAllowlistEntry } from "openclaw/plugin-sdk/provider-onboard"; import { applyAgentDefaultModelPrimary, + resolveAgentModelPrimaryValue, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; -export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.5"; -export const OPENAI_CODEX_DEFAULT_MODEL = OPENAI_DEFAULT_MODEL; +export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.6"; +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"; export const OPENAI_DEFAULT_TTS_VOICE = "alloy"; @@ -14,14 +15,29 @@ export const OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL = "gpt-4o-transcribe"; export const OPENAI_DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small"; export function applyOpenAIProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const next = ensureModelAllowlistEntry({ + const configuredModel = cfg.agents?.defaults?.model; + const configuredRefs = [ + resolveAgentModelPrimaryValue(configuredModel), + ...(typeof configuredModel === "object" ? (configuredModel.fallbacks ?? []) : []), + ].filter((ref): ref is string => typeof ref === "string" && ref.trim().length > 0); + const withConfiguredRefs = configuredRefs.reduce( + (next, modelRef) => ensureModelAllowlistEntry({ cfg: next, modelRef }), cfg, + ); + const next = ensureModelAllowlistEntry({ + cfg: withConfiguredRefs, modelRef: OPENAI_DEFAULT_MODEL, }); const models = { ...next.agents?.defaults?.models }; + const gptAliasClaimed = Object.entries(models).some( + ([modelRef, model]) => + modelRef !== OPENAI_DEFAULT_MODEL && model?.alias?.trim().toLowerCase() === "gpt", + ); models[OPENAI_DEFAULT_MODEL] = { ...models[OPENAI_DEFAULT_MODEL], - alias: models[OPENAI_DEFAULT_MODEL]?.alias ?? "GPT", + ...(models[OPENAI_DEFAULT_MODEL]?.alias === undefined && !gptAliasClaimed + ? { alias: "GPT" } + : {}), }; return { @@ -37,5 +53,8 @@ export function applyOpenAIProviderConfig(cfg: OpenClawConfig): OpenClawConfig { } export function applyOpenAIConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary(applyOpenAIProviderConfig(cfg), OPENAI_DEFAULT_MODEL); + const next = applyOpenAIProviderConfig(cfg); + return resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model) === undefined + ? applyAgentDefaultModelPrimary(next, OPENAI_DEFAULT_MODEL) + : next; } diff --git a/extensions/openai/openai-chatgpt-provider.test.ts b/extensions/openai/openai-chatgpt-provider.test.ts index 94fbe52af5fa..b09a1f707461 100644 --- a/extensions/openai/openai-chatgpt-provider.test.ts +++ b/extensions/openai/openai-chatgpt-provider.test.ts @@ -69,6 +69,10 @@ describe("OpenAI provider Codex transport hooks", () => { refresh: "refresh-token", }, }); + expect(result?.defaultModel).toBe("openai/gpt-5.6-sol"); + expect(result?.configPatch?.agents?.defaults?.models).toEqual({ + "openai/gpt-5.6-sol": {}, + }); }); it("routes Codex-backed OpenAI models through the Codex Responses transport", () => { @@ -115,6 +119,19 @@ describe("OpenAI provider Codex transport hooks", () => { }, ); + it("does not invent a bare GPT-5.6 alias for the Codex transport", () => { + const provider = buildOpenAIProvider(); + + const model = provider.resolveDynamicModel?.({ + provider: "openai", + modelId: "gpt-5.6", + authProfileMode: "oauth", + modelRegistry: { find: () => null }, + } as never); + + expect(model).toBeUndefined(); + }); + it.each([ { name: "fills a missing map", thinkingLevelMap: undefined, expectedOff: null }, { name: "preserves explicit overrides", thinkingLevelMap: { off: "low" }, expectedOff: "low" }, diff --git a/extensions/openai/openai-provider-missing-auth.test.ts b/extensions/openai/openai-provider-missing-auth.test.ts index a835426a0ed9..a403e93e7eb3 100644 --- a/extensions/openai/openai-provider-missing-auth.test.ts +++ b/extensions/openai/openai-provider-missing-auth.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -const FUTURE_DEFAULT_MODEL = "openai/gpt-next-default"; +const FUTURE_API_DEFAULT_MODEL = "openai/gpt-next-default"; +const FUTURE_CODEX_DEFAULT_MODEL = "openai/gpt-next-codex-default"; describe("OpenAI missing auth message", () => { afterEach(() => { @@ -14,8 +15,8 @@ describe("OpenAI missing auth message", () => { const actual = await importOriginal(); return { ...actual, - OPENAI_DEFAULT_MODEL: FUTURE_DEFAULT_MODEL, - OPENAI_CODEX_DEFAULT_MODEL: FUTURE_DEFAULT_MODEL, + OPENAI_DEFAULT_MODEL: FUTURE_API_DEFAULT_MODEL, + OPENAI_CODEX_DEFAULT_MODEL: FUTURE_CODEX_DEFAULT_MODEL, }; }); @@ -27,6 +28,9 @@ describe("OpenAI missing auth message", () => { listProfileIds: (providerId: string) => (providerId === "openai" ? ["openai:codex"] : []), } as never); - expect(message).toContain(`Use ${FUTURE_DEFAULT_MODEL} with the ChatGPT/Codex OAuth profile`); + expect(message).toContain( + `Use ${FUTURE_CODEX_DEFAULT_MODEL} with the ChatGPT/Codex OAuth profile`, + ); + expect(message).not.toContain(FUTURE_API_DEFAULT_MODEL); }); }); diff --git a/extensions/openai/openai-provider.test.ts b/extensions/openai/openai-provider.test.ts index 4456e43f2af9..46292bbf0fa8 100644 --- a/extensions/openai/openai-provider.test.ts +++ b/extensions/openai/openai-provider.test.ts @@ -6,7 +6,7 @@ import { type LiveModelCatalogFetchGuard, } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { OPENAI_DEFAULT_MODEL } from "./default-models.js"; +import { OPENAI_CODEX_DEFAULT_MODEL, OPENAI_DEFAULT_MODEL } from "./default-models.js"; import { buildOpenAICodexLiveProviderConfig, buildOpenAILiveProviderConfig, @@ -162,6 +162,46 @@ describe("buildOpenAIProvider", () => { groupLabel: "OpenAI", groupHint: "ChatGPT/Codex sign-in or API key", }); + expect(apiKey?.starterModel).toBe("openai/gpt-5.6"); + }); + + it("preserves existing model selection during non-interactive API key setup", async () => { + const provider = buildOpenAIProvider(); + const apiKey = provider.auth.find((method) => method.id === "api-key"); + if (!apiKey?.runNonInteractive) { + throw new Error("expected OpenAI API key non-interactive auth"); + } + const primaryModel = "anthropic/claude-opus-4-6"; + const fallbackModel = "openai/gpt-5.5"; + + const next = await apiKey.runNonInteractive({ + config: { + agents: { + defaults: { + model: { primary: primaryModel, fallbacks: [fallbackModel] }, + models: { + [primaryModel]: { alias: "Primary" }, + [fallbackModel]: { alias: "Fallback" }, + }, + }, + }, + }, + opts: {}, + env: {}, + runtime: {}, + resolveApiKey: async () => ({ key: "sk-test", source: "profile" }), + toApiKeyCredential: () => null, + } as never); + + expect(next?.agents?.defaults?.model).toEqual({ + primary: primaryModel, + fallbacks: [fallbackModel], + }); + expect(next?.agents?.defaults?.models).toMatchObject({ + [primaryModel]: { alias: "Primary" }, + [fallbackModel]: { alias: "Fallback" }, + [OPENAI_DEFAULT_MODEL]: { alias: "GPT" }, + }); }); it("marks the OpenAI manifest catalog as runtime-discovered", () => { @@ -197,18 +237,28 @@ describe("buildOpenAIProvider", () => { } const gpt55 = result.providers.openai?.models.find((model) => model.id === "gpt-5.5"); const gpt56Models = result.providers.openai?.models.filter((model) => - model.id.startsWith("gpt-5.6-"), + model.id.startsWith("gpt-5.6"), ); expect(gpt55?.mediaInput).toEqual({ 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.thinkingLevelMap?.off)).toEqual([null, null, null]); - expect(OPENAI_DEFAULT_MODEL).toBe("openai/gpt-5.5"); + expect(gpt56Models?.map((model) => model.contextWindow)).toEqual([ + 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(OPENAI_DEFAULT_MODEL).toBe("openai/gpt-5.6"); + expect(OPENAI_CODEX_DEFAULT_MODEL).toBe("openai/gpt-5.6-sol"); }); it("scopes the OpenAI API-key catalog to the OpenAI provider id", async () => { @@ -295,6 +345,7 @@ describe("buildOpenAIProvider", () => { const fetchGuard: LiveModelCatalogFetchGuard = vi.fn(async () => ({ response: Response.json({ data: [ + { id: "gpt-5.6", object: "model" }, { id: "gpt-5.5", object: "model" }, { id: "not-in-manifest", object: "model" }, ], @@ -309,6 +360,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)).toContain("gpt-5.5"); expect(provider.models.map((model) => model.id)).not.toContain("not-in-manifest"); const fetchParams = vi.mocked(fetchGuard).mock.calls[0]?.[0]; @@ -358,8 +410,8 @@ describe("buildOpenAIProvider", () => { Response.json({ models: [ { - slug: "gpt-5.6", - display_name: "GPT-5.6", + slug: "gpt-5.6-sol", + display_name: "GPT-5.6 Sol", visibility: "list", supported_reasoning_levels: [ { effort: "low", description: "low" }, @@ -386,6 +438,12 @@ describe("buildOpenAIProvider", () => { max_context_window: 1_000_000, max_output_tokens: 128_000, }, + { + slug: "gpt-5.6-terra", + display_name: "GPT-5.6 Terra", + visibility: "list", + supported_reasoning_levels: [{ effort: "medium", description: "medium" }], + }, { slug: "gpt-5.3-codex-spark", display_name: "GPT-5.3 Codex Spark", @@ -443,17 +501,22 @@ describe("buildOpenAIProvider", () => { expect(openai?.auth).toBe("oauth"); expect(openai?.baseUrl).toBe("https://chatgpt.com/backend-api/codex"); expect(openai?.models.map((model) => model.id)).toEqual([ - "gpt-5.6", + "gpt-5.6-sol", "gpt-5.5", + "gpt-5.6-terra", "gpt-5.3-codex-spark", ]); - expect(openai?.models.find((model) => model.id === "gpt-5.6")).toMatchObject({ + expect(openai?.models.find((model) => model.id === "gpt-5.6-sol")).toMatchObject({ contextWindow: 372_000, compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh", "max"], }, thinkingLevelMap: { off: null, xhigh: "xhigh", max: "max" }, }); + expect(openai?.models.find((model) => model.id === "gpt-5.6-terra")).toMatchObject({ + contextWindow: 372_000, + contextTokens: 372_000, + }); expect(openai?.models.find((model) => model.id === "gpt-5.3-codex-spark")).toMatchObject({ name: "GPT-5.3 Codex Spark", reasoning: true, @@ -615,6 +678,21 @@ describe("buildOpenAIProvider", () => { expect(provider.auth).toBe("oauth"); expect(provider.baseUrl).toBe("https://chatgpt.com/backend-api/codex"); expect(provider.models.length).toBeGreaterThan(0); + expect(provider.models.map((model) => model.id)).not.toContain("gpt-5.6"); + expect(provider.models.find((model) => model.id === "gpt-5.6-sol")).toMatchObject({ + contextWindow: 372_000, + contextTokens: 372_000, + thinkingLevelMap: { off: null }, + compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh", "max"] }, + }); + expect(provider.models.find((model) => model.id === "gpt-5.6-terra")).toMatchObject({ + contextWindow: 372_000, + contextTokens: 372_000, + }); + expect(provider.models.find((model) => model.id === "gpt-5.6-luna")).toMatchObject({ + contextWindow: 372_000, + contextTokens: 372_000, + }); expect(provider.models.map((model) => model.id)).toContain("gpt-5.5"); expect(release).toHaveBeenCalledOnce(); }); @@ -1029,22 +1107,27 @@ describe("buildOpenAIProvider", () => { }); it.each([ + { + id: "gpt-5.6", + cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, + thinkingLevelMap: { off: "none", xhigh: "xhigh", max: "max" }, + }, { id: "gpt-5.6-sol", cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, - thinkingLevelMap: { off: null, xhigh: "xhigh", max: "max" }, + thinkingLevelMap: { off: "none", xhigh: "xhigh", max: "max" }, }, { id: "gpt-5.6-terra", cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 3.125 }, - thinkingLevelMap: { off: null, xhigh: "xhigh", max: "max" }, + thinkingLevelMap: { off: "none", xhigh: "xhigh", max: "max" }, }, { id: "gpt-5.6-luna", cost: { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25 }, - thinkingLevelMap: { off: null, xhigh: "xhigh", max: "max" }, + thinkingLevelMap: { off: "none", xhigh: "xhigh", max: "max" }, }, - ])("resolves $id locally with preview metadata", ({ id, cost, thinkingLevelMap }) => { + ])("resolves $id locally with direct API metadata", ({ id, cost, thinkingLevelMap }) => { const provider = buildOpenAIProvider(); const model = provider.resolveDynamicModel?.({ @@ -1075,8 +1158,8 @@ describe("buildOpenAIProvider", () => { provider: "openai", api: "openai-responses", baseUrl: "https://api.openai.com/v1", - contextWindow: 372_000, - contextTokens: 372_000, + contextWindow: 1_050_000, + contextTokens: 1_050_000, maxTokens: 128_000, cost, thinkingLevelMap, diff --git a/extensions/openai/openai-provider.ts b/extensions/openai/openai-provider.ts index 5f0ed2aaf110..41ee88ab657a 100644 --- a/extensions/openai/openai-provider.ts +++ b/extensions/openai/openai-provider.ts @@ -29,7 +29,11 @@ import { isOpenAICodexBaseUrl, resolveOpenAIDefaultBaseUrl, } from "./base-url.js"; -import { applyOpenAIConfig, OPENAI_DEFAULT_MODEL } from "./default-models.js"; +import { + applyOpenAIConfig, + OPENAI_CODEX_DEFAULT_MODEL, + OPENAI_DEFAULT_MODEL, +} from "./default-models.js"; import { buildOpenAIChatGPTAuthMethods, buildOpenAICodexProviderHooks, @@ -50,6 +54,7 @@ const OPENAI_CODEX_MODELS_ENDPOINT = `${OPENAI_CODEX_RESPONSES_BASE_URL}/models? const OPENAI_MODELS_CACHE_TTL_MS = 60_000; const OPENAI_CODEX_MODELS_CACHE_TTL_MS = 60_000; const OPENAI_CHAT_LATEST_MODEL_ID = "chat-latest"; +const OPENAI_GPT_56_MODEL_ID = "gpt-5.6"; const OPENAI_GPT_56_SOL_MODEL_ID = "gpt-5.6-sol"; const OPENAI_GPT_56_TERRA_MODEL_ID = "gpt-5.6-terra"; const OPENAI_GPT_56_LUNA_MODEL_ID = "gpt-5.6-luna"; @@ -60,7 +65,8 @@ const OPENAI_GPT_54_PRO_MODEL_ID = "gpt-5.4-pro"; const OPENAI_GPT_54_MINI_MODEL_ID = "gpt-5.4-mini"; const OPENAI_GPT_54_NANO_MODEL_ID = "gpt-5.4-nano"; const OPENAI_GPT_53_CODEX_SPARK_MODEL_ID = "gpt-5.3-codex-spark"; -const OPENAI_GPT_56_CONTEXT_TOKENS = 372_000; +const OPENAI_GPT_56_DIRECT_CONTEXT_TOKENS = 1_050_000; +const OPENAI_CODEX_GPT_56_CONTEXT_TOKENS = 372_000; const OPENAI_GPT_55_CONTEXT_WINDOW = 1_000_000; const OPENAI_GPT_55_CONTEXT_TOKENS = 272_000; const OPENAI_GPT_55_PRO_CONTEXT_TOKENS = 1_000_000; @@ -121,12 +127,13 @@ const OPENAI_CHAT_LATEST_TEMPLATE_MODEL_IDS = [ ] as const; const OPENAI_GPT_56_TEMPLATE_MODEL_IDS = [OPENAI_GPT_55_MODEL_ID] as const; const OPENAI_GPT_56_THINKING_LEVEL_MAP = { - off: null, + off: "none", xhigh: "xhigh", max: "max", } as const; const OPENAI_MODERN_MODEL_IDS = [ OPENAI_CHAT_LATEST_MODEL_ID, + OPENAI_GPT_56_MODEL_ID, OPENAI_GPT_56_SOL_MODEL_ID, OPENAI_GPT_56_TERRA_MODEL_ID, OPENAI_GPT_56_LUNA_MODEL_ID, @@ -315,11 +322,45 @@ function resolveCodexModelInput( return input.size > 0 ? [...input] : (fallback?.input ?? ["text", "image"]); } +function normalizeOpenAICodexCatalogModel( + model: ModelDefinitionConfig, +): ModelDefinitionConfig | undefined { + const modelId = normalizeLowercaseStringOrEmpty(model.id); + if (modelId === OPENAI_GPT_56_MODEL_ID) { + return undefined; + } + if ( + modelId === OPENAI_GPT_56_SOL_MODEL_ID || + modelId === OPENAI_GPT_56_TERRA_MODEL_ID || + modelId === OPENAI_GPT_56_LUNA_MODEL_ID + ) { + const supportedReasoningEfforts = model.compat?.supportedReasoningEfforts?.filter( + (effort) => effort !== "none", + ); + return { + ...model, + contextWindow: OPENAI_CODEX_GPT_56_CONTEXT_TOKENS, + contextTokens: OPENAI_CODEX_GPT_56_CONTEXT_TOKENS, + thinkingLevelMap: { ...model.thinkingLevelMap, off: null }, + ...(model.compat + ? { + compat: { + ...model.compat, + ...(supportedReasoningEfforts ? { supportedReasoningEfforts } : {}), + }, + } + : {}), + }; + } + return model; +} + function resolveCodexModelFallback(modelId: string): ModelDefinitionConfig | undefined { - return OPENAI_MANIFEST_PROVIDER.models.find( - (model) => - normalizeLowercaseStringOrEmpty(model.id) === normalizeLowercaseStringOrEmpty(modelId), + const fallbackModel = OPENAI_MANIFEST_PROVIDER.models.find( + (candidate) => + normalizeLowercaseStringOrEmpty(candidate.id) === normalizeLowercaseStringOrEmpty(modelId), ); + return fallbackModel ? normalizeOpenAICodexCatalogModel(fallbackModel) : undefined; } function buildOpenAICodexModelFromLiveRow(row: unknown): ModelDefinitionConfig | undefined { @@ -386,7 +427,10 @@ function buildOpenAICodexStaticProviderConfig(): ModelProviderConfig { baseUrl: OPENAI_CODEX_RESPONSES_BASE_URL, api: "openai-chatgpt-responses", auth: "oauth", - models: OPENAI_MANIFEST_PROVIDER.models, + models: OPENAI_MANIFEST_PROVIDER.models.flatMap((model) => { + const normalized = normalizeOpenAICodexCatalogModel(model); + return normalized ? [normalized] : []; + }), }; } @@ -593,13 +637,14 @@ function resolveOpenAIGptForwardCompatModel(ctx: ProviderResolveDynamicModelCont maxTokens: OPENAI_GPT_54_MAX_TOKENS, }; } else if ( + lower === OPENAI_GPT_56_MODEL_ID || lower === OPENAI_GPT_56_SOL_MODEL_ID || lower === OPENAI_GPT_56_TERRA_MODEL_ID || lower === OPENAI_GPT_56_LUNA_MODEL_ID ) { templateIds = OPENAI_GPT_56_TEMPLATE_MODEL_IDS; const cost = - lower === OPENAI_GPT_56_SOL_MODEL_ID + lower === OPENAI_GPT_56_MODEL_ID || lower === OPENAI_GPT_56_SOL_MODEL_ID ? OPENAI_GPT_56_SOL_COST : lower === OPENAI_GPT_56_TERRA_MODEL_ID ? OPENAI_GPT_56_TERRA_COST @@ -611,8 +656,8 @@ function resolveOpenAIGptForwardCompatModel(ctx: ProviderResolveDynamicModelCont reasoning: true, input: ["text", "image"], cost, - contextWindow: OPENAI_GPT_56_CONTEXT_TOKENS, - contextTokens: OPENAI_GPT_56_CONTEXT_TOKENS, + contextWindow: OPENAI_GPT_56_DIRECT_CONTEXT_TOKENS, + contextTokens: OPENAI_GPT_56_DIRECT_CONTEXT_TOKENS, maxTokens: OPENAI_GPT_54_MAX_TOKENS, thinkingLevelMap: OPENAI_GPT_56_THINKING_LEVEL_MAP, }; @@ -736,6 +781,7 @@ export function buildOpenAIProvider(): ProviderPlugin { promptMessage: "Enter OpenAI API key", profileId: "openai:api-key", defaultModel: OPENAI_DEFAULT_MODEL, + preserveExistingPrimary: true, expectedProviders: ["openai"], applyConfig: (cfg) => applyOpenAIConfig(cfg), wizard: { @@ -871,7 +917,7 @@ export function buildOpenAIProvider(): ProviderPlugin { if (ctx.listProfileIds(PROVIDER_ID).length === 0) { return undefined; } - return `No API key found for provider "openai". You are authenticated with OpenAI ChatGPT/Codex OAuth. Use ${OPENAI_DEFAULT_MODEL} with the ChatGPT/Codex OAuth profile, or set OPENAI_API_KEY for direct OpenAI API access.`; + return `No API key found for provider "openai". You are authenticated with OpenAI ChatGPT/Codex OAuth. Use ${OPENAI_CODEX_DEFAULT_MODEL} with the ChatGPT/Codex OAuth profile, or set OPENAI_API_KEY for direct OpenAI API access.`; }, matchesContextOverflowError: ({ errorMessage }) => /content_filter.*(?:prompt|input).*(?:too long|exceed)/i.test(errorMessage), diff --git a/extensions/openai/openclaw.plugin.json b/extensions/openai/openclaw.plugin.json index 500e555fbe6a..e1cb993c7ea8 100644 --- a/extensions/openai/openclaw.plugin.json +++ b/extensions/openai/openclaw.plugin.json @@ -105,18 +105,32 @@ "maxTokens": 128000, "cost": { "input": 30, "output": 180, "cacheRead": 0, "cacheWrite": 0 } }, + { + "id": "gpt-5.6", + "name": "GPT-5.6", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 1050000, + "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"] + } + }, { "id": "gpt-5.6-sol", "name": "GPT-5.6 Sol", "reasoning": true, "input": ["text", "image"], - "contextWindow": 372000, + "contextWindow": 1050000, "maxTokens": 128000, "cost": { "input": 5, "output": 30, "cacheRead": 0.5, "cacheWrite": 6.25 }, - "thinkingLevelMap": { "off": null, "xhigh": "xhigh", "max": "max" }, + "thinkingLevelMap": { "off": "none", "xhigh": "xhigh", "max": "max" }, "compat": { "supportsReasoningEffort": true, - "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"] + "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"] } }, { @@ -124,13 +138,13 @@ "name": "GPT-5.6 Terra", "reasoning": true, "input": ["text", "image"], - "contextWindow": 372000, + "contextWindow": 1050000, "maxTokens": 128000, "cost": { "input": 2.5, "output": 15, "cacheRead": 0.25, "cacheWrite": 3.125 }, - "thinkingLevelMap": { "off": null, "xhigh": "xhigh", "max": "max" }, + "thinkingLevelMap": { "off": "none", "xhigh": "xhigh", "max": "max" }, "compat": { "supportsReasoningEffort": true, - "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"] + "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"] } }, { @@ -138,13 +152,13 @@ "name": "GPT-5.6 Luna", "reasoning": true, "input": ["text", "image"], - "contextWindow": 372000, + "contextWindow": 1050000, "maxTokens": 128000, "cost": { "input": 1, "output": 6, "cacheRead": 0.1, "cacheWrite": 1.25 }, - "thinkingLevelMap": { "off": null, "xhigh": "xhigh", "max": "max" }, + "thinkingLevelMap": { "off": "none", "xhigh": "xhigh", "max": "max" }, "compat": { "supportsReasoningEffort": true, - "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"] + "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"] } }, { diff --git a/extensions/openai/test-support/provider-catalog.contract-test-support.ts b/extensions/openai/test-support/provider-catalog.contract-test-support.ts index 57a73140c2c7..398d4ed45333 100644 --- a/extensions/openai/test-support/provider-catalog.contract-test-support.ts +++ b/extensions/openai/test-support/provider-catalog.contract-test-support.ts @@ -118,7 +118,7 @@ export function describeOpenAIProviderCatalogContract() { const { openaiProvider } = await contractDepsPromise; expectCodexMissingAuthHint( (params) => openaiProvider.buildMissingAuthMessage?.(params.context) ?? undefined, - "openai/gpt-5.5", + "openai/gpt-5.6-sol", ); }); diff --git a/extensions/qa-lab/src/model-selection.runtime.test.ts b/extensions/qa-lab/src/model-selection.runtime.test.ts index ab737776da99..9dbeb8d0e313 100644 --- a/extensions/qa-lab/src/model-selection.runtime.test.ts +++ b/extensions/qa-lab/src/model-selection.runtime.test.ts @@ -37,7 +37,7 @@ describe("qa model selection runtime", () => { resolveEnvApiKey.mockReturnValue({ apiKey: "sk-test" }); expect(resolveQaPreferredLiveModel()).toBeUndefined(); - expect(defaultQaRuntimeModelForMode("live-frontier")).toBe("openai/gpt-5.5"); + expect(defaultQaRuntimeModelForMode("live-frontier")).toBe("openai/gpt-5.6"); expect(loadAuthProfileStoreForRuntime).not.toHaveBeenCalled(); }); @@ -71,7 +71,7 @@ describe("qa model selection runtime", () => { }); expect(resolveQaPreferredLiveModel()).toBeUndefined(); - expect(defaultQaRuntimeModelForMode("live-frontier")).toBe("openai/gpt-5.5"); + expect(defaultQaRuntimeModelForMode("live-frontier")).toBe("openai/gpt-5.6"); }); it("leaves mock defaults unchanged", () => { diff --git a/extensions/qa-lab/src/providers/live-frontier/index.ts b/extensions/qa-lab/src/providers/live-frontier/index.ts index aeec24c21ccd..5c5716dbc8c0 100644 --- a/extensions/qa-lab/src/providers/live-frontier/index.ts +++ b/extensions/qa-lab/src/providers/live-frontier/index.ts @@ -31,7 +31,7 @@ function isClaudeOpusModel(modelRef: string) { export const liveFrontierProviderDefinition: QaProviderDefinition = { mode: "live-frontier", kind: "live", - defaultModel: (options) => options?.preferredLiveModel ?? "openai/gpt-5.5", + defaultModel: (options) => options?.preferredLiveModel ?? "openai/gpt-5.6", defaultImageGenerationProviderIds: ["openai"], defaultImageGenerationModel: ({ modelProviderIds }) => modelProviderIds.includes("openai") ? "openai/gpt-image-1" : null, diff --git a/packages/ai/src/providers/openai-reasoning-effort.test.ts b/packages/ai/src/providers/openai-reasoning-effort.test.ts index a842d305fa10..878c7d5224b8 100644 --- a/packages/ai/src/providers/openai-reasoning-effort.test.ts +++ b/packages/ai/src/providers/openai-reasoning-effort.test.ts @@ -7,7 +7,7 @@ import { } from "./openai-reasoning-effort.js"; describe("OpenAI reasoning effort support", () => { - it("preserves max for the GPT-5.6 series", () => { + it("preserves disabled and max effort for the GPT-5.6 series", () => { const sol = { provider: "openai", id: "gpt-5.6-sol" }; const terra = { provider: "openai", id: "gpt-5.6-terra" }; const luna = { provider: "openai", id: "gpt-5.6-luna" }; @@ -15,6 +15,9 @@ describe("OpenAI reasoning effort support", () => { expect(resolveOpenAIReasoningEffortForModel({ model: sol, effort: "max" })).toBe("max"); expect(resolveOpenAIReasoningEffortForModel({ model: terra, effort: "max" })).toBe("max"); expect(resolveOpenAIReasoningEffortForModel({ model: luna, effort: "max" })).toBe("max"); + expect(resolveOpenAIReasoningEffortForModel({ model: sol, effort: "off" })).toBe("none"); + expect(resolveOpenAIReasoningEffortForModel({ model: terra, effort: "none" })).toBe("none"); + expect(resolveOpenAIReasoningEffortForModel({ model: luna, effort: "off" })).toBe("none"); }); it.each([ diff --git a/packages/ai/src/providers/openai-reasoning-effort.ts b/packages/ai/src/providers/openai-reasoning-effort.ts index 377dfaa20cc1..124a98062441 100644 --- a/packages/ai/src/providers/openai-reasoning-effort.ts +++ b/packages/ai/src/providers/openai-reasoning-effort.ts @@ -32,7 +32,7 @@ type OpenAIReasoningModel = { const GPT_5_REASONING_EFFORTS = ["minimal", "low", "medium", "high"] as const; const GPT_51_REASONING_EFFORTS = ["none", "low", "medium", "high"] as const; const GPT_52_REASONING_EFFORTS = ["none", "low", "medium", "high", "xhigh"] as const; -const GPT_56_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const; +const GPT_56_REASONING_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"] as const; const GPT_CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"] as const; const GPT_PRO_REASONING_EFFORTS = ["medium", "high", "xhigh"] as const; const GPT_5_PRO_REASONING_EFFORTS = ["high"] as const; @@ -167,6 +167,9 @@ export function resolveOpenAIReasoningEffortForModel(params: { if (supported.includes(normalized as OpenAIApiReasoningEffort)) { return normalized as OpenAIApiReasoningEffort; } + if (requested === "off" && supported.includes("none")) { + return "none"; + } if (isDisabledReasoningEffort(requested) || isDisabledReasoningEffort(normalized)) { return undefined; } diff --git a/scripts/e2e/lib/codex-on-demand/assertions.mjs b/scripts/e2e/lib/codex-on-demand/assertions.mjs index a4d0c4abf9df..1a298807ea63 100644 --- a/scripts/e2e/lib/codex-on-demand/assertions.mjs +++ b/scripts/e2e/lib/codex-on-demand/assertions.mjs @@ -91,8 +91,8 @@ if (!hasHarness) { } const primaryModel = cfg.agents?.defaults?.model?.primary; -if (primaryModel !== "openai/gpt-5.5") { - throw new Error(`expected OpenAI onboarding model openai/gpt-5.5, got ${primaryModel}`); +if (primaryModel !== "openai/gpt-5.6") { + throw new Error(`expected OpenAI onboarding model openai/gpt-5.6, got ${primaryModel}`); } const providerRuntime = cfg.models?.providers?.openai?.agentRuntime?.id; if (providerRuntime && providerRuntime !== "codex") { diff --git a/src/agents/live-model-filter.ts b/src/agents/live-model-filter.ts index 5235133af2c1..f918a65c7d4f 100644 --- a/src/agents/live-model-filter.ts +++ b/src/agents/live-model-filter.ts @@ -28,7 +28,7 @@ const HIGH_SIGNAL_LIVE_MODEL_PRIORITY = [ "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro", "minimax/minimax-m3", - "openai/gpt-5.5", + "openai/gpt-5.6", "openrouter/openai/gpt-5.2-chat", "openrouter/minimax/minimax-m2.7", "opencode-go/glm-5", @@ -56,6 +56,12 @@ export const DEFAULT_HIGH_SIGNAL_LIVE_MODEL_LIMIT = HIGH_SIGNAL_LIVE_MODEL_PRIOR /** Default cap for the small-model live smoke lane. */ export const DEFAULT_SMALL_LIVE_MODEL_LIMIT = SMALL_LIVE_MODEL_PRIORITY.length; const DEFAULT_HIGH_SIGNAL_LIVE_EXCLUDED_PROVIDERS = new Set(["codex", "codex-cli"]); +const DIRECT_OPENAI_HIGH_SIGNAL_LIVE_MODEL_IDS = new Set([ + "gpt-5.6", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", +]); const CURATED_ONLY_HIGH_SIGNAL_LIVE_PROVIDERS = new Set([ "fireworks", "google", @@ -157,7 +163,7 @@ function isUnsupportedOpenAiLiveModelRef(provider: string, id: string): boolean } const modelName = normalizeLowercaseStringOrEmpty(id).split("/").pop() ?? ""; if (provider === "openai") { - return modelName !== "gpt-5.5"; + return !DIRECT_OPENAI_HIGH_SIGNAL_LIVE_MODEL_IDS.has(modelName); } return !modelName.startsWith("gpt-5.2"); } diff --git a/src/agents/model-catalog-visibility.test.ts b/src/agents/model-catalog-visibility.test.ts index 96b2b1f625c3..1975d9195f34 100644 --- a/src/agents/model-catalog-visibility.test.ts +++ b/src/agents/model-catalog-visibility.test.ts @@ -4,7 +4,10 @@ */ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { resolveVisibleModelCatalog } from "./model-catalog-visibility.js"; +import { + isCodexRoutableOpenAIPlatformCatalogEntry, + resolveVisibleModelCatalog, +} from "./model-catalog-visibility.js"; import type { ModelCatalogEntry } from "./model-catalog.types.js"; const normalizeProviderModelIdWithRuntimeMock = vi.hoisted(() => vi.fn()); @@ -19,6 +22,20 @@ describe("resolveVisibleModelCatalog", () => { normalizeProviderModelIdWithRuntimeMock.mockReset(); }); + it("recognizes exact GPT-5.6 Codex ids without treating the API alias as routable", () => { + const entry = (id: string): ModelCatalogEntry => ({ + provider: "openai", + id, + name: id, + api: "openai-responses", + }); + + expect(isCodexRoutableOpenAIPlatformCatalogEntry(entry("gpt-5.6"))).toBe(false); + expect(isCodexRoutableOpenAIPlatformCatalogEntry(entry("gpt-5.6-sol"))).toBe(true); + expect(isCodexRoutableOpenAIPlatformCatalogEntry(entry("gpt-5.6-terra"))).toBe(true); + expect(isCodexRoutableOpenAIPlatformCatalogEntry(entry("gpt-5.6-luna"))).toBe(true); + }); + it("can use static auth checks for gateway read-only model lists", async () => { const authChecker = vi.fn((provider: string) => provider === "openai"); const catalog: ModelCatalogEntry[] = [ diff --git a/src/agents/model-catalog-visibility.ts b/src/agents/model-catalog-visibility.ts index 709373638526..1604fada96d3 100644 --- a/src/agents/model-catalog-visibility.ts +++ b/src/agents/model-catalog-visibility.ts @@ -20,6 +20,9 @@ type ProviderAuthChecker = (provider: string, modelApi?: string) => boolean | Pr const OPENAI_PROVIDER_ID = "openai"; const OPENAI_CODEX_RESPONSES_API = "openai-chatgpt-responses"; const OPENAI_CODEX_ROUTABLE_MODEL_IDS = new Set([ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", "gpt-5.5", "gpt-5.5-pro", "gpt-5.4", diff --git a/src/agents/model-compat.test.ts b/src/agents/model-compat.test.ts index ac8ccdb343e0..15667e806c70 100644 --- a/src/agents/model-compat.test.ts +++ b/src/agents/model-compat.test.ts @@ -393,9 +393,18 @@ describe("isModernModelRef", () => { it("includes plugin-advertised modern models", () => { providerRuntimeMocks.resolveProviderModernModelRef.mockImplementation(({ provider, context }) => provider === "openai" && - ["gpt-5.5", "gpt-5.5-pro", "gpt-5.4", "gpt-5.4-pro", "gpt-5.4-mini", "gpt-5.4-nano"].includes( - context.modelId, - ) + [ + "gpt-5.6", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gpt-5.5", + "gpt-5.5-pro", + "gpt-5.4", + "gpt-5.4-pro", + "gpt-5.4-mini", + "gpt-5.4-nano", + ].includes(context.modelId) ? true : provider === "openai" && ["gpt-5.5", "gpt-5.5-pro", "gpt-5.4", "gpt-5.4-pro", "gpt-5.4-mini"].includes( @@ -409,6 +418,8 @@ describe("isModernModelRef", () => { : undefined, ); + expect(isModernModelRef({ provider: "openai", id: "gpt-5.6" })).toBe(true); + expect(isModernModelRef({ provider: "openai", id: "gpt-5.6-sol" })).toBe(true); expect(isModernModelRef({ provider: "openai", id: "gpt-5.5" })).toBe(true); expect(isModernModelRef({ provider: "openai", id: "gpt-5.5-pro" })).toBe(true); expect(isModernModelRef({ provider: "openai", id: "gpt-5.4" })).toBe(true); @@ -495,7 +506,7 @@ describe("isHighSignalLiveModelRef", () => { ); }); - it("keeps only the current direct OpenAI-family model in the default live matrix", () => { + it("keeps only the current direct OpenAI-family models in the default live matrix", () => { providerRuntimeMocks.resolveProviderModernModelRef.mockReturnValue(true); expect(isHighSignalLiveModelRef({ provider: "openrouter", id: "openai/gpt-3.5-turbo" })).toBe( @@ -510,7 +521,10 @@ describe("isHighSignalLiveModelRef", () => { expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5" })).toBe(false); expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.1" })).toBe(false); expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.4" })).toBe(false); - expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.5" })).toBe(true); + expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.5" })).toBe(false); + for (const id of ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) { + expect(isHighSignalLiveModelRef({ provider: "openai", id })).toBe(true); + } expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.2-codex" })).toBe(false); expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.2-chat-latest" })).toBe(false); expect(isHighSignalLiveModelRef({ provider: "openrouter", id: "openai/gpt-5.1-chat" })).toBe( @@ -680,7 +694,7 @@ describe("isPrioritizedHighSignalLiveModelRef", () => { { provider: "deepseek", id: "deepseek-v4-flash" }, { provider: "deepseek", id: "deepseek-v4-pro" }, { provider: "minimax", id: "minimax-m3" }, - { provider: "openai", id: "gpt-5.5" }, + { provider: "openai", id: "gpt-5.6" }, { provider: "openrouter", id: "openai/gpt-5.2-chat" }, { provider: "openrouter", id: "minimax/minimax-m2.7" }, { provider: "opencode-go", id: "glm-5" }, diff --git a/src/commands/onboard-inference.test.ts b/src/commands/onboard-inference.test.ts index e35a333fd369..ab131dbd5660 100644 --- a/src/commands/onboard-inference.test.ts +++ b/src/commands/onboard-inference.test.ts @@ -17,6 +17,11 @@ 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"); + expect(CODEX_APP_SERVER_DEFAULT_MODEL_REF).toBe("openai/gpt-5.6-sol"); + }); + it("returns nothing when no backend exists", async () => { const candidates = await detectInferenceBackends({ env: {}, diff --git a/src/commands/onboard-inference.ts b/src/commands/onboard-inference.ts index 23d3270c0b7a..0967fb683781 100644 --- a/src/commands/onboard-inference.ts +++ b/src/commands/onboard-inference.ts @@ -5,7 +5,6 @@ import { readGeminiCliCredentialsCached, } from "../agents/cli-credentials.js"; // Inference backend detection shared by onboarding bootstrap and Crestodian setup. -import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { probeLocalCommand, type LocalCommandProbe } from "../crestodian/probes.js"; @@ -16,10 +15,10 @@ import { probeLocalCommand, type LocalCommandProbe } from "../crestodian/probes. * asking the user anything. The ladder order is a documented contract * (docs/cli/crestodian.md "Setup bootstrap") — change docs when changing it. */ -export const OPENAI_API_DEFAULT_MODEL_REF = `${DEFAULT_PROVIDER}/${DEFAULT_MODEL}`; +export const OPENAI_API_DEFAULT_MODEL_REF = "openai/gpt-5.6"; export const ANTHROPIC_API_DEFAULT_MODEL_REF = "anthropic/claude-opus-4-8"; export const CLAUDE_CLI_DEFAULT_MODEL_REF = "claude-cli/claude-opus-4-8"; -export const CODEX_APP_SERVER_DEFAULT_MODEL_REF = OPENAI_API_DEFAULT_MODEL_REF; +export const CODEX_APP_SERVER_DEFAULT_MODEL_REF = "openai/gpt-5.6-sol"; export const GEMINI_CLI_DEFAULT_MODEL_REF = "google-gemini-cli/gemini-3.1-pro-preview"; export type InferenceBackendKind = diff --git a/src/crestodian/assistant.test.ts b/src/crestodian/assistant.test.ts index e3d521dbd988..cc94e6d58b06 100644 --- a/src/crestodian/assistant.test.ts +++ b/src/crestodian/assistant.test.ts @@ -211,7 +211,7 @@ describe("Crestodian assistant", () => { const codexAppServerEntries = requireRecord(codexAppServerPlugins.entries); const codexAppServerCodexEntry = requireRecord(codexAppServerEntries.codex); expect(codexAppServerDefaults.workspace).toBe("/tmp/workspace"); - expect(codexAppServerModel.primary).toBe("openai/gpt-5.5"); + expect(codexAppServerModel.primary).toBe("openai/gpt-5.6-sol"); expect(codexAppServerCodexEntry.enabled).toBe(true); }); @@ -246,12 +246,12 @@ describe("Crestodian assistant", () => { } expect(result.command).toBe("gateway status"); expect(result.reply).toBe("Codex planner online."); - expect(result.modelLabel).toBe("openai/gpt-5.5 via codex"); + expect(result.modelLabel).toBe("openai/gpt-5.6-sol via codex"); expect(runEmbeddedAgent).toHaveBeenCalledTimes(1); const firstEmbeddedCall = firstMockArg(runEmbeddedAgent); expect(firstEmbeddedCall.provider).toBe("openai"); - expect(firstEmbeddedCall.model).toBe("gpt-5.5"); + expect(firstEmbeddedCall.model).toBe("gpt-5.6-sol"); expect(firstEmbeddedCall.agentHarnessId).toBe("codex"); expect(firstEmbeddedCall.disableTools).toBe(true); expect(firstEmbeddedCall.toolsAllow).toEqual([]); @@ -262,7 +262,7 @@ describe("Crestodian assistant", () => { const embeddedPlugins = requireRecord(embeddedConfig.plugins); const embeddedEntries = requireRecord(embeddedPlugins.entries); const embeddedCodexEntry = requireRecord(embeddedEntries.codex); - expect(embeddedModel.primary).toBe("openai/gpt-5.5"); + expect(embeddedModel.primary).toBe("openai/gpt-5.6-sol"); expect(embeddedCodexEntry.enabled).toBe(true); }); diff --git a/src/crestodian/operations.test.ts b/src/crestodian/operations.test.ts index b83907039a09..3aa599af5d4e 100644 --- a/src/crestodian/operations.test.ts +++ b/src/crestodian/operations.test.ts @@ -737,7 +737,7 @@ describe("parseCrestodianOperation", () => { const { runtime, lines } = createCrestodianTestRuntime(); const applySetup = vi.fn(async () => ({ configPath: path.join(tempDir, "openclaw.json"), - lines: ["Workspace: /tmp/work", "Default model: openai/gpt-5.5"], + lines: ["Workspace: /tmp/work", "Default model: openai/gpt-5.6"], })); const plan = await executeCrestodianOperation( @@ -748,7 +748,7 @@ describe("parseCrestodianOperation", () => { expectRecordFields(plan as unknown as Record, { applied: false, }); - expect(lines.join("\n")).toContain("Model choice: openai/gpt-5.5 (OPENAI_API_KEY)."); + expect(lines.join("\n")).toContain("Model choice: openai/gpt-5.6 (OPENAI_API_KEY)."); expect(applySetup).not.toHaveBeenCalled(); const result = await executeCrestodianOperation( @@ -765,7 +765,7 @@ describe("parseCrestodianOperation", () => { expect(lines.join("\n")).toContain("[crestodian] done: crestodian.setup"); expect(applySetup).toHaveBeenCalledWith({ workspace: "/tmp/work", - model: "openai/gpt-5.5", + model: "openai/gpt-5.6", surface: "cli", runtime, }); @@ -775,12 +775,12 @@ describe("parseCrestodianOperation", () => { audit, { operation: "crestodian.setup", - summary: "Bootstrapped setup with openai/gpt-5.5", + summary: "Bootstrapped setup with openai/gpt-5.6", }, { rescue: true, workspace: "/tmp/work", - model: "openai/gpt-5.5", + model: "openai/gpt-5.6", modelSource: "OPENAI_API_KEY", }, ); diff --git a/src/crestodian/setup-inference.test.ts b/src/crestodian/setup-inference.test.ts index 25ac209c9f04..eff350715666 100644 --- a/src/crestodian/setup-inference.test.ts +++ b/src/crestodian/setup-inference.test.ts @@ -751,9 +751,10 @@ describe("activateSetupInference", () => { async (params: { transform: (config: OpenClawConfig) => { nextConfig: OpenClawConfig } }) => { const transformed = params.transform(persistedConfig).nextConfig; const configuredRuntime = - transformed.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime?.id ?? - transformed.agents?.list?.find((agent) => agent.id === "ops")?.models?.["openai/gpt-5.5"] - ?.agentRuntime?.id; + transformed.agents?.defaults?.models?.["openai/gpt-5.6-sol"]?.agentRuntime?.id ?? + transformed.agents?.list?.find((agent) => agent.id === "ops")?.models?.[ + "openai/gpt-5.6-sol" + ]?.agentRuntime?.id; events.push( configuredRuntime === "codex" ? "persist-plugin-config" : "persist-plugin-install", ); @@ -798,10 +799,13 @@ describe("activateSetupInference", () => { expect.objectContaining({ id: "ops", model: { - primary: "openai/gpt-5.5", + primary: "openai/gpt-5.6-sol", fallbacks: ["google/gemini-3.1-pro-preview"], }, - models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } } }, + models: { + "openai/gpt-5.5": { agentRuntime: { id: "openclaw" } }, + "openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } }, + }, }), ], }, @@ -811,7 +815,7 @@ describe("activateSetupInference", () => { }, }, }), - model: "openai/gpt-5.5", + model: "openai/gpt-5.6-sol", agentId: "ops", }), ); @@ -852,10 +856,13 @@ describe("activateSetupInference", () => { expect.objectContaining({ id: "ops", model: { - primary: "openai/gpt-5.5", + primary: "openai/gpt-5.6-sol", fallbacks: ["google/gemini-3.1-pro-preview"], }, - models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } } }, + models: { + "openai/gpt-5.5": { agentRuntime: { id: "openclaw" } }, + "openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } }, + }, }), ], }, @@ -879,10 +886,13 @@ describe("activateSetupInference", () => { expect.objectContaining({ id: "ops", model: { - primary: "openai/gpt-5.5", + primary: "openai/gpt-5.6-sol", fallbacks: ["google/gemini-3.1-pro-preview"], }, - models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } } }, + models: { + "openai/gpt-5.5": { agentRuntime: { id: "openclaw" } }, + "openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } }, + }, }), ], }, diff --git a/src/plugins/provider-api-key-auth.ts b/src/plugins/provider-api-key-auth.ts index f53028d5ccb5..9a1b883f6c01 100644 --- a/src/plugins/provider-api-key-auth.ts +++ b/src/plugins/provider-api-key-auth.ts @@ -2,6 +2,7 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; import { upsertAuthProfileWithLock } from "../agents/auth-profiles/profiles.js"; +import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { SecretInput } from "../config/types.secrets.js"; import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js"; @@ -26,6 +27,7 @@ type ProviderApiKeyAuthMethodOptions = { profileIds?: string[]; allowProfile?: boolean; defaultModel?: string; + preserveExistingPrimary?: boolean; expectedProviders?: string[]; metadata?: Record; noteMessage?: string; @@ -74,6 +76,7 @@ async function applyApiKeyConfig(params: { providerId: string; profileIds: string[]; defaultModel?: string; + preserveExistingPrimary?: boolean; applyConfig?: (cfg: OpenClawConfig) => OpenClawConfig; }) { const { applyAuthProfileConfig, applyPrimaryModel } = await loadProviderApiKeyAuthRuntime(); @@ -88,7 +91,16 @@ async function applyApiKeyConfig(params: { if (params.applyConfig) { next = params.applyConfig(next); } - return params.defaultModel ? applyPrimaryModel(next, params.defaultModel) : next; + if (!params.defaultModel) { + return next; + } + if ( + params.preserveExistingPrimary === true && + resolveAgentModelPrimaryValue(next.agents?.defaults?.model) !== undefined + ) { + return next; + } + return applyPrimaryModel(next, params.defaultModel); } /** Creates a provider auth method that captures, stores, and configures API-key credentials. */ @@ -204,6 +216,7 @@ export function createProviderApiKeyAuthMethod( providerId: params.providerId, profileIds, defaultModel: params.defaultModel, + preserveExistingPrimary: params.preserveExistingPrimary, applyConfig: params.applyConfig, }); }, diff --git a/src/plugins/provider-auth-choice-helpers.test.ts b/src/plugins/provider-auth-choice-helpers.test.ts index 186ea6db3b46..847f83bf8d25 100644 --- a/src/plugins/provider-auth-choice-helpers.test.ts +++ b/src/plugins/provider-auth-choice-helpers.test.ts @@ -31,6 +31,28 @@ describe("applyProviderAuthConfigPatch", () => { expect(next.agents?.defaults?.model).toEqual(base.agents.defaults.model); }); + it("keeps configured primary and fallback refs in a newly introduced allowlist", () => { + const next = applyProviderAuthConfigPatch( + { + agents: { + defaults: { + model: { + primary: "openai/gpt-5.5", + fallbacks: ["anthropic/claude-opus-4-6"], + }, + }, + }, + }, + { agents: { defaults: { models: { "openai/gpt-5.6-sol": {} } } } }, + ); + + expect(next.agents?.defaults?.models).toEqual({ + "openai/gpt-5.6-sol": {}, + "openai/gpt-5.5": {}, + "anthropic/claude-opus-4-6": {}, + }); + }); + it("replaces the allowlist only when replaceDefaultModels is set", () => { const patch = { agents: { @@ -176,6 +198,7 @@ describe("applyProviderAuthConfigPatch", () => { alias: "gemini", params: { thinking: "high", maxTokens: 12_000 }, }, + "openai/gpt-5.5": {}, }); }); @@ -327,6 +350,10 @@ describe("applyDefaultModel", () => { expect(next.agents?.defaults?.model).toEqual({ primary: "anthropic/claude-opus-4-6", }); + expect(next.agents?.defaults?.models).toEqual({ + "openrouter/auto": {}, + "anthropic/claude-opus-4-6": {}, + }); }); it("normalizes a preserved retired Google Gemini primary", () => { @@ -363,6 +390,11 @@ describe("applyDefaultModel", () => { primary: "anthropic/claude-opus-4-6", fallbacks: ["openai/gpt-5.4"], }); + expect(next.agents?.defaults?.models).toEqual({ + "openrouter/auto": {}, + "anthropic/claude-opus-4-6": {}, + "openai/gpt-5.4": {}, + }); }); it("adds the model to the allowlist", () => { diff --git a/src/plugins/provider-auth-choice-helpers.ts b/src/plugins/provider-auth-choice-helpers.ts index 9c27592b6016..5b866548a05a 100644 --- a/src/plugins/provider-auth-choice-helpers.ts +++ b/src/plugins/provider-auth-choice-helpers.ts @@ -276,6 +276,38 @@ function normalizeConfigModelRefsForWrite( }; } +/** Keep a restrictive model allowlist consistent with the configured primary and fallbacks. */ +function ensureConfiguredDefaultModelsAllowed(cfg: OpenClawConfig): OpenClawConfig { + const defaults = cfg.agents?.defaults; + if (!defaults?.models) { + return cfg; + } + const model = defaults.model; + const refs = [ + typeof model === "string" ? model : model?.primary, + ...(typeof model === "object" ? (model.fallbacks ?? []) : []), + ].filter((ref): ref is string => typeof ref === "string" && ref.trim().length > 0); + const models = normalizeAgentModelMapForConfig(defaults.models); + let changed = false; + for (const ref of refs) { + const normalizedRef = normalizeAgentModelRefForConfig(ref); + if (!models[normalizedRef]) { + models[normalizedRef] = {}; + changed = true; + } + } + if (!changed) { + return cfg; + } + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { ...defaults, models }, + }, + }; +} + export function applyProviderAuthConfigPatch( cfg: OpenClawConfig, patch: unknown, @@ -291,7 +323,7 @@ export function applyProviderAuthConfigPatch( providerConfigNormalizer, ); if (!options?.replaceDefaultModels || !isPlainRecord(patch)) { - return merged; + return ensureConfiguredDefaultModelsAllowed(merged); } const patchModels = (patch.agents as { defaults?: { models?: unknown } } | undefined)?.defaults @@ -369,7 +401,7 @@ export function applyDefaultModel( normalizeAgentModelRefForConfig(fallback), ) : undefined; - return { + return ensureConfiguredDefaultModelsAllowed({ ...cfg, agents: { ...cfg.agents, @@ -385,5 +417,5 @@ export function applyDefaultModel( }, }, }, - }; + }); } diff --git a/test/scripts/codex-install-assertions.test.ts b/test/scripts/codex-install-assertions.test.ts index e2c4f30bd35b..d17d5d9fb476 100644 --- a/test/scripts/codex-install-assertions.test.ts +++ b/test/scripts/codex-install-assertions.test.ts @@ -222,7 +222,7 @@ function createCodexInstallFixture(root: string) { writeFileSync(codexBin, "#!/usr/bin/env node\n", { mode: 0o755 }); chmodSync(codexBin, 0o755); writeJson(path.join(stateDir, "openclaw.json"), { - agents: { defaults: { model: { primary: "openai/gpt-5.5" } } }, + agents: { defaults: { model: { primary: "openai/gpt-5.6" } } }, models: { providers: { openai: { agentRuntime: { id: "codex" } } } }, plugins: { installs: {