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({