mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 07:31:33 +00:00
feat(models): curate default model picker to current-generation catalogs (#113594)
* feat(models): curate default picker visibility * chore(models): curate provider catalogs * test(openai): generalize static-catalog transport guard after row curation (#91710) * fix(models): propagate catalog lifecycle metadata * test(models): keep picker suite within line budget
This commit is contained in:
committed by
GitHub
parent
1d43d602c5
commit
91b5b43d78
@@ -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).
|
||||
|
||||
@@ -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": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 |
|
||||
|
||||
<Warning>
|
||||
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.
|
||||
</Warning>
|
||||
|
||||
@@ -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
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
<Tip>
|
||||
GLM models are available as `zai/<model>` (example: `zai/glm-5`).
|
||||
GLM models are available as `zai/<model>` (example: `zai/glm-5.2`).
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ type AnthropicCatalogModel = {
|
||||
cacheWrite?: number;
|
||||
};
|
||||
thinkingLevelMap?: Record<string, string | null>;
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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<string, unknown>
|
||||
>;
|
||||
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 () => {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -104,6 +104,19 @@ function dedupeLogicalModelCatalogEntries(
|
||||
});
|
||||
}
|
||||
|
||||
function isPickerVisibleCatalogEntry(
|
||||
entry: ModelCatalogEntry,
|
||||
configuredKeys: ReadonlySet<string>,
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
compat?: ModelCompatConfig;
|
||||
mediaInput?: ModelMediaInputConfig;
|
||||
status?: ModelCatalogStatus;
|
||||
statusReason?: string;
|
||||
replaces?: string[];
|
||||
replacedBy?: string;
|
||||
};
|
||||
|
||||
/** Logical catalog rows plus the physical variants used for route selection. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user