mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
a1a1c791fc
commit
8c567306ba
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"8aef71ef2bd4a5984c9a0ddea4c4f1e6d86b9bf74504b50f8ec6a25ad65401cb","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
{"contentHash":"48813fa984919c8790905b995f4033d19fc9178e6eed7f2e5148ea9729c5eba3","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
|
||||
@@ -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" } } },
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user