fix(ollama): preserve selected model capabilities during onboarding (#115467)

This commit is contained in:
Peter Steinberger
2026-07-28 21:07:48 -04:00
committed by GitHub
parent 76c6ec0740
commit 25909ef5da
2 changed files with 104 additions and 5 deletions
@@ -1,5 +1,5 @@
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { jsonResponse, requestUrl } from "openclaw/plugin-sdk/test-env";
import { jsonResponse, requestBodyText, requestUrl } from "openclaw/plugin-sdk/test-env";
import { afterEach, describe, expect, it, vi } from "vitest";
import { configureOllamaNonInteractive } from "./setup.js";
@@ -32,14 +32,25 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => {
};
});
function createOllamaFetchMock(params: { tags: string[]; pullResponse?: Response }) {
return vi.fn(async (input: string | URL | Request) => {
function createOllamaFetchMock(params: {
tags: string[];
show?: Record<string, number | undefined>;
capabilities?: Record<string, string[] | undefined>;
pullResponse?: Response;
}) {
return vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = requestUrl(input);
if (url.endsWith("/api/tags")) {
return jsonResponse({ models: params.tags.map((name) => ({ name })) });
}
if (url.endsWith("/api/show")) {
return jsonResponse({ capabilities: ["tools"] });
const body = JSON.parse(requestBodyText(init?.body)) as { name?: string };
const contextWindow = body.name ? params.show?.[body.name] : undefined;
const capabilities = body.name ? (params.capabilities?.[body.name] ?? ["tools"]) : ["tools"];
return jsonResponse({
...(contextWindow ? { model_info: { "llama.context_length": contextWindow } } : {}),
capabilities,
});
}
if (url.endsWith("/api/pull")) {
return params.pullResponse ?? new Response('{"status":"success"}\n', { status: 200 });
@@ -56,7 +67,7 @@ function createRuntime(): RuntimeEnv {
} as unknown as RuntimeEnv;
}
describe("Ollama non-interactive onboarding auth", () => {
describe("Ollama non-interactive onboarding", () => {
afterEach(() => {
vi.unstubAllGlobals();
upsertAuthProfileWithLock.mockClear();
@@ -140,4 +151,79 @@ describe("Ollama non-interactive onboarding auth", () => {
);
expect(upsertAuthProfileWithLock).toHaveBeenCalledTimes(1);
});
it("preserves the capabilities of an explicitly selected model beyond the discovery limit", async () => {
const modelId = "gemma4:e2b";
const fetchMock = createOllamaFetchMock({
tags: [...Array.from({ length: 200 }, (_, index) => `other-${index}`), modelId],
show: { [modelId]: 131_072 },
capabilities: { [modelId]: ["completion", "tools", "vision", "thinking"] },
});
vi.stubGlobal("fetch", fetchMock);
const result = await configureOllamaNonInteractive({
nextConfig: {},
opts: {
customBaseUrl: "http://127.0.0.1:11434",
customModelId: modelId,
},
runtime: createRuntime(),
});
expect(result.agents?.defaults?.model).toEqual({ primary: `ollama/${modelId}` });
expect(
result.models?.providers?.ollama?.models?.find((model) => model.id === modelId),
).toMatchObject({
id: modelId,
input: ["text", "image"],
reasoning: true,
contextWindow: 131_072,
compat: { supportsTools: true },
});
expect(
fetchMock.mock.calls.filter((call) => {
if (!requestUrl(call[0]).endsWith("/api/show")) {
return false;
}
const init = call[1] as RequestInit | undefined;
return JSON.parse(requestBodyText(init?.body)).name === modelId;
}),
).toHaveLength(1);
});
it("preserves the discovered capabilities of a newly pulled selected model", async () => {
const modelId = "gemma4:e2b";
const fetchMock = createOllamaFetchMock({
tags: [],
show: { [modelId]: 131_072 },
capabilities: { [modelId]: ["completion", "tools", "vision", "thinking"] },
pullResponse: new Response('{"status":"success"}\n', { status: 200 }),
});
vi.stubGlobal("fetch", fetchMock);
const result = await configureOllamaNonInteractive({
nextConfig: {},
opts: {
customBaseUrl: "http://127.0.0.1:11434",
customModelId: modelId,
},
runtime: createRuntime(),
});
expect(result.agents?.defaults?.model).toEqual({ primary: `ollama/${modelId}` });
expect(
result.models?.providers?.ollama?.models?.find((model) => model.id === modelId),
).toMatchObject({
id: modelId,
input: ["text", "image"],
reasoning: true,
contextWindow: 131_072,
compat: { supportsTools: true },
});
expect(fetchMock.mock.calls.map((call) => requestUrl(call[0]))).toEqual([
"http://127.0.0.1:11434/api/tags",
"http://127.0.0.1:11434/api/pull",
"http://127.0.0.1:11434/api/show",
]);
});
});
+13
View File
@@ -911,6 +911,19 @@ export async function configureOllamaNonInteractive(params: {
);
}
if (!requestedCloudModel && !discoveredModelsByName.has(defaultModelId)) {
// Explicit and newly pulled models can fall outside the bounded catalog scan.
const selectedModel = expectDefined(
(
await enrichOllamaModelsWithContext(baseUrl, [
models.find((model) => model.name === defaultModelId) ?? { name: defaultModelId },
])
)[0],
"selected Ollama setup model",
);
discoveredModelsByName.set(defaultModelId, selectedModel);
}
// Failed setup must not leave a durable local profile behind.
await storeOllamaCredential(params.agentDir);