From 15e32c7341ed5c8d4abe0c3eca38dc9f5165f56d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Feb 2026 05:20:48 +0100 Subject: [PATCH] fix(models): refresh Moonshot Kimi vision capabilities Co-authored-by: manikv12 --- CHANGELOG.md | 1 + ...ssing-provider-apikey-from-env-var.test.ts | 64 +++++++++++++++++++ src/agents/models-config.providers.ts | 2 +- src/agents/models-config.ts | 49 +++++++++----- src/agents/synthetic-models.ts | 2 +- src/commands/auth-choice.moonshot.test.ts | 2 + src/commands/onboard-auth.models.ts | 2 +- 7 files changed, 104 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae043cff7b..d4e4043109f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai - Security/CLI: redact sensitive values in `openclaw config get` output before printing config paths, preventing credential leakage to terminal output/history. (#13683) Thanks @SleuthCo. - Agents/Moonshot: force `supportsDeveloperRole=false` for Moonshot-compatible `openai-completions` models (provider `moonshot` and Moonshot base URLs), so initial runs no longer send unsupported `developer` roles that trigger `ROLE_UNSPECIFIED` errors. (#21060, #22194) Thanks @ShengFuC. - Agents/Kimi: classify Moonshot `Your request exceeded model token limit` failures as context overflows so auto-compaction and user-facing overflow recovery trigger correctly instead of surfacing raw invalid-request errors. (#9562) Thanks @danilofalcao. +- Providers/Moonshot: mark Kimi K2.5 as image-capable in implicit + onboarding model definitions, and refresh stale explicit provider capability fields (`input`/`reasoning`/context limits) from implicit catalogs so existing configs pick up Moonshot vision support without manual model rewrites. (#13135, #4459) Thanks @manikv12. - Install/Discord Voice: make `@discordjs/opus` an optional dependency so `openclaw` install/update no longer hard-fails when native Opus builds fail, while keeping `opusscript` as the runtime fallback decoder for Discord voice flows. (#23737, #23733, #23703) Thanks @jeadland, @Sheetaa, and @Breakyman. - Docker/Setup: precreate `$OPENCLAW_CONFIG_DIR/identity` during `docker-setup.sh` so CLI commands that need device identity (for example `devices list`) avoid `EACCES ... /home/node/.openclaw/identity` failures on restrictive bind mounts. (#23948) Thanks @ackson-beep. - Exec/Background: stop applying the default exec timeout to background sessions (`background: true` or explicit `yieldMs`) when no explicit timeout is set, so long-running background jobs are no longer terminated at the default timeout boundary. (#23303) diff --git a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts index 3f80eec7b54..c26142158e8 100644 --- a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts +++ b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts @@ -133,4 +133,68 @@ describe("models-config", () => { expect(parsed.providers["custom-proxy"]?.baseUrl).toBe("http://localhost:4000/v1"); }); }); + + it("refreshes stale explicit moonshot model capabilities from implicit catalog", async () => { + await withTempHome(async () => { + const prevKey = process.env.MOONSHOT_API_KEY; + process.env.MOONSHOT_API_KEY = "sk-moonshot-test"; + try { + const cfg: OpenClawConfig = { + models: { + providers: { + moonshot: { + baseUrl: "https://api.moonshot.ai/v1", + api: "openai-completions", + models: [ + { + id: "kimi-k2.5", + name: "Kimi K2.5", + reasoning: false, + input: ["text"], + cost: { input: 123, output: 456, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1024, + maxTokens: 256, + }, + ], + }, + }, + }, + }; + + await ensureOpenClawModelsJson(cfg); + + const modelPath = path.join(resolveOpenClawAgentDir(), "models.json"); + const raw = await fs.readFile(modelPath, "utf8"); + const parsed = JSON.parse(raw) as { + providers: Record< + string, + { + models?: Array<{ + id: string; + input?: string[]; + reasoning?: boolean; + contextWindow?: number; + maxTokens?: number; + cost?: { input?: number; output?: number }; + }>; + } + >; + }; + const kimi = parsed.providers.moonshot?.models?.find((model) => model.id === "kimi-k2.5"); + expect(kimi?.input).toEqual(["text", "image"]); + expect(kimi?.reasoning).toBe(false); + expect(kimi?.contextWindow).toBe(256000); + expect(kimi?.maxTokens).toBe(8192); + // Preserve explicit user pricing overrides when refreshing capabilities. + expect(kimi?.cost?.input).toBe(123); + expect(kimi?.cost?.output).toBe(456); + } finally { + if (prevKey === undefined) { + delete process.env.MOONSHOT_API_KEY; + } else { + process.env.MOONSHOT_API_KEY = prevKey; + } + } + }); + }); }); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index b1c55b8c353..30e0326e609 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -513,7 +513,7 @@ function buildMoonshotProvider(): ProviderConfig { id: MOONSHOT_DEFAULT_MODEL_ID, name: "Kimi K2.5", reasoning: false, - input: ["text"], + input: ["text", "image"], cost: MOONSHOT_DEFAULT_COST, contextWindow: MOONSHOT_DEFAULT_CONTEXT_WINDOW, maxTokens: MOONSHOT_DEFAULT_MAX_TOKENS, diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index b44c0d60b60..5ca971646e1 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -29,22 +29,41 @@ function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig) const id = (model as { id?: unknown }).id; return typeof id === "string" ? id.trim() : ""; }; - const seen = new Set(explicitModels.map(getId).filter(Boolean)); + const implicitById = new Map( + implicitModels.map((model) => [getId(model), model] as const).filter(([id]) => Boolean(id)), + ); + const seen = new Set(); - const mergedModels = [ - ...explicitModels, - ...implicitModels.filter((model) => { - const id = getId(model); - if (!id) { - return false; - } - if (seen.has(id)) { - return false; - } - seen.add(id); - return true; - }), - ]; + const mergedModels = explicitModels.map((explicitModel) => { + const id = getId(explicitModel); + if (!id) { + return explicitModel; + } + seen.add(id); + const implicitModel = implicitById.get(id); + if (!implicitModel) { + return explicitModel; + } + + // Refresh capability metadata from the implicit catalog while preserving + // user-specific fields (cost, headers, compat, etc.) on explicit entries. + return { + ...explicitModel, + input: implicitModel.input, + reasoning: implicitModel.reasoning, + contextWindow: implicitModel.contextWindow, + maxTokens: implicitModel.maxTokens, + }; + }); + + for (const implicitModel of implicitModels) { + const id = getId(implicitModel); + if (!id || seen.has(id)) { + continue; + } + seen.add(id); + mergedModels.push(implicitModel); + } return { ...implicit, diff --git a/src/agents/synthetic-models.ts b/src/agents/synthetic-models.ts index 5d820c8474b..78a0226921a 100644 --- a/src/agents/synthetic-models.ts +++ b/src/agents/synthetic-models.ts @@ -103,7 +103,7 @@ export const SYNTHETIC_MODEL_CATALOG = [ id: "hf:moonshotai/Kimi-K2.5", name: "Kimi K2.5", reasoning: true, - input: ["text"], + input: ["text", "image"], contextWindow: 256000, maxTokens: 8192, }, diff --git a/src/commands/auth-choice.moonshot.test.ts b/src/commands/auth-choice.moonshot.test.ts index 780c0e8e71b..6c9bcb45068 100644 --- a/src/commands/auth-choice.moonshot.test.ts +++ b/src/commands/auth-choice.moonshot.test.ts @@ -77,6 +77,7 @@ describe("applyAuthChoice (moonshot)", () => { "anthropic/claude-opus-4-5", ); expect(result.config.models?.providers?.moonshot?.baseUrl).toBe("https://api.moonshot.cn/v1"); + expect(result.config.models?.providers?.moonshot?.models?.[0]?.input).toContain("image"); expect(result.agentModelOverride).toBe("moonshot/kimi-k2.5"); const parsed = await readAuthProfiles(); @@ -95,6 +96,7 @@ describe("applyAuthChoice (moonshot)", () => { "moonshot/kimi-k2.5", ); expect(result.config.models?.providers?.moonshot?.baseUrl).toBe("https://api.moonshot.cn/v1"); + expect(result.config.models?.providers?.moonshot?.models?.[0]?.input).toContain("image"); expect(result.agentModelOverride).toBeUndefined(); const parsed = await readAuthProfiles(); diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index fa97cc7b96d..167cde6809d 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -130,7 +130,7 @@ export function buildMoonshotModelDefinition(): ModelDefinitionConfig { id: MOONSHOT_DEFAULT_MODEL_ID, name: "Kimi K2.5", reasoning: false, - input: ["text"], + input: ["text", "image"], cost: MOONSHOT_DEFAULT_COST, contextWindow: MOONSHOT_DEFAULT_CONTEXT_WINDOW, maxTokens: MOONSHOT_DEFAULT_MAX_TOKENS,