mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
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:
@@ -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 () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user