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",