From 4fdfb8b1bf69c4b74656ada6c1f5bd7c970c5f48 Mon Sep 17 00:00:00 2001 From: Vito Cappello Date: Thu, 20 Aug 2026 11:46:39 -0400 Subject: [PATCH] fix(ollama): carry real Ollama Cloud context windows and capabilities (#126653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ollama): carry real Ollama Cloud context windows and capabilities The ollama-cloud catalog still described three models (minimax-m2.7, glm-5.1, glm-5.2) plus a retired kimi-k2.5. Every other cloud model — including kimi-k3, the current flagship — was absent, so core synthesized it at the generic DEFAULT_CONTEXT_TOKENS of 200k. A kimi-k3 session therefore ran with 200,000 of its real 1,048,576 token window: 80% of the context silently discarded, with no warning anywhere in the product. Describe the full current cloud lineup with context windows, input modalities and reasoning support verified against live /api/show and the ollama.com model pages. Only mistral-large-3 lacks thinking (vision + tools + cloud only). Suffixed refs shared the same defect from the other side: the default lookup is keyed bare, so `kimi-k3:cloud` missed it and fell to the 128k plugin default. A hardcoded glm-5.2 literal in buildOllamaModelDefinition had been papering over that for exactly one model; replace it with a lookup through the canonical cloud-id normalizer, which model-reasoning.ts already owned, and drop the duplicate spelling of that helper. * fix(ollama): cover exact cloud catalog variants * fix(ollama): remove invalid cloud aliases * fix(ollama): default Ollama Cloud onboarding to minimax-m3 Cloud onboarding derives `defaultModel` from the first entry of OLLAMA_CLOUD_DEFAULT_MODELS, so array order silently owned the out-of-box model choice. Put minimax-m3 (524,288 ctx, thinking + tools + vision) at index 0, add it to the bundled rows it was missing from, and document the ordering contract at the declaration. Pin the resolved default id in the cloud setup tests so a reorder cannot move it unnoticed, and align the provider doc's onboarding default and fallback row list. Claude-Session: https://claude.ai/code/session_01QXUQuDVataA5o16kxNnmoX * fix(ollama): preserve default and shared model contracts * test(ollama): consolidate cloud setup capability expectations --------- Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com> Co-authored-by: Peter Steinberger --- docs/providers/ollama-cloud.md | 9 +- extensions/ollama/index.test.ts | 16 + extensions/ollama/openclaw.plugin.json | 418 ++++++++++++++++-- extensions/ollama/src/defaults.ts | 23 + extensions/ollama/src/model-reasoning.ts | 9 +- extensions/ollama/src/provider-models.test.ts | 80 ++++ extensions/ollama/src/provider-models.ts | 21 +- .../ollama/src/setup-model-selection.ts | 11 +- extensions/ollama/src/setup.test.ts | 46 +- 9 files changed, 548 insertions(+), 85 deletions(-) diff --git a/docs/providers/ollama-cloud.md b/docs/providers/ollama-cloud.md index 014190fe0861..ebd2f57c4aa7 100644 --- a/docs/providers/ollama-cloud.md +++ b/docs/providers/ollama-cloud.md @@ -71,10 +71,11 @@ openclaw models set ollama-cloud/kimi-k2.6 ``` Hosted ids in the live catalog include `deepseek-v4-flash`, `glm-5.2`, -`gpt-oss:20b`, `kimi-k2.6`, and `minimax-m2.7`. When live discovery returns -nothing, OpenClaw falls back to the bundled rows `minimax-m2.7`, `glm-5.1`, -and `glm-5.2`. The retiring `kimi-k2.5` model is hidden from model pickers but -remains selectable by exact reference until Ollama retires it on July 31, 2026. +`gpt-oss:20b`, `kimi-k3`, and `minimax-m3`. When live discovery returns +nothing, OpenClaw falls back to the bundled rows `minimax-m2.7`, `minimax-m3`, +`kimi-k3`, `glm-5.1`, and `glm-5.2`. Retired `kimi-k2.5` remains marked +deprecated for existing exact references, but is no longer a current hosted +model. Model ids are cloud catalog ids, not local pull names. If a model name works in a local Ollama host but is absent from the hosted catalog, use the `ollama` diff --git a/extensions/ollama/index.test.ts b/extensions/ollama/index.test.ts index 3e9ea875716f..0a20239cf55b 100644 --- a/extensions/ollama/index.test.ts +++ b/extensions/ollama/index.test.ts @@ -2119,6 +2119,8 @@ describe("ollama plugin", () => { expect(result.provider.baseUrl).toBe("https://ollama.com"); expect(result.provider.models?.map((model: { id: string }) => model.id)).toEqual([ "minimax-m2.7", + "minimax-m3", + "kimi-k3", "glm-5.1", "glm-5.2", ]); @@ -2130,6 +2132,20 @@ describe("ollama plugin", () => { input: ["text"], compat: { supportsTools: true, supportsUsageInStreaming: true }, }), + expect.objectContaining({ + id: "minimax-m3", + contextWindow: 524_288, + reasoning: true, + input: ["text", "image"], + compat: { supportsTools: true, supportsUsageInStreaming: true }, + }), + expect.objectContaining({ + id: "kimi-k3", + contextWindow: 1_048_576, + reasoning: true, + input: ["text", "image"], + compat: { supportsTools: true, supportsUsageInStreaming: true }, + }), expect.objectContaining({ id: "glm-5.1", contextWindow: 202_752, diff --git a/extensions/ollama/openclaw.plugin.json b/extensions/ollama/openclaw.plugin.json index 90a3aa3836a4..4431a6151474 100644 --- a/extensions/ollama/openclaw.plugin.json +++ b/extensions/ollama/openclaw.plugin.json @@ -8,10 +8,7 @@ "onStartup": true }, "enabledByDefault": true, - "providers": [ - "ollama", - "ollama-cloud" - ], + "providers": ["ollama", "ollama-cloud"], "providerCatalogEntry": "./provider-discovery.ts", "providerRequest": { "providers": { @@ -33,25 +30,17 @@ } } }, - "syntheticAuthRefs": [ - "ollama" - ], - "nonSecretAuthMarkers": [ - "ollama-local" - ], + "syntheticAuthRefs": ["ollama"], + "nonSecretAuthMarkers": ["ollama-local"], "setup": { "providers": [ { "id": "ollama", - "envVars": [ - "OLLAMA_API_KEY" - ] + "envVars": ["OLLAMA_API_KEY"] }, { "id": "ollama-cloud", - "envVars": [ - "OLLAMA_API_KEY" - ] + "envVars": ["OLLAMA_API_KEY"] } ] }, @@ -100,10 +89,7 @@ "name": "kimi-k2.5", "status": "deprecated", "reasoning": true, - "input": [ - "text", - "image" - ], + "input": ["text", "image"], "cost": { "input": 0, "output": 0, @@ -118,19 +104,199 @@ } }, { - "id": "minimax-m2.7", - "name": "minimax-m2.7", + "id": "kimi-k2.6", + "name": "kimi-k2.6", "reasoning": true, - "input": [ - "text" - ], + "input": ["text", "image"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 196608, + "contextWindow": 262144, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "kimi-k2.7-code", + "name": "kimi-k2.7-code", + "reasoning": true, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "kimi-k3", + "name": "kimi-k3", + "reasoning": true, + "input": ["text", "image"], + "cost": { + "input": 3, + "output": 15, + "cacheRead": 0.3 + }, + "contextWindow": 1048576, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true, + "codeMode": "capable" + } + }, + { + "id": "deepseek-v4-flash", + "name": "deepseek-v4-flash", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true, + "codeMode": "capable" + } + }, + { + "id": "deepseek-v4-flash:0731", + "name": "deepseek-v4-flash:0731", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "deepseek-v4-flash:preview", + "name": "deepseek-v4-flash:preview", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "deepseek-v4-pro", + "name": "deepseek-v4-pro", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true, + "codeMode": "capable" + } + }, + { + "id": "deepseek-v4-pro:0813", + "name": "deepseek-v4-pro:0813", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "deepseek-v4-pro:preview", + "name": "deepseek-v4-pro:preview", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 524288, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "gemma4", + "name": "gemma4", + "reasoning": true, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "gemma4:31b", + "name": "gemma4:31b", + "reasoning": true, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, "maxTokens": 8192, "compat": { "supportsTools": true, @@ -141,9 +307,7 @@ "id": "glm-5.1", "name": "glm-5.1", "reasoning": true, - "input": [ - "text" - ], + "input": ["text"], "cost": { "input": 0, "output": 0, @@ -162,9 +326,7 @@ "id": "glm-5.2", "name": "glm-5.2", "reasoning": true, - "input": [ - "text" - ], + "input": ["text"], "cost": { "input": 0, "output": 0, @@ -178,6 +340,186 @@ "supportsUsageInStreaming": true, "codeMode": "capable" } + }, + { + "id": "gpt-oss:120b", + "name": "gpt-oss:120b", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "gpt-oss:20b", + "name": "gpt-oss:20b", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "minimax-m2.7", + "name": "minimax-m2.7", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 196608, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "minimax-m3", + "name": "minimax-m3", + "reasoning": true, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 524288, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "mistral-large-3:675b", + "name": "mistral-large-3:675b", + "reasoning": false, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "nemotron-3-nano:30b", + "name": "nemotron-3-nano:30b", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "nemotron-3-super", + "name": "nemotron-3-super", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "nemotron-3-ultra", + "name": "nemotron-3-ultra", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "qwen3.5", + "name": "qwen3.5", + "reasoning": true, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "qwen3.5:397b", + "name": "qwen3.5:397b", + "reasoning": true, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } } ] } @@ -188,15 +530,9 @@ } }, "contracts": { - "embeddingProviders": [ - "ollama" - ], - "tools": [ - "node_inference" - ], - "webSearchProviders": [ - "ollama" - ] + "embeddingProviders": ["ollama"], + "tools": ["node_inference"], + "webSearchProviders": ["ollama"] }, "configSchema": { "type": "object", diff --git a/extensions/ollama/src/defaults.ts b/extensions/ollama/src/defaults.ts index e20cd07bd48c..f8f303948950 100644 --- a/extensions/ollama/src/defaults.ts +++ b/extensions/ollama/src/defaults.ts @@ -5,12 +5,27 @@ const OLLAMA_DOCKER_HOST_BASE_URL = "http://host.docker.internal:11434"; export const OLLAMA_CLOUD_BASE_URL = "https://ollama.com"; export const OLLAMA_CLOUD_PROVIDER_ID = "ollama-cloud"; export const OLLAMA_GLM52_CLOUD_MODEL_ID = "glm-5.2"; +/** + * Order is a contract: cloud onboarding merges this list ahead of live discovery and takes + * the first name as `defaultModel` (`setup.runtime.ts`). Reordering this array changes what + * every new setup selects, so keep the intended default at index 0. + */ export const OLLAMA_CLOUD_DEFAULT_MODELS = [ { id: "minimax-m2.7", contextWindow: 196_608, capabilities: ["completion", "thinking", "tools"], }, + { + id: "minimax-m3", + contextWindow: 524_288, + capabilities: ["completion", "thinking", "tools", "vision"], + }, + { + id: "kimi-k3", + contextWindow: 1_048_576, + capabilities: ["completion", "thinking", "tools", "vision"], + }, { id: "glm-5.1", contextWindow: 202_752, @@ -23,6 +38,14 @@ export const OLLAMA_CLOUD_DEFAULT_MODELS = [ }, ] as const; +/** Cloud models are referenced bare, `:cloud`-suffixed, and `-cloud`-suffixed. */ +export function normalizeOllamaCloudModelId(modelId: string): string { + return modelId + .trim() + .toLowerCase() + .replace(/(?::cloud|-cloud)$/, ""); +} + export const OLLAMA_DEFAULT_CONTEXT_WINDOW = 128000; export const OLLAMA_LOCAL_CONTEXT_TOKENS = 32_768; export const OLLAMA_DEFAULT_MAX_TOKENS = 8192; diff --git a/extensions/ollama/src/model-reasoning.ts b/extensions/ollama/src/model-reasoning.ts index d3678ed73d99..60298c4269f1 100644 --- a/extensions/ollama/src/model-reasoning.ts +++ b/extensions/ollama/src/model-reasoning.ts @@ -1,14 +1,9 @@ // Ollama plugin module owns model-specific native thinking contracts. +import { normalizeOllamaCloudModelId } from "./defaults.js"; + export function supportsOllamaCloudFullThinkingEffort(modelId: string): boolean { // These hosted families accept low, medium, high, and max even when // lightweight catalog projections omit their reasoning metadata. const normalized = normalizeOllamaCloudModelId(modelId); return normalized === "glm-5.2" || /^deepseek-v4-(?:flash|pro)$/.test(normalized); } - -function normalizeOllamaCloudModelId(modelId: string): string { - return modelId - .trim() - .toLowerCase() - .replace(/(?::cloud|-cloud)$/, ""); -} diff --git a/extensions/ollama/src/provider-models.test.ts b/extensions/ollama/src/provider-models.test.ts index 7ee1ac0b5526..0195c01e26ec 100644 --- a/extensions/ollama/src/provider-models.test.ts +++ b/extensions/ollama/src/provider-models.test.ts @@ -1,10 +1,12 @@ // Ollama tests cover provider models plugin behavior. import { once } from "node:events"; +import { readFileSync } from "node:fs"; import { createServer } from "node:http"; import type { Socket } from "node:net"; import { expectDefined } from "@openclaw/normalization-core"; import { jsonResponse, requestBodyText, requestUrl } from "openclaw/plugin-sdk/test-env"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { OLLAMA_DEFAULT_CONTEXT_WINDOW } from "./defaults.js"; import { buildOllamaProvider, buildOllamaModelDefinition, @@ -51,6 +53,68 @@ describe("ollama provider models", () => { expect(resolveOllamaApiBase("http://127.0.0.1:11434///")).toBe("http://127.0.0.1:11434"); }); + it("declares every exact currently served Ollama Cloud model id", () => { + const manifest = JSON.parse( + readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"), + ) as { + modelCatalog: { + providers: Record< + string, + { + models: Array<{ + id: string; + status?: string; + contextWindow: number; + input: string[]; + reasoning: boolean; + cost?: { + input?: number; + output?: number; + cacheRead?: number; + }; + }>; + } + >; + }; + }; + const models = manifest.modelCatalog.providers["ollama-cloud"]?.models ?? []; + const declared = new Map(models.map((model) => [model.id, model])); + + const servedModels = [ + ["glm-5.1", 202_752, ["text"], true], + ["glm-5.2", 1_000_000, ["text"], true], + ["minimax-m2.7", 196_608, ["text"], true], + ["deepseek-v4-flash", 1_048_576, ["text"], true], + ["deepseek-v4-flash:0731", 1_048_576, ["text"], true], + ["deepseek-v4-flash:preview", 1_048_576, ["text"], true], + ["deepseek-v4-pro", 1_048_576, ["text"], true], + ["deepseek-v4-pro:0813", 1_048_576, ["text"], true], + ["deepseek-v4-pro:preview", 524_288, ["text"], true], + ["gemma4", 262_144, ["text", "image"], true], + ["gemma4:31b", 262_144, ["text", "image"], true], + ["gpt-oss:120b", 131_072, ["text"], true], + ["gpt-oss:20b", 131_072, ["text"], true], + ["kimi-k2.6", 262_144, ["text", "image"], true], + ["kimi-k2.7-code", 262_144, ["text", "image"], true], + ["kimi-k3", 1_048_576, ["text", "image"], true], + ["minimax-m3", 524_288, ["text", "image"], true], + ["mistral-large-3:675b", 262_144, ["text", "image"], false], + ["nemotron-3-nano:30b", 262_144, ["text"], true], + ["nemotron-3-super", 262_144, ["text"], true], + ["nemotron-3-ultra", 262_144, ["text"], true], + ["qwen3.5", 262_144, ["text", "image"], true], + ["qwen3.5:397b", 262_144, ["text", "image"], true], + ] as const; + servedModels.forEach(([id, contextWindow, input, reasoning]) => { + expect(declared.get(id)).toMatchObject({ contextWindow, input, reasoning }); + }); + expect([...declared.keys()].toSorted()).toEqual( + [...servedModels.map(([id]) => id), "kimi-k2.5"].toSorted(), + ); + expect(declared.get("kimi-k2.5")).toMatchObject({ status: "deprecated" }); + expect(declared.get("kimi-k3")?.cost).toEqual({ input: 3, output: 15, cacheRead: 0.3 }); + }); + it("inspects local models using Ollama's canonical model request field", async () => { const fetchMock = vi.fn(async (_input: string | URL | Request, _init?: RequestInit) => jsonResponse({ model_info: {} }), @@ -249,6 +313,22 @@ describe("ollama provider models", () => { ); }); + it("resolves known cloud context windows for bare and :cloud model refs", () => { + // A suffixed ref must not silently drop to the generic default when live + // inspection is unavailable; both spellings name the same cloud model. + for (const modelId of ["kimi-k3", "kimi-k3:cloud"]) { + expect(buildOllamaModelDefinition(modelId)).toEqual( + expect.objectContaining({ id: modelId, contextWindow: 1_048_576 }), + ); + } + }); + + it("keeps the generic default for cloud models with no known context window", () => { + expect(buildOllamaModelDefinition("not-a-known-model:cloud")).toEqual( + expect.objectContaining({ contextWindow: OLLAMA_DEFAULT_CONTEXT_WINDOW }), + ); + }); + it("uses Modelfile num_ctx when it expands the discovered context window", async () => { const models: OllamaTagModel[] = [{ name: "llama3-32k:latest" }]; const fetchMock = vi.fn(async () => diff --git a/extensions/ollama/src/provider-models.ts b/extensions/ollama/src/provider-models.ts index 6bd2c5ab5dbc..fce6121ac2ed 100644 --- a/extensions/ollama/src/provider-models.ts +++ b/extensions/ollama/src/provider-models.ts @@ -15,6 +15,7 @@ import { OLLAMA_DEFAULT_COST, OLLAMA_DEFAULT_MAX_TOKENS, OLLAMA_LOCAL_CONTEXT_TOKENS, + normalizeOllamaCloudModelId, } from "./defaults.js"; import { supportsOllamaCloudFullThinkingEffort } from "./model-reasoning.js"; @@ -340,6 +341,18 @@ export function isOllamaCloudModel(modelName: string | undefined): boolean { return isCloudModelRef(modelName); } +/** + * Cloud models are referenced both bare (`kimi-k3`) and suffixed (`kimi-k3:cloud`). + * Both spellings must reach the same known context window, or a suffixed ref silently + * falls back to the generic default whenever live inspection is unavailable. + */ +function resolveOllamaCloudDefaultModel( + modelId: string, +): (typeof OLLAMA_CLOUD_DEFAULT_MODELS)[number] | undefined { + const normalized = normalizeOllamaCloudModelId(modelId); + return OLLAMA_CLOUD_DEFAULT_MODELS.find((model) => model.id === normalized); +} + export function isReasoningModelHeuristic(modelId: string): boolean { return /r1|reasoning|think|reason/i.test(modelId); } @@ -371,12 +384,8 @@ export function buildOllamaModelDefinition( cost: OLLAMA_DEFAULT_COST, contextWindow: contextWindow ?? - (modelId - .trim() - .toLowerCase() - .replace(/:cloud$/, "") === "glm-5.2" - ? 1_000_000 - : OLLAMA_DEFAULT_CONTEXT_WINDOW), + resolveOllamaCloudDefaultModel(modelId)?.contextWindow ?? + OLLAMA_DEFAULT_CONTEXT_WINDOW, maxTokens: OLLAMA_DEFAULT_MAX_TOKENS, compat, }; diff --git a/extensions/ollama/src/setup-model-selection.ts b/extensions/ollama/src/setup-model-selection.ts index a7469118694c..565faf2cf554 100644 --- a/extensions/ollama/src/setup-model-selection.ts +++ b/extensions/ollama/src/setup-model-selection.ts @@ -1,6 +1,6 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { selectPreferredLocalModelId } from "openclaw/plugin-sdk/provider-model-shared"; -import { OLLAMA_CLOUD_DEFAULT_MODELS } from "./defaults.js"; +import { normalizeOllamaCloudModelId, OLLAMA_CLOUD_DEFAULT_MODELS } from "./defaults.js"; import { buildDefaultOllamaCloudModelDefinition, buildOllamaModelDefinition, @@ -126,8 +126,13 @@ export function buildOllamaModelsConfig( ) { return modelNames.map((name) => { const discovered = discoveredModelsByName?.get(name); - const defaultModel = defaultModels.find((model) => model.id === name); - if (defaultModel && !discovered) { + // Cloud suggestions arrive suffixed (`kimi-k3:cloud`); the default table is keyed bare. + // Match through the suffix for context/capabilities, but keep the requested id: the + // suffixed spelling is what gets written into config. + const defaultModel = defaultModels.find( + (model) => model.id === normalizeOllamaCloudModelId(name), + ); + if (defaultModel && !discovered && defaultModel.id === name) { return buildDefaultOllamaCloudModelDefinition(defaultModel); } const capabilities = diff --git a/extensions/ollama/src/setup.test.ts b/extensions/ollama/src/setup.test.ts index 0e20eef67544..67a6b340ba48 100644 --- a/extensions/ollama/src/setup.test.ts +++ b/extensions/ollama/src/setup.test.ts @@ -205,6 +205,7 @@ describe("ollama setup", () => { const modelIds = result.config.models?.providers?.ollama?.models?.map((m) => m.id); expect(modelIds?.[0]).toBe("minimax-m2.7"); + expect(result.defaultModel).toBe("ollama/minimax-m2.7"); expect(result.config.models?.providers?.ollama?.baseUrl).toBe("https://ollama.com"); expect(result.config.models?.providers?.ollama?.apiKey).toBe("test-ollama-key"); expect(result.credential).toBe("test-ollama-key"); @@ -246,6 +247,8 @@ describe("ollama setup", () => { expect(modelIds).toEqual([ "gemma4", "minimax-m2.7:cloud", + "minimax-m3:cloud", + "kimi-k3:cloud", "glm-5.1:cloud", "glm-5.2:cloud", "llama3:8b", @@ -457,30 +460,23 @@ describe("ollama setup", () => { const models = result.config.models?.providers?.ollama?.models; const modelIds = models?.map((m) => m.id); - expect(modelIds).toEqual(["minimax-m2.7", "glm-5.1", "glm-5.2"]); - expect(models).toEqual([ - expect.objectContaining({ - id: "minimax-m2.7", - contextWindow: 196_608, - reasoning: true, - input: ["text"], - compat: { supportsTools: true, supportsUsageInStreaming: true }, - }), - expect.objectContaining({ - id: "glm-5.1", - contextWindow: 202_752, - reasoning: true, - input: ["text"], - compat: { supportsTools: true, supportsUsageInStreaming: true }, - }), - expect.objectContaining({ - id: "glm-5.2", - contextWindow: 1_000_000, - reasoning: true, - input: ["text"], - compat: { supportsTools: true, supportsUsageInStreaming: true }, - }), - ]); + expect(modelIds).toEqual(["minimax-m2.7", "minimax-m3", "kimi-k3", "glm-5.1", "glm-5.2"]); + expect(models).toEqual( + expect.arrayContaining( + [ + { id: "minimax-m2.7", contextWindow: 196_608 }, + { id: "glm-5.1", contextWindow: 202_752 }, + { id: "glm-5.2", contextWindow: 1_000_000 }, + ].map((model) => + expect.objectContaining({ + ...model, + reasoning: true, + input: ["text"], + compat: { supportsTools: true, supportsUsageInStreaming: true }, + }), + ), + ), + ); }); it("cloud mode populates models from ollama.com /api/tags when reachable", async () => { @@ -502,6 +498,8 @@ describe("ollama setup", () => { expect(modelIds).toEqual([ "minimax-m2.7", + "minimax-m3", + "kimi-k3", "glm-5.1", "glm-5.2", "qwen3-coder:480b-cloud",