diff --git a/docs/concepts/models.md b/docs/concepts/models.md index f855af86304b..6d78b79db6b5 100644 --- a/docs/concepts/models.md +++ b/docs/concepts/models.md @@ -80,7 +80,7 @@ Other selection rules: - Changing `agents.defaults.model.primary` does not rewrite existing session pins. If status reports `This session is pinned to X; config primary Y will apply to new/unpinned sessions.`, run `/model default` to clear the pin. - CLI default-model and allowlist pickers respect `models.mode: "replace"` by listing only `models.providers.*.models` instead of the full built-in catalog. -- The Control UI model picker asks the Gateway for its configured model view. An explicit `modelPolicy.allow` filters it, including trailing prefix wildcard entries; otherwise it shows configured models plus providers with usable auth. The full built-in catalog is reserved for explicit browse views (`models.list` with `view: "all"`, or `openclaw models list --all`). +- The Control UI model picker asks the Gateway for its configured model view. An explicit `modelPolicy.allow` filters it, including trailing prefix wildcard entries; otherwise it shows configured models plus providers with usable auth. Default and configured picker views hide catalog rows marked `deprecated` or `disabled` unless that exact model is configured as a primary, fallback, utility/tool model, alias/settings key, or exact policy entry. Hidden rows remain selectable by exact `provider/model` ref. The full built-in catalog, including hidden rows, is reserved for explicit browse views (`models.list` with `view: "all"`, or `openclaw models list --all`). - Provider inventory UIs use `models.list` with `view: "provider-config"` to show source-authored `models.providers.*.models` rows without applying picker allowlists. Full mechanics: [Model failover](/concepts/model-failover). diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md index 51ab093c8a8d..e9e2be97d169 100644 --- a/docs/providers/anthropic.md +++ b/docs/providers/anthropic.md @@ -543,7 +543,7 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth. "anthropic/claude-opus-5": {}, "anthropic/claude-sonnet-5": {}, "anthropic/claude-mythos-5": {}, - "anthropic/claude-opus-4-6": {}, + "anthropic/claude-opus-4-8": {}, }, }, }, diff --git a/docs/providers/deepseek.md b/docs/providers/deepseek.md index 88b75ab4db8c..e0bdee01f6ea 100644 --- a/docs/providers/deepseek.md +++ b/docs/providers/deepseek.md @@ -76,16 +76,16 @@ available to that process (for example, in `~/.openclaw/.env` or via ## Built-in catalog -| Model ref | Name | Input | Context | Max output | Notes | -| ---------------------------- | ----------------- | ----- | --------- | ---------- | --------------------------------------------------- | -| `deepseek/deepseek-v4-flash` | DeepSeek V4 Flash | text | 1,000,000 | 384,000 | Default model; V4 thinking-capable surface | -| `deepseek/deepseek-v4-pro` | DeepSeek V4 Pro | text | 1,000,000 | 384,000 | V4 thinking-capable surface | -| `deepseek/deepseek-chat` | DeepSeek Chat | text | 1,000,000 | 384,000 | Deprecated V4 Flash non-thinking compatibility name | -| `deepseek/deepseek-reasoner` | DeepSeek Reasoner | text | 1,000,000 | 384,000 | Deprecated V4 Flash thinking compatibility name | +| Model ref | Name | Input | Context | Max output | Notes | +| ---------------------------- | ----------------- | ----- | --------- | ---------- | --------------------------------------------- | +| `deepseek/deepseek-v4-flash` | DeepSeek V4 Flash | text | 1,000,000 | 384,000 | Default model; V4 thinking-capable surface | +| `deepseek/deepseek-v4-pro` | DeepSeek V4 Pro | text | 1,000,000 | 384,000 | V4 thinking-capable surface | +| `deepseek/deepseek-chat` | DeepSeek Chat | text | 1,000,000 | 384,000 | Hidden deprecated V4 Flash compatibility name | +| `deepseek/deepseek-reasoner` | DeepSeek Reasoner | text | 1,000,000 | 384,000 | Hidden deprecated V4 Flash thinking name | -DeepSeek will retire `deepseek-chat` and `deepseek-reasoner` on July 24, 2026 -at 15:59 UTC. They currently route to DeepSeek V4 Flash in non-thinking and +DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026 at +15:59 UTC. They route to DeepSeek V4 Flash in non-thinking and thinking mode, respectively. Move configured model refs to `deepseek/deepseek-v4-flash` or `deepseek/deepseek-v4-pro` before the cutoff. diff --git a/docs/providers/moonshot.md b/docs/providers/moonshot.md index da3ace62cfc6..7273d94e3274 100644 --- a/docs/providers/moonshot.md +++ b/docs/providers/moonshot.md @@ -21,20 +21,16 @@ Moonshot and Kimi Coding are **separate providers**, each shipped as a separate | Model ref | Name | Reasoning | Input | Context | Max output | | ----------------------------------- | ------------------------ | ---------- | ----------- | --------- | ---------- | -| `moonshot/kimi-k2.6` | Kimi K2.6 | No | text, image | 262,144 | 262,144 | | `moonshot/kimi-k3` | Kimi K3 | Always max | text, image | 1,048,576 | 1,048,576 | | `moonshot/kimi-k2.7-code` | Kimi K2.7 Code | Always on | text, image | 262,144 | 262,144 | | `moonshot/kimi-k2.7-code-highspeed` | Kimi K2.7 Code HighSpeed | Always on | text, image | 262,144 | 262,144 | -| `moonshot/kimi-k2.5` | Kimi K2.5 | No | text, image | 262,144 | 262,144 | [//]: # "moonshot-kimi-k2-ids:end" Catalog cost estimates use Moonshot's published pay-as-you-go rates. Check the -live vendor pages for [Kimi K3](https://platform.kimi.ai/docs/pricing/chat-k3), -[Kimi K2.7 Code](https://platform.kimi.ai/docs/pricing/chat-k27-code), -[Kimi K2.6](https://platform.kimi.ai/docs/pricing/chat-k26), and -[Kimi K2.5](https://platform.kimi.ai/docs/pricing/chat-k25) before making cost -decisions. +live vendor pages for [Kimi K3](https://platform.kimi.ai/docs/pricing/chat-k3) +and [Kimi K2.7 Code](https://platform.kimi.ai/docs/pricing/chat-k27-code) +before making cost decisions. Kimi K3 always reasons at `reasoning_effort: "max"`. OpenClaw exposes only `/think max`, omits the K2-only `thinking` field, and removes sampling @@ -119,14 +115,12 @@ onboarding. env: { MOONSHOT_API_KEY: "sk-..." }, agents: { defaults: { - model: { primary: "moonshot/kimi-k2.6" }, + model: { primary: "moonshot/kimi-k3" }, models: { // moonshot-kimi-k2-aliases:start - "moonshot/kimi-k2.6": { alias: "Kimi K2.6" }, "moonshot/kimi-k3": { alias: "Kimi K3" }, "moonshot/kimi-k2.7-code": { alias: "Kimi K2.7 Code" }, "moonshot/kimi-k2.7-code-highspeed": { alias: "Kimi K2.7 Code HighSpeed" }, - "moonshot/kimi-k2.5": { alias: "Kimi K2.5" }, // moonshot-kimi-k2-aliases:end }, }, @@ -140,15 +134,6 @@ onboarding. api: "openai-completions", models: [ // moonshot-kimi-k2-models:start - { - id: "kimi-k2.6", - name: "Kimi K2.6", - reasoning: false, - input: ["text", "image"], - cost: { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 }, - contextWindow: 262144, - maxTokens: 262144, - }, { id: "kimi-k3", name: "Kimi K3", @@ -185,15 +170,6 @@ onboarding. contextWindow: 262144, maxTokens: 262144, }, - { - id: "kimi-k2.5", - name: "Kimi K2.5", - reasoning: false, - input: ["text", "image"], - cost: { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 }, - contextWindow: 262144, - maxTokens: 262144, - }, // moonshot-kimi-k2-models:end ], }, diff --git a/docs/providers/zai.md b/docs/providers/zai.md index 1faee05fb868..b403b2591fdf 100644 --- a/docs/providers/zai.md +++ b/docs/providers/zai.md @@ -165,22 +165,10 @@ openclaw models list --all --provider zai The manifest-backed catalog currently includes: -| Model ref | Notes | -| -------------------- | ------------------------------- | -| `zai/glm-5.2` | Coding Plan default; 1M context | -| `zai/glm-5.1` | General API default | -| `zai/glm-5` | | -| `zai/glm-5-turbo` | | -| `zai/glm-5v-turbo` | | -| `zai/glm-4.7` | | -| `zai/glm-4.7-flash` | | -| `zai/glm-4.7-flashx` | | -| `zai/glm-4.6` | | -| `zai/glm-4.6v` | | -| `zai/glm-4.5` | | -| `zai/glm-4.5-air` | | -| `zai/glm-4.5-flash` | | -| `zai/glm-4.5v` | | +| Model ref | Notes | +| ------------- | ------------------------------------------------- | +| `zai/glm-5.2` | Coding Plan default; 1M context | +| `zai/glm-5.1` | Deprecated; hidden unless configured; use GLM-5.2 | Catalog token-cost metadata follows Z.AI's current [pay-as-you-go pricing](https://docs.z.ai/guides/overview/pricing). Coding Plan @@ -188,7 +176,7 @@ subscriptions use plan quota instead of per-token billing; see the live [subscription page](https://z.ai/subscribe) for plan pricing and availability. -GLM models are available as `zai/` (example: `zai/glm-5`). +GLM models are available as `zai/` (example: `zai/glm-5.2`). diff --git a/extensions/anthropic/openclaw.plugin.json b/extensions/anthropic/openclaw.plugin.json index a287724e19ae..e3c5f6d149a5 100644 --- a/extensions/anthropic/openclaw.plugin.json +++ b/extensions/anthropic/openclaw.plugin.json @@ -53,6 +53,8 @@ { "id": "claude-opus-4-8", "name": "Claude Opus 4.8 (Claude CLI)", + "status": "deprecated", + "replacedBy": "claude-opus-5", "reasoning": true, "input": ["text", "image"], "mediaInput": { @@ -60,39 +62,6 @@ }, "contextWindow": 200000, "maxTokens": 128000 - }, - { - "id": "claude-opus-4-7", - "name": "Claude Opus 4.7 (Claude CLI)", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 2576, "preferredSidePx": 2576, "tokenMode": "provider" } - }, - "contextWindow": 200000, - "maxTokens": 128000 - }, - { - "id": "claude-sonnet-4-6", - "name": "Claude Sonnet 4.6 (Claude CLI)", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 1568, "preferredSidePx": 1568, "tokenMode": "provider" } - }, - "contextWindow": 200000, - "maxTokens": 128000 - }, - { - "id": "claude-opus-4-6", - "name": "Claude Opus 4.6 (Claude CLI)", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 1568, "preferredSidePx": 1568, "tokenMode": "provider" } - }, - "contextWindow": 200000, - "maxTokens": 128000 } ] }, @@ -172,17 +141,8 @@ { "id": "claude-opus-4-8", "name": "Claude Opus 4.8", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 2576, "preferredSidePx": 2576, "tokenMode": "provider" } - }, - "contextWindow": 1000000, - "maxTokens": 128000 - }, - { - "id": "claude-opus-4-7", - "name": "Claude Opus 4.7", + "status": "deprecated", + "replacedBy": "claude-opus-5", "reasoning": true, "input": ["text", "image"], "mediaInput": { @@ -201,39 +161,6 @@ }, "contextWindow": 200000, "maxTokens": 64000 - }, - { - "id": "claude-haiku-4-5-20251001", - "name": "Claude Haiku 4.5", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 1568, "preferredSidePx": 1568, "tokenMode": "provider" } - }, - "contextWindow": 200000, - "maxTokens": 64000 - }, - { - "id": "claude-sonnet-4-6", - "name": "Claude Sonnet 4.6", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 1568, "preferredSidePx": 1568, "tokenMode": "provider" } - }, - "contextWindow": 1000000, - "maxTokens": 128000 - }, - { - "id": "claude-opus-4-6", - "name": "Claude Opus 4.6", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 1568, "preferredSidePx": 1568, "tokenMode": "provider" } - }, - "contextWindow": 1000000, - "maxTokens": 128000 } ] } diff --git a/extensions/anthropic/openclaw.plugin.test.ts b/extensions/anthropic/openclaw.plugin.test.ts index 43bcd0a15d6f..e9381cfd2f2f 100644 --- a/extensions/anthropic/openclaw.plugin.test.ts +++ b/extensions/anthropic/openclaw.plugin.test.ts @@ -23,6 +23,8 @@ type AnthropicCatalogModel = { cacheWrite?: number; }; thinkingLevelMap?: Record; + status?: string; + replacedBy?: string; }; type AnthropicManifest = { @@ -97,47 +99,42 @@ describe("Anthropic plugin manifest", () => { }); }); - it("declares direct API limits without overstating bare Claude CLI context", () => { + it("declares Opus 4.8 limits without overstating bare Claude CLI context", () => { const models = manifest.modelCatalog?.providers?.anthropic?.models ?? []; - for (const id of ["claude-opus-4-7", "claude-sonnet-4-6", "claude-opus-4-6"]) { - expect(models.find((model) => model.id === id)).toMatchObject({ - contextWindow: 1_000_000, - maxTokens: 128_000, - }); - } + expect(models.find((model) => model.id === "claude-opus-4-8")).toMatchObject({ + contextWindow: 1_000_000, + maxTokens: 128_000, + status: "deprecated", + replacedBy: "claude-opus-5", + }); const cliModels = manifest.modelCatalog?.providers?.["claude-cli"]?.models ?? []; - for (const id of ["claude-opus-4-7", "claude-sonnet-4-6", "claude-opus-4-6"]) { - expect(cliModels.find((model) => model.id === id)).toMatchObject({ - contextWindow: 200_000, - maxTokens: 128_000, - }); - } expect(cliModels.find((model) => model.id === "claude-opus-4-8")).toMatchObject({ contextWindow: 200_000, maxTokens: 128_000, + status: "deprecated", + replacedBy: "claude-opus-5", }); }); - it("resolves both official Claude Haiku 4.5 API identifiers from the static catalog", () => { + it("keeps only the dateless Claude Haiku 4.5 identifier in the static catalog", () => { expect(manifest.modelCatalog?.discovery?.anthropic).toBe("refreshable"); const models = manifest.modelCatalog?.providers?.anthropic?.models ?? []; - for (const id of ["claude-haiku-4-5", "claude-haiku-4-5-20251001"]) { - expect(models.find((model) => model.id === id)).toEqual({ - id, - name: "Claude Haiku 4.5", - reasoning: true, - input: ["text", "image"], - mediaInput: { - image: { - maxSidePx: 1568, - preferredSidePx: 1568, - tokenMode: "provider", - }, + expect(models.find((model) => model.id === "claude-haiku-4-5")).toEqual({ + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5", + reasoning: true, + input: ["text", "image"], + mediaInput: { + image: { + maxSidePx: 1568, + preferredSidePx: 1568, + tokenMode: "provider", }, - contextWindow: 200000, - maxTokens: 64000, - }); - } + }, + contextWindow: 200000, + maxTokens: 64000, + }); + expect(models.find((model) => model.id === "claude-haiku-4-5-20251001")).toBeUndefined(); }); }); diff --git a/extensions/deepseek/openclaw.plugin.json b/extensions/deepseek/openclaw.plugin.json index e6065e2bb90b..fb71b6c8719d 100644 --- a/extensions/deepseek/openclaw.plugin.json +++ b/extensions/deepseek/openclaw.plugin.json @@ -70,6 +70,8 @@ { "id": "deepseek-chat", "name": "DeepSeek Chat", + "status": "deprecated", + "replacedBy": "deepseek-v4-flash", "input": ["text"], "contextWindow": 1000000, "maxTokens": 384000, @@ -87,6 +89,8 @@ { "id": "deepseek-reasoner", "name": "DeepSeek Reasoner", + "status": "deprecated", + "replacedBy": "deepseek-v4-flash", "reasoning": true, "input": ["text"], "contextWindow": 1000000, diff --git a/extensions/moonshot/openclaw.plugin.json b/extensions/moonshot/openclaw.plugin.json index 8bd8630ed278..09e6c84c781e 100644 --- a/extensions/moonshot/openclaw.plugin.json +++ b/extensions/moonshot/openclaw.plugin.json @@ -50,19 +50,6 @@ "baseUrl": "https://api.moonshot.ai/v1", "api": "openai-completions", "models": [ - { - "id": "kimi-k2.6", - "name": "Kimi K2.6", - "input": ["text", "image"], - "contextWindow": 262144, - "maxTokens": 262144, - "cost": { - "input": 0.95, - "output": 4, - "cacheRead": 0.16, - "cacheWrite": 0 - } - }, { "id": "kimi-k3", "name": "Kimi K3", @@ -117,19 +104,6 @@ "cacheRead": 0.38, "cacheWrite": 0 } - }, - { - "id": "kimi-k2.5", - "name": "Kimi K2.5", - "input": ["text", "image"], - "contextWindow": 262144, - "maxTokens": 262144, - "cost": { - "input": 0.6, - "output": 3, - "cacheRead": 0.1, - "cacheWrite": 0 - } } ] } diff --git a/extensions/moonshot/provider-catalog.test.ts b/extensions/moonshot/provider-catalog.test.ts index 4abc5ecdc97e..6eaa6919fccb 100644 --- a/extensions/moonshot/provider-catalog.test.ts +++ b/extensions/moonshot/provider-catalog.test.ts @@ -40,18 +40,10 @@ describe("moonshot provider catalog", () => { expect(provider.baseUrl).toBe(MOONSHOT_BASE_URL); expect(provider.api).toBe("openai-completions"); expect(provider.models.map((model) => model.id)).toEqual([ - "kimi-k2.6", "kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", - "kimi-k2.5", ]); - expect(requireMoonshotModel(provider, "kimi-k2.6").cost).toEqual({ - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }); expect(requireMoonshotModel(provider, "kimi-k3")).toMatchObject({ reasoning: true, thinkingLevelMap: { @@ -101,12 +93,6 @@ describe("moonshot provider catalog", () => { cacheWrite: 0, }, }); - expect(requireMoonshotModel(provider, "kimi-k2.5").cost).toEqual({ - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }); }); it("opts native Moonshot baseUrls into streaming usage only inside the extension", () => { diff --git a/extensions/openai/openai-provider.test.ts b/extensions/openai/openai-provider.test.ts index 75177427a5ef..d8016509c780 100644 --- a/extensions/openai/openai-provider.test.ts +++ b/extensions/openai/openai-provider.test.ts @@ -348,14 +348,17 @@ describe("buildOpenAIProvider", () => { expect(manifest.modelCatalog.discovery.openai).toBe("runtime"); }); - it("does not hardcode chatgpt-responses transport on gpt-5.3-codex catalog entry (#91710)", () => { + it("does not hardcode transport routing on static catalog entries (#91710)", () => { const openaiModels = manifest.modelCatalog.providers.openai.models as Array< Record >; - const codexEntry = openaiModels.find((m) => m.id === "gpt-5.3-codex"); - expect(codexEntry).toBeDefined(); - expect(codexEntry?.api).toBeUndefined(); - expect(codexEntry?.baseUrl).toBeUndefined(); + expect(openaiModels.length).toBeGreaterThan(0); + // Transport selection is runtime-owned; a manifest row pinning api/baseUrl + // would bypass route policy (the original #91710 regression). + for (const entry of openaiModels) { + expect(entry.api, `catalog row ${String(entry.id)} must not pin api`).toBeUndefined(); + expect(entry.baseUrl, `catalog row ${String(entry.id)} must not pin baseUrl`).toBeUndefined(); + } }); it("keeps a network-free OpenAI static catalog", async () => { diff --git a/extensions/openai/openclaw.plugin.json b/extensions/openai/openclaw.plugin.json index 15d372c0ccad..f34177fdbfe3 100644 --- a/extensions/openai/openclaw.plugin.json +++ b/extensions/openai/openclaw.plugin.json @@ -40,72 +40,6 @@ "api": "openai-responses", "defaultUtilityModel": "gpt-5.6-luna", "models": [ - { - "id": "gpt-5.3-chat-latest", - "name": "GPT-5.3 Chat (latest)", - "reasoning": false, - "input": ["text", "image"], - "contextWindow": 128000, - "maxTokens": 16384, - "cost": { "input": 1.75, "output": 14, "cacheRead": 0.175, "cacheWrite": 0 } - }, - { - "id": "gpt-5.3-codex", - "name": "GPT-5.3 Codex", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 400000, - "maxTokens": 128000, - "cost": { "input": 1.75, "output": 14, "cacheRead": 0.175, "cacheWrite": 0 } - }, - { - "id": "gpt-5.4", - "name": "GPT-5.4", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 2048, "preferredSidePx": 2048, "tokenMode": "detail" } - }, - "contextWindow": 272000, - "maxTokens": 128000, - "cost": { "input": 2.5, "output": 15, "cacheRead": 0.25, "cacheWrite": 0 } - }, - { - "id": "gpt-5.4-mini", - "name": "GPT-5.4 mini", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 2048, "preferredSidePx": 2048, "tokenMode": "detail" } - }, - "contextWindow": 400000, - "maxTokens": 128000, - "cost": { "input": 0.75, "output": 4.5, "cacheRead": 0.075, "cacheWrite": 0 } - }, - { - "id": "gpt-5.4-nano", - "name": "GPT-5.4 nano", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 2048, "preferredSidePx": 2048, "tokenMode": "detail" } - }, - "contextWindow": 400000, - "maxTokens": 128000, - "cost": { "input": 0.2, "output": 1.25, "cacheRead": 0.02, "cacheWrite": 0 } - }, - { - "id": "gpt-5.4-pro", - "name": "GPT-5.4 Pro", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 2048, "preferredSidePx": 2048, "tokenMode": "detail" } - }, - "contextWindow": 1050000, - "maxTokens": 128000, - "cost": { "input": 30, "output": 180, "cacheRead": 0, "cacheWrite": 0 } - }, { "id": "gpt-5.6", "name": "GPT-5.6", @@ -173,6 +107,8 @@ { "id": "gpt-5.5", "name": "GPT-5.5", + "status": "deprecated", + "replacedBy": "gpt-5.6", "reasoning": true, "input": ["text", "image"], "mediaInput": { @@ -183,81 +119,11 @@ "maxTokens": 128000, "cost": { "input": 5, "output": 30, "cacheRead": 0.5, "cacheWrite": 0 } }, - { - "id": "o1", - "name": "o1", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000, - "maxTokens": 100000, - "cost": { "input": 15, "output": 60, "cacheRead": 7.5, "cacheWrite": 0 } - }, - { - "id": "o1-pro", - "name": "o1-pro", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000, - "maxTokens": 100000, - "cost": { "input": 150, "output": 600, "cacheRead": 0, "cacheWrite": 0 } - }, - { - "id": "o3", - "name": "o3", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000, - "maxTokens": 100000, - "cost": { "input": 2, "output": 8, "cacheRead": 0.5, "cacheWrite": 0 } - }, - { - "id": "o3-deep-research", - "name": "o3-deep-research", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000, - "maxTokens": 100000, - "cost": { "input": 10, "output": 40, "cacheRead": 2.5, "cacheWrite": 0 } - }, - { - "id": "o3-mini", - "name": "o3-mini", - "reasoning": true, - "input": ["text"], - "contextWindow": 200000, - "maxTokens": 100000, - "cost": { "input": 1.1, "output": 4.4, "cacheRead": 0.55, "cacheWrite": 0 } - }, - { - "id": "o3-pro", - "name": "o3-pro", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000, - "maxTokens": 100000, - "cost": { "input": 20, "output": 80, "cacheRead": 0, "cacheWrite": 0 } - }, - { - "id": "o4-mini", - "name": "o4-mini", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000, - "maxTokens": 100000, - "cost": { "input": 1.1, "output": 4.4, "cacheRead": 0.28, "cacheWrite": 0 } - }, - { - "id": "o4-mini-deep-research", - "name": "o4-mini-deep-research", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 200000, - "maxTokens": 100000, - "cost": { "input": 2, "output": 8, "cacheRead": 0.5, "cacheWrite": 0 } - }, { "id": "gpt-5.5-pro", "name": "gpt-5.5-pro", + "status": "deprecated", + "replacedBy": "gpt-5.6", "reasoning": true, "input": ["text", "image"], "mediaInput": { diff --git a/extensions/zai/model-definitions.test.ts b/extensions/zai/model-definitions.test.ts index 11d192441e8e..454489105578 100644 --- a/extensions/zai/model-definitions.test.ts +++ b/extensions/zai/model-definitions.test.ts @@ -53,77 +53,4 @@ describe("zai model definitions", () => { cost: { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 }, }); }); - - it("uses current OpenClaw metadata for the new GLM-5V Turbo model", () => { - expectZaiModelFields({ - id: "glm-5v-turbo", - reasoning: true, - input: ["text", "image"], - contextWindow: 202800, - maxTokens: 131100, - cost: { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 }, - }); - }); - - it("uses current OpenClaw metadata for the GLM-5 model", () => { - expectZaiModelFields({ - id: "glm-5", - reasoning: true, - input: ["text"], - contextWindow: 202800, - maxTokens: 131100, - cost: { input: 1, output: 3.2, cacheRead: 0.2, cacheWrite: 0 }, - }); - }); - - it("publishes newer GLM 4.5/4.6 family metadata from OpenClaw", () => { - expectZaiModelFields({ - id: "glm-4.6v", - input: ["text", "image"], - contextWindow: 128000, - maxTokens: 32768, - cost: { input: 0.3, output: 0.9, cacheRead: 0.05, cacheWrite: 0 }, - }); - expectZaiModelFields({ - id: "glm-4.5-air", - input: ["text"], - contextWindow: 131072, - maxTokens: 98304, - cost: { input: 0.2, output: 1.1, cacheRead: 0.03, cacheWrite: 0 }, - }); - expectZaiModelFields({ - id: "glm-4.5v", - input: ["text", "image"], - contextWindow: 64000, - maxTokens: 16384, - cost: { input: 0.6, output: 1.8, cacheRead: 0.11, cacheWrite: 0 }, - }); - }); - - it("keeps the remaining GLM 4.7/5 pricing and token limits aligned with OpenClaw", () => { - expectZaiModelFields({ - id: "glm-4.7", - cost: { input: 0.6, output: 2.2, cacheRead: 0.11, cacheWrite: 0 }, - contextWindow: 204800, - maxTokens: 131072, - }); - expectZaiModelFields({ - id: "glm-4.7-flash", - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200000, - maxTokens: 131072, - }); - expectZaiModelFields({ - id: "glm-4.7-flashx", - cost: { input: 0.07, output: 0.4, cacheRead: 0.01, cacheWrite: 0 }, - contextWindow: 200000, - maxTokens: 128000, - }); - expectZaiModelFields({ - id: "glm-5-turbo", - contextWindow: 202800, - maxTokens: 131100, - cost: { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 }, - }); - }); }); diff --git a/extensions/zai/onboard.test.ts b/extensions/zai/onboard.test.ts index 1662b752c74d..d11fd7943f53 100644 --- a/extensions/zai/onboard.test.ts +++ b/extensions/zai/onboard.test.ts @@ -28,22 +28,7 @@ describe("zai onboard", () => { expect(defaultCfg.models?.providers?.zai?.baseUrl).toBe(ZAI_GLOBAL_BASE_URL); expect(defaultCfg.models?.providers?.zai?.api).toBe("openai-completions"); const ids = defaultCfg.models?.providers?.zai?.models?.map((m) => m.id); - expect(ids).toEqual([ - "glm-5.2", - "glm-5.1", - "glm-5", - "glm-5-turbo", - "glm-5v-turbo", - "glm-4.7", - "glm-4.7-flash", - "glm-4.7-flashx", - "glm-4.6", - "glm-4.6v", - "glm-4.5", - "glm-4.5-air", - "glm-4.5-flash", - "glm-4.5v", - ]); + expect(ids).toEqual(["glm-5.2", "glm-5.1"]); expect( defaultCfg.models?.providers?.zai?.models?.find((model) => model.id === "glm-5.2"), ).toMatchObject({ diff --git a/extensions/zai/openclaw.plugin.json b/extensions/zai/openclaw.plugin.json index bc4484aeb9a3..707680875445 100644 --- a/extensions/zai/openclaw.plugin.json +++ b/extensions/zai/openclaw.plugin.json @@ -50,6 +50,8 @@ { "id": "glm-5.1", "name": "GLM-5.1", + "status": "deprecated", + "replacedBy": "glm-5.2", "reasoning": true, "input": ["text"], "contextWindow": 202800, @@ -60,174 +62,6 @@ "cacheRead": 0.26, "cacheWrite": 0 } - }, - { - "id": "glm-5", - "name": "GLM-5", - "reasoning": true, - "input": ["text"], - "contextWindow": 202800, - "maxTokens": 131100, - "cost": { - "input": 1, - "output": 3.2, - "cacheRead": 0.2, - "cacheWrite": 0 - } - }, - { - "id": "glm-5-turbo", - "name": "GLM-5 Turbo", - "reasoning": true, - "input": ["text"], - "contextWindow": 202800, - "maxTokens": 131100, - "cost": { - "input": 1.2, - "output": 4, - "cacheRead": 0.24, - "cacheWrite": 0 - } - }, - { - "id": "glm-5v-turbo", - "name": "GLM-5V Turbo", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 202800, - "maxTokens": 131100, - "cost": { - "input": 1.2, - "output": 4, - "cacheRead": 0.24, - "cacheWrite": 0 - } - }, - { - "id": "glm-4.7", - "name": "GLM-4.7", - "reasoning": true, - "input": ["text"], - "contextWindow": 204800, - "maxTokens": 131072, - "cost": { - "input": 0.6, - "output": 2.2, - "cacheRead": 0.11, - "cacheWrite": 0 - } - }, - { - "id": "glm-4.7-flash", - "name": "GLM-4.7 Flash", - "reasoning": true, - "input": ["text"], - "contextWindow": 200000, - "maxTokens": 131072, - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - } - }, - { - "id": "glm-4.7-flashx", - "name": "GLM-4.7 FlashX", - "reasoning": true, - "input": ["text"], - "contextWindow": 200000, - "maxTokens": 128000, - "cost": { - "input": 0.07, - "output": 0.4, - "cacheRead": 0.01, - "cacheWrite": 0 - } - }, - { - "id": "glm-4.6", - "name": "GLM-4.6", - "reasoning": true, - "input": ["text"], - "contextWindow": 204800, - "maxTokens": 131072, - "cost": { - "input": 0.6, - "output": 2.2, - "cacheRead": 0.11, - "cacheWrite": 0 - } - }, - { - "id": "glm-4.6v", - "name": "GLM-4.6V", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 128000, - "maxTokens": 32768, - "cost": { - "input": 0.3, - "output": 0.9, - "cacheRead": 0.05, - "cacheWrite": 0 - } - }, - { - "id": "glm-4.5", - "name": "GLM-4.5", - "reasoning": true, - "input": ["text"], - "contextWindow": 131072, - "maxTokens": 98304, - "cost": { - "input": 0.6, - "output": 2.2, - "cacheRead": 0.11, - "cacheWrite": 0 - } - }, - { - "id": "glm-4.5-air", - "name": "GLM-4.5 Air", - "reasoning": true, - "input": ["text"], - "contextWindow": 131072, - "maxTokens": 98304, - "cost": { - "input": 0.2, - "output": 1.1, - "cacheRead": 0.03, - "cacheWrite": 0 - } - }, - { - "id": "glm-4.5-flash", - "name": "GLM-4.5 Flash", - "reasoning": true, - "input": ["text"], - "contextWindow": 131072, - "maxTokens": 98304, - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - } - }, - { - "id": "glm-4.5v", - "name": "GLM-4.5V", - "reasoning": true, - "input": ["text", "image"], - "contextWindow": 64000, - "maxTokens": 16384, - "cost": { - "input": 0.6, - "output": 1.8, - "cacheRead": 0.11, - "cacheWrite": 0 - } } ] } diff --git a/src/agents/model-catalog-route.ts b/src/agents/model-catalog-route.ts index 476056aa57bc..ce041a65a4de 100644 --- a/src/agents/model-catalog-route.ts +++ b/src/agents/model-catalog-route.ts @@ -86,12 +86,21 @@ function sameLogicalModel( return policy.resolveIdentity(a)?.key === identity.key; } -function logicalIdentity(entry: ModelCatalogEntry, id: string, name?: string): ModelCatalogEntry { +function logicalIdentity( + entry: ModelCatalogEntry, + id: string, + name?: string, + lifecycleEntry: ModelCatalogEntry = entry, +): ModelCatalogEntry { return { id, name: name ?? id, provider: entry.provider, ...(entry.alias ? { alias: entry.alias } : {}), + ...(lifecycleEntry.status ? { status: lifecycleEntry.status } : {}), + ...(lifecycleEntry.statusReason ? { statusReason: lifecycleEntry.statusReason } : {}), + ...(lifecycleEntry.replaces ? { replaces: lifecycleEntry.replaces } : {}), + ...(lifecycleEntry.replacedBy ? { replacedBy: lifecycleEntry.replacedBy } : {}), }; } @@ -158,7 +167,12 @@ export function projectModelCatalogEntryForRoute(params: { policy, catalog: params.catalog, }); - const projected = logicalIdentity(params.entry, identity.id, donor?.name ?? params.entry.name); + const projected = logicalIdentity( + params.entry, + identity.id, + donor?.name ?? params.entry.name, + donor ?? params.entry, + ); return applyLogicalOverrides( { ...projected, diff --git a/src/agents/model-catalog-visibility.test.ts b/src/agents/model-catalog-visibility.test.ts index f76883df0979..7bea7c4d5af7 100644 --- a/src/agents/model-catalog-visibility.test.ts +++ b/src/agents/model-catalog-visibility.test.ts @@ -39,6 +39,81 @@ describe("resolveLogicalVisibleModelCatalog", () => { input: ["text"], }; + const evaluateAvailableEntry = async (entry: ModelCatalogEntry) => + resolveLogicalModelCatalogEntryState({ + entry, + evaluation: { availability: true, routeResolution: null }, + routePolicy: openAIModelCatalogRoutePolicy, + }); + + it.each(["default", "configured"] as const)( + "hides deprecated and disabled rows from the %s picker view", + async (view) => { + const catalog: ModelCatalogEntry[] = [ + { provider: "demo", id: "current", name: "Current", status: "available" }, + { provider: "demo", id: "old", name: "Old", status: "deprecated" }, + { provider: "demo", id: "off", name: "Off", status: "disabled" }, + ]; + + const result = await resolveLogicalVisibleModelCatalog({ + cfg: {} as OpenClawConfig, + catalog, + defaultProvider: "demo", + view, + routePolicy: openAIModelCatalogRoutePolicy, + evaluateEntry: evaluateAvailableEntry, + }); + + expect(result.map((entry) => entry.id)).toEqual(["current"]); + }, + ); + + it("keeps deprecated and disabled rows in the all inventory", async () => { + const catalog: ModelCatalogEntry[] = [ + { provider: "demo", id: "old", name: "Old", status: "deprecated" }, + { provider: "demo", id: "off", name: "Off", status: "disabled" }, + ]; + + const result = await resolveLogicalVisibleModelCatalog({ + cfg: {} as OpenClawConfig, + catalog, + defaultProvider: "demo", + view: "all", + routePolicy: openAIModelCatalogRoutePolicy, + evaluateEntry: evaluateAvailableEntry, + }); + + expect(result.map((entry) => entry.id)).toEqual(["off", "old"]); + }); + + it("keeps deprecated configured primary and alias-key rows visible", async () => { + const catalog: ModelCatalogEntry[] = [ + { provider: "demo", id: "primary", name: "Primary", status: "deprecated" }, + { provider: "demo", id: "alias-key", name: "Alias Key", status: "deprecated" }, + { provider: "demo", id: "hidden", name: "Hidden", status: "deprecated" }, + ]; + const cfg = { + agents: { + defaults: { + model: { primary: "demo/primary" }, + models: { "demo/alias-key": { alias: "legacy" } }, + }, + }, + } as OpenClawConfig; + + const result = await resolveLogicalVisibleModelCatalog({ + cfg, + catalog, + defaultProvider: "demo", + defaultModel: "primary", + view: "configured", + routePolicy: openAIModelCatalogRoutePolicy, + evaluateEntry: evaluateAvailableEntry, + }); + + expect(result.map((entry) => entry.id)).toEqual(["alias-key", "primary"]); + }); + it("dedupes physical routes after selected-route projection", async () => { const catalog = [platform, chatGPT]; const result = await resolveLogicalVisibleModelCatalog({ @@ -73,6 +148,34 @@ describe("resolveLogicalVisibleModelCatalog", () => { ]); }); + it.each([ + ["deprecated", []], + ["available", ["gpt-5.5"]], + ] as const)("uses the selected route's %s lifecycle status", async (status, expectedIds) => { + const platformAvailable = { ...platform, status: "available" as const }; + const chatGPTSelected = { ...chatGPT, status }; + const catalog = [platformAvailable, chatGPTSelected]; + const result = await resolveLogicalVisibleModelCatalog({ + cfg: {} as OpenClawConfig, + catalog, + routeVariants: catalog, + defaultProvider: "openai", + routePolicy: openAIModelCatalogRoutePolicy, + evaluateEntry: async (entry) => + resolveLogicalModelCatalogEntryState({ + entry, + evaluation: { + availability: true, + routeResolution: { kind: "routes", routes: [selectedRoute] }, + selectedRoute, + }, + routePolicy: openAIModelCatalogRoutePolicy, + }), + }); + + expect(result.map((entry) => entry.id)).toEqual(expectedIds); + }); + it("omits physical capabilities while managed route selection is unresolved", async () => { const result = await resolveLogicalVisibleModelCatalog({ cfg: {} as OpenClawConfig, diff --git a/src/agents/model-catalog-visibility.ts b/src/agents/model-catalog-visibility.ts index d264053ed182..45a26b8847b0 100644 --- a/src/agents/model-catalog-visibility.ts +++ b/src/agents/model-catalog-visibility.ts @@ -104,6 +104,19 @@ function dedupeLogicalModelCatalogEntries( }); } +function isPickerVisibleCatalogEntry( + entry: ModelCatalogEntry, + configuredKeys: ReadonlySet, + routePolicy: ModelCatalogRoutePolicy, +): boolean { + // Deprecated and disabled rows stay selectable but are picker-hidden. + // Exact configured refs always remain visible so pinned models never disappear. + return ( + (entry.status !== "deprecated" && entry.status !== "disabled") || + configuredKeys.has(resolveLogicalKey(entry, routePolicy)) + ); +} + /** * Resolve catalog entries visible for one view, honoring explicit visibility * policy, configured models, and providers with usable auth. @@ -332,5 +345,8 @@ export async function resolveLogicalVisibleModelCatalog(params: { } // Physical route rows can share one logical provider/id. Selected-route rows // must lead this merge so dedupe cannot retain sibling-route metadata instead. - return await projectEntries([...preferred, ...kept, ...retained, ...routeBacked]); + const projected = await projectEntries([...preferred, ...kept, ...retained, ...routeBacked]); + return projected.filter((entry) => + isPickerVisibleCatalogEntry(entry, configuredKeys, params.routePolicy), + ); } diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index e59704bf6387..949a3196a620 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -215,6 +215,10 @@ function overlayCatalogMetadata( ...(overlay.input !== undefined ? { input: overlay.input } : {}), ...(params ? { params } : {}), ...(overlay.mediaInput !== undefined ? { mediaInput: overlay.mediaInput } : {}), + ...(overlay.status !== undefined ? { status: overlay.status } : {}), + ...(overlay.statusReason !== undefined ? { statusReason: overlay.statusReason } : {}), + ...(overlay.replaces !== undefined ? { replaces: overlay.replaces } : {}), + ...(overlay.replacedBy !== undefined ? { replacedBy: overlay.replacedBy } : {}), compat: options?.preserveBaseCompat ? resolveCatalogOwnedModelCompat({ catalogRoute: options.catalogCompatRoute ?? base, @@ -371,6 +375,7 @@ export function loadManifestModelCatalog(params: { name: row.name, provider: row.provider, api: row.api, + status: row.status, }; if (row.baseUrl) { entry.baseUrl = row.baseUrl; @@ -391,6 +396,15 @@ export function loadManifestModelCatalog(params: { if (row.compat) { entry.compat = row.compat; } + if (row.statusReason) { + entry.statusReason = row.statusReason; + } + if (row.replaces?.length) { + entry.replaces = [...row.replaces]; + } + if (row.replacedBy) { + entry.replacedBy = row.replacedBy; + } return entry; }); manifestModelCatalogCache.set(params.config, { snapshot: resolvedSnapshot, rows }); diff --git a/src/agents/model-catalog.types.ts b/src/agents/model-catalog.types.ts index a98e20e0c8d6..207af613578c 100644 --- a/src/agents/model-catalog.types.ts +++ b/src/agents/model-catalog.types.ts @@ -3,6 +3,7 @@ * Used by discovery, browsing, visibility, and provider-auth code so renderers * and filters agree on stable model metadata. */ +import type { ModelCatalogStatus } from "@openclaw/model-catalog-core/model-catalog-types"; import type { ModelApi, ModelCompatConfig, ModelMediaInputConfig } from "../config/types.models.js"; /** Input modalities a catalog entry can advertise. */ @@ -24,6 +25,10 @@ export type ModelCatalogEntry = { params?: Record; compat?: ModelCompatConfig; mediaInput?: ModelMediaInputConfig; + status?: ModelCatalogStatus; + statusReason?: string; + replaces?: string[]; + replacedBy?: string; }; /** Logical catalog rows plus the physical variants used for route selection. */ diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index 100364788aca..2c07251407e5 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -1603,6 +1603,28 @@ describe("model-selection", () => { }); describe("resolveAllowedModelRef", () => { + it("keeps deprecated catalog refs selectable", () => { + const result = resolveAllowedModelRef({ + cfg: {} as OpenClawConfig, + catalog: [ + { + provider: "openai", + id: "gpt-5.5", + name: "GPT-5.5", + status: "deprecated", + replacedBy: "gpt-5.6", + }, + ], + raw: "openai/gpt-5.5", + defaultProvider: "openai", + }); + + expect(result).toEqual({ + key: "openai/gpt-5.5", + ref: { provider: "openai", model: "gpt-5.5" }, + }); + }); + it("accepts explicit allowlist refs absent from bundled catalog", () => { const result = resolveAllowedModelRef({ cfg: EXPLICIT_ALLOWLIST_CONFIG, diff --git a/src/agents/model-visibility-policy.test.ts b/src/agents/model-visibility-policy.test.ts index a904d43ec15b..1cb291b5ccea 100644 --- a/src/agents/model-visibility-policy.test.ts +++ b/src/agents/model-visibility-policy.test.ts @@ -29,6 +29,56 @@ function createPolicy(cfg: OpenClawConfig, agentId?: string) { } describe("explicit model visibility policy", () => { + it("tracks every exact configured picker ref independently of the allow policy", () => { + const cfg = { + agents: { + defaults: { + model: { + primary: "demo/default-primary", + fallbacks: ["demo/default-fallback"], + }, + models: { "demo/default-alias": { alias: "default" } }, + utilityModel: "demo/default-utility", + imageModel: { + primary: "demo/image", + fallbacks: ["demo/image-fallback"], + }, + pdfModel: { primary: "demo/pdf", fallbacks: ["demo/pdf-fallback"] }, + modelPolicy: { allow: [] }, + }, + list: [ + { + id: "research", + model: { primary: "demo/primary", fallbacks: ["demo/fallback"] }, + models: { "demo/agent-alias": { alias: "agent" } }, + utilityModel: "demo/agent-utility", + }, + ], + }, + } as OpenClawConfig; + const policy = createModelVisibilityPolicy({ + cfg, + catalog: [], + defaultProvider: "openai", + agentId: "research", + }); + + const configuredRefs = [ + "demo/default-alias", + "demo/default-primary", + "demo/default-fallback", + "demo/agent-alias", + "demo/agent-utility", + "demo/image", + "demo/image-fallback", + "demo/pdf", + "demo/pdf-fallback", + "demo/primary", + "demo/fallback", + ]; + expect(configuredRefs.filter((key) => !policy.configuredKeys.has(key))).toEqual([]); + }); + it("keeps overrides open when model entries only configure aliases or params", () => { const policy = createPolicy({ meta: { migrations: { modelPolicyAllowlist: true } }, diff --git a/src/agents/model-visibility-policy.ts b/src/agents/model-visibility-policy.ts index 485f5a297b4a..59656877978b 100644 --- a/src/agents/model-visibility-policy.ts +++ b/src/agents/model-visibility-policy.ts @@ -1,7 +1,10 @@ /** * Builds model visibility policies while retaining configured automatic fallbacks. */ -import { resolveAgentModelFallbackValues } from "../config/model-input.js"; +import { + resolveAgentModelFallbackValues, + resolveAgentModelPrimaryValue, +} from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveAgentConfig, resolveAgentModelFallbacksOverride } from "./agent-scope.js"; import type { ModelCatalogEntry } from "./model-catalog.types.js"; @@ -26,6 +29,27 @@ function resolveAllowedFallbacks(params: { cfg: OpenClawConfig; agentId?: string return resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model); } +function resolveAdditionalConfiguredModelRefs(params: { + cfg: OpenClawConfig; + agentId?: string; +}): string[] { + const defaults = params.cfg.agents?.defaults; + const agent = params.agentId ? resolveAgentConfig(params.cfg, params.agentId) : undefined; + return [ + resolveAgentModelPrimaryValue(defaults?.model), + ...resolveAgentModelFallbackValues(defaults?.model), + resolveAgentModelPrimaryValue(agent?.model), + ...resolveAgentModelFallbackValues(agent?.model), + ...Object.keys(defaults?.models ?? {}), + ...Object.keys(agent?.models ?? {}), + agent?.utilityModel ?? defaults?.utilityModel, + resolveAgentModelPrimaryValue(defaults?.imageModel), + ...resolveAgentModelFallbackValues(defaults?.imageModel), + resolveAgentModelPrimaryValue(defaults?.pdfModel), + ...resolveAgentModelFallbackValues(defaults?.pdfModel), + ].filter((ref): ref is string => typeof ref === "string"); +} + export function createModelVisibilityPolicy( params: { cfg: OpenClawConfig; @@ -47,9 +71,7 @@ export function createModelVisibilityPolicy( cfg: params.cfg, agentId: params.agentId, }), - additionalConfiguredModelRefs: params.agentId - ? Object.keys(resolveAgentConfig(params.cfg, params.agentId)?.models ?? {}) - : [], + additionalConfiguredModelRefs: resolveAdditionalConfiguredModelRefs(params), // Model visibility is used by lightweight status/list paths. Keep plugin // manifest normalization opt-in so those paths do not load plugin runtime // metadata unless a caller explicitly needs it. diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index ed1830ce5a71..a3deaa1d32ce 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -3163,8 +3163,8 @@ describe("image compression policy", () => { testing.resolveImageCompressionPolicy({ cfg: {}, imageModelConfig: { - primary: "anthropic/claude-opus-4-7", - fallbacks: ["anthropic/claude-sonnet-4-6"], + primary: "anthropic/claude-opus-4-8", + fallbacks: ["anthropic/claude-haiku-4-5"], }, imageCount: 1, }), diff --git a/src/gateway/server-methods/models.test.ts b/src/gateway/server-methods/models.test.ts index 864705513009..0ef49027de66 100644 --- a/src/gateway/server-methods/models.test.ts +++ b/src/gateway/server-methods/models.test.ts @@ -259,7 +259,19 @@ describe("models.list", () => { }, }, } as unknown as OpenClawConfig; - const loadGatewayModelCatalog = vi.fn(() => Promise.resolve([])); + const loadGatewayModelCatalog = vi.fn(() => + Promise.resolve([ + { + id: "source-model", + name: "Source Model", + provider: "vllm", + status: "disabled", + contextWindow: 128_000, + reasoning: true, + input: ["text", "image"], + }, + ]), + ); setRuntimeConfigSnapshot(runtimeConfig, sourceConfig); try { const { request, respond } = requestModelsList({ diff --git a/src/gateway/sessions-patch.test.ts b/src/gateway/sessions-patch.test.ts index 5618259234ca..4b0df27a1954 100644 --- a/src/gateway/sessions-patch.test.ts +++ b/src/gateway/sessions-patch.test.ts @@ -1015,6 +1015,27 @@ describe("gateway sessions patch", () => { expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID); }); + test("supports uncataloged configured primary and session override refs", async () => { + const primary = "openai/o3"; + const override = "openai/o1"; + const entry = expectPatchOk( + await runPatch({ + cfg: { + agents: { + defaults: { + model: { primary }, + modelPolicy: { allow: [] }, + }, + }, + } as OpenClawConfig, + patch: { key: MAIN_SESSION_KEY, model: override }, + loadGatewayModelCatalog: async () => [], + }), + ); + + expectModelSelection(entry, "openai", "o1"); + }); + test("persists provider-qualified aliases without cross-provider collisions", async () => { const entry = expectPatchOk( await runPatch({