diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index 19880371ea73..f4543b5fe6c3 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -1014,7 +1014,7 @@ Suppression fields: | `when.baseUrlHosts` | `string[]` | Optional list of effective provider base URL hosts required before the suppression applies. | | `when.providerConfigApiIn` | `string[]` | Optional list of exact provider-config `api` values required before the suppression applies. | -`upstreamModel` marks a row that serves the same upstream model as a row in another bundled catalog under a different id, for example a subscription endpoint next to the vendor's API endpoint. It is authoring metadata: normalization drops it, and a contract test uses it to keep capability flags such as `compat.codeMode` from drifting between catalogs that ship the same model. Rows that already share the same model id need no marker. See [Code mode](/tools/code-mode#models-shipped-by-more-than-one-provider). +`upstreamModel` marks a row that serves the same upstream model as a row in another bundled catalog under a different name, for example a subscription endpoint next to the vendor's API endpoint. It is authoring metadata: normalization drops it, and a contract test uses it to keep capability flags such as `compat.codeMode` from drifting between catalogs that ship the same model. Most rows need no marker, because matching ignores a leading vendor namespace and casing: `moonshotai/kimi-k3` and `zai-org/GLM-5.2` already match the first-party `kimi-k3` and `glm-5.2` rows. Reach for `upstreamModel` only when the vendor's own names genuinely differ. See [Code mode](/tools/code-mode#models-shipped-by-more-than-one-provider). Do not put runtime-only data in `modelCatalog`. Use `static` only when manifest rows are complete enough for provider-filtered list and picker surfaces to skip registry/runtime discovery. Use `refreshable` when manifest rows are useful listable seeds or supplements but a refresh/cache can add more rows later; refreshable rows are not authoritative by themselves. Use `runtime` when OpenClaw must load provider runtime to know the list. diff --git a/docs/tools/code-mode.md b/docs/tools/code-mode.md index 36dacbc67343..305188d81909 100644 --- a/docs/tools/code-mode.md +++ b/docs/tools/code-mode.md @@ -318,15 +318,21 @@ model. Because `"auto"` resolves the tier from whichever catalog served the run, two catalogs describing the same upstream model must not disagree by accident. Every catalog row for a shared model therefore states its tier explicitly once -any sibling row states one. When the shared model is listed under different ids, -the non-canonical row points at the canonical one with `upstreamModel`, as the -`kimi` catalog does for `moonshot/kimi-k3`. Reseller catalogs such as -`github-copilot`, `opencode`, `opencode-go`, `ollama-cloud`, `qianfan`, -`venice`, and `volcengine-plan` currently declare `"capable"` for the models -first-party catalogs flag `"preferred"`: the preferred tier came from -evaluations on the first-party endpoints, and those runs have not been repeated -per reseller. Promoting one of those rows is a deliberate, evidence-backed -change rather than an oversight. +any sibling row states one. Rows are matched on the vendor's own name for the +weights, so a catalog that republishes a model under a namespaced id or +different casing is matched automatically: `novita/moonshotai/kimi-k3`, +`nvidia/z-ai/glm-5.2`, and `together/deepseek-ai/DeepSeek-V4-Pro` all group with +the first-party rows without anyone declaring anything. Only genuinely different +names need the manifest's `upstreamModel` marker, as the `kimi` catalog uses for +`moonshot/kimi-k3`. + +Reseller and aggregator catalogs such as `baseten`, `deepinfra`, +`github-copilot`, `gmi`, `novita`, `nvidia`, `ollama-cloud`, `opencode`, +`opencode-go`, `qianfan`, `together`, `venice`, and `volcengine-plan` currently +declare `"capable"` for the models first-party catalogs flag `"preferred"`: the +preferred tier came from evaluations on the first-party endpoints, and those +runs have not been repeated per reseller. Promoting one of those rows is a +deliberate, evidence-backed change rather than an oversight. For OpenAI models, the flag matters only when the run resolves to the OpenClaw embedded agent runtime. Default OpenAI routing uses the Codex-style harness diff --git a/extensions/baseten/openclaw.plugin.json b/extensions/baseten/openclaw.plugin.json index 9d1c2b62244d..3bcdf9cca477 100644 --- a/extensions/baseten/openclaw.plugin.json +++ b/extensions/baseten/openclaw.plugin.json @@ -40,6 +40,9 @@ "output": 3.48, "cacheRead": 0.145, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } }, { @@ -72,6 +75,9 @@ "output": 4.4, "cacheRead": 0.14, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } }, { diff --git a/extensions/deepinfra/openclaw.plugin.json b/extensions/deepinfra/openclaw.plugin.json index d035d9a98505..a981f701a9c2 100644 --- a/extensions/deepinfra/openclaw.plugin.json +++ b/extensions/deepinfra/openclaw.plugin.json @@ -48,7 +48,8 @@ "cacheWrite": 0 }, "compat": { - "supportsUsageInStreaming": true + "supportsUsageInStreaming": true, + "codeMode": "capable" } }, { @@ -65,7 +66,8 @@ "cacheWrite": 0 }, "compat": { - "supportsUsageInStreaming": true + "supportsUsageInStreaming": true, + "codeMode": "capable" } }, { @@ -82,7 +84,8 @@ "cacheWrite": 0 }, "compat": { - "supportsUsageInStreaming": true + "supportsUsageInStreaming": true, + "codeMode": "capable" } }, { @@ -205,7 +208,8 @@ "cacheWrite": 0 }, "compat": { - "supportsUsageInStreaming": true + "supportsUsageInStreaming": true, + "codeMode": "capable" } }, { diff --git a/extensions/gmi/openclaw.plugin.json b/extensions/gmi/openclaw.plugin.json index 679b67666cbd..457b4793e4c6 100644 --- a/extensions/gmi/openclaw.plugin.json +++ b/extensions/gmi/openclaw.plugin.json @@ -117,6 +117,9 @@ "output": 1.357, "cacheRead": 0.056, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } }, { @@ -161,6 +164,9 @@ "output": 10, "cacheRead": 0.2, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } }, { @@ -191,6 +197,9 @@ "output": 30, "cacheRead": 0.5, "cacheWrite": 6.25 + }, + "compat": { + "codeMode": "capable" } } ] diff --git a/extensions/novita/openclaw.plugin.json b/extensions/novita/openclaw.plugin.json index ee0fcc0386c3..8198093683ff 100644 --- a/extensions/novita/openclaw.plugin.json +++ b/extensions/novita/openclaw.plugin.json @@ -85,6 +85,9 @@ "output": 15, "cacheRead": 0.3, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } }, { @@ -127,6 +130,9 @@ "output": 4.4, "cacheRead": 0.26, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } }, { @@ -141,6 +147,9 @@ "output": 3.2, "cacheRead": 0.135, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } }, { @@ -155,6 +164,9 @@ "output": 0.28, "cacheRead": 0.028, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } }, { diff --git a/extensions/nvidia/openclaw.plugin.json b/extensions/nvidia/openclaw.plugin.json index 126e9e8c27c8..0b0ca54c0dd8 100644 --- a/extensions/nvidia/openclaw.plugin.json +++ b/extensions/nvidia/openclaw.plugin.json @@ -86,7 +86,8 @@ "cacheWrite": 0 }, "compat": { - "requiresStringContent": true + "requiresStringContent": true, + "codeMode": "capable" } }, { @@ -145,7 +146,8 @@ "cacheWrite": 0 }, "compat": { - "requiresStringContent": true + "requiresStringContent": true, + "codeMode": "capable" } }, { @@ -206,7 +208,8 @@ "cacheWrite": 0 }, "compat": { - "requiresStringContent": true + "requiresStringContent": true, + "codeMode": "capable" }, "status": "deprecated", "statusReason": "Still available by exact reference; use z-ai/glm-5.2 for new NVIDIA setups.", diff --git a/extensions/together/openclaw.plugin.json b/extensions/together/openclaw.plugin.json index 968bb7882af0..5709b9d16eef 100644 --- a/extensions/together/openclaw.plugin.json +++ b/extensions/together/openclaw.plugin.json @@ -85,6 +85,9 @@ "output": 3.48, "cacheRead": 0.2, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } }, { @@ -99,6 +102,9 @@ "output": 4.4, "cacheRead": 0.26, "cacheWrite": 0 + }, + "compat": { + "codeMode": "capable" } } ] diff --git a/src/plugins/contracts/shared-upstream-model.contract.test.ts b/src/plugins/contracts/shared-upstream-model.contract.test.ts index 8d58c57d9bd1..4dfc9de9dd59 100644 --- a/src/plugins/contracts/shared-upstream-model.contract.test.ts +++ b/src/plugins/contracts/shared-upstream-model.contract.test.ts @@ -7,15 +7,17 @@ import { listGitTrackedFiles } from "../../test-utils/repo-files.js"; const repoRoot = path.resolve(import.meta.dirname, "../../.."); const MANIFEST_BASENAME = "openclaw.plugin.json"; const CODE_MODE_TIER_LITERAL = /codeMode:\s*"(?:preferred|capable)"/; -// Catalogs still built in plugin source instead of `modelCatalog` manifest rows. -// The shared-model checks below read manifests, so these two stay invisible to -// them; moving their rows into the manifest closes the gap. +// Catalogs still built in plugin source instead of `modelCatalog` manifest rows, +// so the manifest scan below cannot see their tiers. Moving them is not free: +// `google` rows would newly feed model visibility and pre-discovery thinking +// metadata through `loadManifestModelCatalog`, and `minimax` resolves cost per +// provider surface, so its rows cannot live in one manifest catalog. const UNCONVERTED_SOURCE_CATALOG_PLUGINS = ["google", "minimax"]; type CatalogEntry = { /** `provider/model` ref used in failure output. */ ref: string; - /** Shared-model group: the declared upstream model id, else the row's own id. */ + /** Shared-model group: the normalized upstream model id, else the row's own. */ groupKey: string; /** Declared upstream ref, present only on non-canonical rows. */ upstreamModel?: string; @@ -43,6 +45,18 @@ function readBundledManifests(): Array> { ); } +/** + * Reduces a catalog model id to the vendor's own name for the weights. + * Aggregators republish first-party models under a namespaced id and varying + * case (`moonshotai/kimi-k3`, `zai-org/GLM-5.2`), so dropping one leading + * namespace segment groups those with the first-party row automatically instead + * of waiting for someone to declare `upstreamModel` on each one. + */ +function normalizeSharedModelId(modelId: string): string { + const separator = modelId.indexOf("/"); + return (separator === -1 ? modelId : modelId.slice(separator + 1)).toLowerCase(); +} + function readRecord(value: unknown): Record | undefined { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) @@ -76,7 +90,9 @@ function collectCatalogEntries(): CatalogEntry[] { const codeMode = readRecord(model?.compat)?.codeMode; entries.push({ ref: `${providerId}/${id}`, - groupKey: upstreamModel ? upstreamModel.slice(upstreamModel.indexOf("/") + 1) : id, + groupKey: normalizeSharedModelId( + upstreamModel ? upstreamModel.slice(upstreamModel.indexOf("/") + 1) : id, + ), ...(upstreamModel ? { upstreamModel } : {}), ...(typeof codeMode === "string" ? { codeMode } : {}), });