mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(ollama): preserve selected model capabilities during onboarding (#115467)
This commit is contained in:
committed by
GitHub
parent
76c6ec0740
commit
25909ef5da
@@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user