fix(ollama): preserve cloud model context windows (#112430)

* fix(ollama): preserve cloud model context windows

* fix(ollama): recognize cloud model source forms

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>

---------

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
WhatsSkiLL
2026-07-22 07:45:03 +02:00
committed by GitHub
parent a691eb7372
commit feb529fd2b
4 changed files with 74 additions and 27 deletions
@@ -7,6 +7,7 @@ import {
buildOllamaModelDefinition,
capLocalOllamaProviderContext,
enrichOllamaModelsWithContext,
isOllamaCloudModel,
fetchOllamaModels,
queryOllamaModelShowInfo,
resolveOllamaApiBase,
@@ -30,13 +31,33 @@ describe("ollama provider models", () => {
models: [
buildOllamaModelDefinition("qwen3.5:4b", 262_144),
buildOllamaModelDefinition("small", 16_384),
buildOllamaModelDefinition("glm-5.2:cloud", 1_000_000),
buildOllamaModelDefinition("gpt-oss:120b-cloud", 131_072),
buildOllamaModelDefinition("local-cloud", 65_536),
],
});
expect(provider.models).toEqual([
expect.objectContaining({ contextWindow: 262_144, contextTokens: 32_768 }),
expect.objectContaining({ contextWindow: 16_384, contextTokens: 16_384 }),
expect.objectContaining({ id: "glm-5.2:cloud", contextWindow: 1_000_000 }),
expect.objectContaining({ id: "gpt-oss:120b-cloud", contextWindow: 131_072 }),
expect.objectContaining({ id: "local-cloud", contextTokens: 32_768 }),
]);
expect(provider.models?.[2]).not.toHaveProperty("contextTokens");
expect(provider.models?.[3]).not.toHaveProperty("contextTokens");
});
it.each([
["glm-5.2:cloud", true],
["gpt-oss:120b-cloud", true],
["local-cloud", false],
["invalid:cloud-cloud", false],
["invalid:local:cloud", false],
["invalid:local-cloud", false],
["invalid:cloud:local", false],
])("classifies Ollama model source %s", (modelId, expected) => {
expect(isOllamaCloudModel(modelId)).toBe(expected);
});
it("sets discovered models with context windows from /api/show", async () => {
+32 -1
View File
@@ -256,6 +256,37 @@ export async function enrichOllamaModelsWithContext(
return enriched;
}
type OllamaModelSource = "cloud" | "local";
function parseOllamaModelSourceSuffix(
modelName: string,
): { base: string; source: OllamaModelSource } | undefined {
const sourceSeparator = modelName.lastIndexOf(":");
if (sourceSeparator < 0) {
return undefined;
}
const source = modelName.slice(sourceSeparator + 1);
if (source === "cloud" || source === "local") {
return { base: modelName.slice(0, sourceSeparator), source };
}
if (!source.includes("/") && source.endsWith("-cloud")) {
return {
base: modelName.slice(0, sourceSeparator + 1) + source.slice(0, -"-cloud".length),
source: "cloud",
};
}
return undefined;
}
export function isOllamaCloudModel(modelName: string | undefined): boolean {
const normalized = modelName?.trim().toLowerCase();
if (!normalized) {
return false;
}
const parsed = parseOllamaModelSourceSuffix(normalized);
return parsed?.source === "cloud" && parseOllamaModelSourceSuffix(parsed.base) === undefined;
}
export function isReasoningModelHeuristic(modelId: string): boolean {
return /r1|reasoning|think|reason/i.test(modelId);
}
@@ -304,7 +335,7 @@ export function buildOllamaModelDefinition(
}
export function capLocalOllamaModelContext(model: ModelDefinitionConfig): ModelDefinitionConfig {
if (typeof model.contextWindow !== "number") {
if (isOllamaCloudModel(model.id) || typeof model.contextWindow !== "number") {
return model;
}
return {
+19 -18
View File
@@ -986,26 +986,27 @@ describe("ollama setup", () => {
expect(runtime.log).toHaveBeenCalledWith("Default Ollama model: gemma4:latest");
});
it("accepts cloud models in non-interactive mode without pulling", async () => {
const fetchMock = createOllamaFetchMock({ tags: [] });
vi.stubGlobal("fetch", fetchMock);
const runtime = createRuntime();
it.each(["kimi-k2.5:cloud", "gpt-oss:120b-cloud"])(
"accepts cloud model %s in non-interactive mode without pulling",
async (modelId) => {
const fetchMock = createOllamaFetchMock({ tags: [] });
vi.stubGlobal("fetch", fetchMock);
const runtime = createRuntime();
const result = await configureOllamaNonInteractive({
nextConfig: {},
opts: {
customBaseUrl: "http://127.0.0.1:11434",
customModelId: "kimi-k2.5:cloud",
},
runtime,
});
const result = await configureOllamaNonInteractive({
nextConfig: {},
opts: {
customBaseUrl: "http://127.0.0.1:11434",
customModelId: modelId,
},
runtime,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(result.models?.providers?.ollama?.models?.map((model) => model.id)).toContain(
"kimi-k2.5:cloud",
);
expect(result.agents?.defaults?.model).toEqual({ primary: "ollama/kimi-k2.5:cloud" });
});
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(result.models?.providers?.ollama?.models?.map((model) => model.id)).toContain(modelId);
expect(result.agents?.defaults?.model).toEqual({ primary: `ollama/${modelId}` });
},
);
it("exits when Ollama is unreachable", async () => {
const fetchMock = createOllamaFetchMock({
+2 -8
View File
@@ -19,10 +19,7 @@ import { applyAgentDefaultModelPrimary } from "openclaw/plugin-sdk/provider-onbo
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
import { WizardCancelledError, type WizardPrompter } from "openclaw/plugin-sdk/setup";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
OLLAMA_CLOUD_BASE_URL,
OLLAMA_CLOUD_DEFAULT_MODELS,
@@ -37,6 +34,7 @@ import {
buildOllamaModelDefinition,
enrichOllamaModelsWithContext,
fetchOllamaModels,
isOllamaCloudModel,
resolveOllamaApiBase,
type OllamaModelWithContext,
} from "./provider-models.js";
@@ -121,10 +119,6 @@ function normalizeOllamaModelName(value: string | undefined): string | undefined
return trimmed;
}
function isOllamaCloudModel(modelName: string | undefined): boolean {
return normalizeOptionalLowercaseString(modelName)?.endsWith(":cloud") === true;
}
function formatOllamaPullStatus(status: string): { text: string; hidePercent: boolean } {
const trimmed = status.trim();
const partStatusMatch = trimmed.match(/^([a-z-]+)\s+(?:sha256:)?[a-f0-9]{8,}$/i);