From bf7cc278d2a87c9495b91c7c495eef916306618b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 06:24:57 +0100 Subject: [PATCH] fix(models): explain missing provider model registration --- CHANGELOG.md | 1 + docs/concepts/model-providers.md | 2 + src/agents/pi-embedded-runner/model.test.ts | 24 +++++++++++ src/agents/pi-embedded-runner/model.ts | 42 +++++++++++++++++++ .../reply/directive-handling.model.test.ts | 6 +-- 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29f655852e2..11f7fe156e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai - Agents/Anthropic: report 1M session context for Claude Opus/Sonnet 4 models even when local model config still advertises 200k, matching model discovery and preventing premature status/UI overflow. Fixes #66766. - Models/OpenRouter: hide missing-auth direct provider rows in `/model status` when they are only duplicated by a nested OpenRouter model id such as `openrouter/google/...`, while preserving explicitly configured direct providers. Fixes #62317. - Models: preserve an explicitly selected provider/model such as `opencode-go/deepseek-v4-pro` when another provider owns the same bare model alias. Fixes #79325. +- Models/config: explain missing `models.providers..models[]` registration when a model exists only in `agents.defaults.models`, instead of returning a bare unknown-model error. Fixes #80089. - Kimi Code: use Kimi's stable `kimi-for-coding` API model id in bundled catalog, onboarding, and docs while normalizing legacy `kimi-code` and `k2p5` refs. Fixes #79965. - Volcengine/Kimi: strip provider-unsupported tool schema length and item constraint keywords for direct and coding-plan models so hosted Kimi runs do not reject message tools with `minLength`. Fixes #38817. - DeepSeek: backfill V4 `reasoning_content` replay fields for unowned OpenAI-compatible proxy providers, preventing follow-up request failures outside the bundled DeepSeek and OpenRouter routes. Fixes #79608. diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index c79fc16a063..78433208009 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -348,6 +348,8 @@ Many of the bundled provider plugins below already publish a default catalog. Us Gateway model capability checks also read explicit `models.providers..models[]` metadata. If a custom or proxy model accepts images, set `input: ["text", "image"]` on that model so WebChat and node-origin attachment paths pass images as native model inputs instead of text-only media refs. +`agents.defaults.models["provider/model"]` only controls model visibility, aliases, and per-model metadata for agents. It does not register a new runtime model by itself. For custom provider models, also add `models.providers..models[]` with at least the matching `id`. + ### Moonshot AI (Kimi) Moonshot ships as a bundled provider plugin. Use the built-in provider by default, and add an explicit `models.providers.moonshot` entry only when you need to override the base URL or model metadata: diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/pi-embedded-runner/model.test.ts index 0102d34e887..97b650c7ff6 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/pi-embedded-runner/model.test.ts @@ -1146,6 +1146,30 @@ describe("resolveModel", () => { expect(result.model?.input).toEqual(["text"]); }); + it("explains when an agent model entry is missing provider model registration", async () => { + const cfg = { + agents: { + defaults: { + models: { + "microsoft-foundry/Kimi-K2.6-1": { + contextWindow: 262144, + maxOutputTokens: 16384, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + const result = await resolveModelAsync("microsoft-foundry", "Kimi-K2.6-1", "/tmp/agent", cfg, { + runtimeHooks: createRuntimeHooks(), + skipPiDiscovery: true, + }); + + expect(result.error).toBe( + 'Unknown model: microsoft-foundry/Kimi-K2.6-1. Found agents.defaults.models["microsoft-foundry/Kimi-K2.6-1"], but no matching models.providers["microsoft-foundry"].models[] entry. Add { "id": "Kimi-K2.6-1" } to models.providers["microsoft-foundry"].models[] to register this provider model.', + ); + }); + it("repairs stale text-only Foundry fallback rows for GPT-family models", () => { const cfg = { models: { diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index bd029484737..965ee0b4e8b 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -1223,6 +1223,14 @@ function buildUnknownModelError(params: { return suppressed; } const base = `Unknown model: ${params.provider}/${params.modelId}`; + const registrationHint = buildMissingProviderModelRegistrationHint({ + provider: params.provider, + modelId: params.modelId, + cfg: params.cfg, + }); + if (registrationHint) { + return `${base}. ${registrationHint}`; + } const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS; const hint = runtimeHooks.buildProviderUnknownModelHintWithPlugin({ provider: params.provider, @@ -1240,3 +1248,37 @@ function buildUnknownModelError(params: { }); return hint ? `${base}. ${hint}` : base; } + +function buildMissingProviderModelRegistrationHint(params: { + provider: string; + modelId: string; + cfg?: OpenClawConfig; +}): string | undefined { + const configuredModels = params.cfg?.agents?.defaults?.models; + if (!configuredModels) { + return undefined; + } + const agentModelKey = modelKey(params.provider, params.modelId); + if ( + !configuredModels[agentModelKey] && + !configuredModels[`${params.provider}/${params.modelId}`] + ) { + return undefined; + } + const providerConfig = findNormalizedProviderValue( + params.cfg?.models?.providers, + params.provider, + ) as { models?: unknown } | undefined; + const providerModels = Array.isArray(providerConfig?.models) ? providerConfig.models : []; + const hasProviderModel = providerModels.some((entry) => { + if (!entry || typeof entry !== "object" || !("id" in entry)) { + return false; + } + const id = (entry as { id?: unknown }).id; + return typeof id === "string" && id === params.modelId; + }); + if (hasProviderModel) { + return undefined; + } + return `Found agents.defaults.models["${agentModelKey}"], but no matching models.providers["${params.provider}"].models[] entry. Add { "id": "${params.modelId}" } to models.providers["${params.provider}"].models[] to register this provider model.`; +} diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index f6eacb690f3..bf9528ef898 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -515,7 +515,7 @@ describe("/model chat UX", () => { }, }, }, - } as OpenClawConfig, + } as unknown as OpenClawConfig, allowedModelCatalog: [ { provider: "anthropic", id: "claude-opus-4-6", name: "Claude Opus 4.5" }, { provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" }, @@ -546,7 +546,7 @@ describe("/model chat UX", () => { }, }, }, - } as OpenClawConfig, + } as unknown as OpenClawConfig, allowedModelCatalog: [ { provider: "google", id: "gemini-3-flash-preview", name: "Gemini 3 Flash" }, { @@ -584,7 +584,7 @@ describe("/model chat UX", () => { }, }, }, - } as OpenClawConfig, + } as unknown as OpenClawConfig, allowedModelCatalog: [ { provider: "google", id: "gemini-3-flash-preview", name: "Gemini 3 Flash" }, {