fix(openai): remove duplicate GPT-5.6 picker entry (#122178)

* fix(openai): remove duplicate GPT-5.6 picker entry

* test(gateway): split OpenAI model picker coverage

* test(openai): align canonical model fixtures

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