fix(ollama): promote plain text tool calls

Wrap Ollama native streams with the shared plain-text tool-call compatibility wrapper so local/plain-text tool requests are delivered as structured toolCall events when matching tools are available.

Verified with live local Ollama proof, focused Testbox Vitest, Testbox check:changed, and autoreview.
This commit is contained in:
Vincent Koc
2026-05-28 19:18:41 +01:00
committed by GitHub
parent 7859ee396e
commit 21e69fdd4f
2 changed files with 117 additions and 2 deletions

View File

@@ -106,6 +106,9 @@ describe("createOllamaStreamFn thinking events", () => {
async function streamOllamaEvents(
chunks: Array<Record<string, unknown>>,
options: Parameters<ReturnType<typeof createOllamaStreamFn>>[2] = {},
context: Parameters<ReturnType<typeof createOllamaStreamFn>>[1] = {
messages: [{ role: "user", content: "test" }],
} as never,
): Promise<Array<{ type: string; [key: string]: unknown }>> {
const body = makeNdjsonBody(chunks);
fetchWithSsrFGuardMock.mockResolvedValue({
@@ -116,7 +119,7 @@ describe("createOllamaStreamFn thinking events", () => {
const streamFn = createOllamaStreamFn("http://localhost:11434");
const stream = streamFn(
{ api: "ollama", provider: "ollama", id: "qwen3.5", contextWindow: 65536 } as never,
{ messages: [{ role: "user", content: "test" }] } as never,
context,
options,
);
@@ -249,4 +252,108 @@ describe("createOllamaStreamFn thinking events", () => {
auditContext: "ollama-stream.chat",
});
});
it("promotes standalone bracketed local-model tool text to a structured tool call", async () => {
const rawToolText = [
"[mempalace_mempalace_search]",
'{"query":"codename","wing":"personal","room":"identities"}',
"[END_TOOL_REQUEST]",
].join("\n");
const events = await streamOllamaEvents(
[
{
model: "qwen3.5",
created_at: "2026-01-01T00:00:00Z",
message: { role: "assistant", content: rawToolText },
done: false,
},
{
model: "qwen3.5",
created_at: "2026-01-01T00:00:01Z",
message: { role: "assistant", content: "" },
done: true,
done_reason: "stop",
prompt_eval_count: 10,
eval_count: 5,
},
],
{},
{
messages: [{ role: "user", content: "test" }],
tools: [
{
name: "mempalace_mempalace_search",
description: "Search MemPalace",
parameters: { type: "object", properties: {} },
},
],
} as never,
);
expect(events.map((event) => event.type)).toEqual([
"start",
"toolcall_start",
"toolcall_delta",
"done",
]);
const done = events.find((event) => event.type === "done") as {
message?: { content?: Array<Record<string, unknown>>; stopReason?: string };
reason?: string;
};
expect(done.reason).toBe("toolUse");
expect(done.message?.stopReason).toBe("toolUse");
expect(done.message?.content?.[0]).toMatchObject({
type: "toolCall",
name: "mempalace_mempalace_search",
arguments: { query: "codename", wing: "personal", room: "identities" },
});
});
it("promotes standalone Harmony local-model tool text to a structured tool call", async () => {
const rawToolText =
'commentary to=read code {"path":"/path/to/file","line_start":1,"line_end":400}';
const events = await streamOllamaEvents(
[
{
model: "qwen3.5",
created_at: "2026-01-01T00:00:00Z",
message: { role: "assistant", content: rawToolText },
done: false,
},
{
model: "qwen3.5",
created_at: "2026-01-01T00:00:01Z",
message: { role: "assistant", content: "" },
done: true,
done_reason: "stop",
prompt_eval_count: 10,
eval_count: 5,
},
],
{},
{
messages: [{ role: "user", content: "test" }],
tools: [{ name: "read", description: "Read files", parameters: { type: "object" } }],
} as never,
);
expect(events.map((event) => event.type)).toEqual([
"start",
"toolcall_start",
"toolcall_delta",
"done",
]);
const done = events.find((event) => event.type === "done") as {
message?: { content?: Array<Record<string, unknown>>; stopReason?: string };
reason?: string;
};
expect(done.reason).toBe("toolUse");
expect(done.message?.content?.[0]).toMatchObject({
type: "toolCall",
name: "read",
arguments: { path: "/path/to/file", line_start: 1, line_end: 400 },
});
});
});

View File

@@ -23,6 +23,7 @@ import {
} from "openclaw/plugin-sdk/provider-model-shared";
import {
createMoonshotThinkingWrapper,
createPlainTextToolCallCompatWrapper,
resolveMoonshotThinkingType,
streamWithPayloadPatch,
} from "openclaw/plugin-sdk/provider-stream-shared";
@@ -1080,7 +1081,7 @@ function resolveOllamaRequestTimeoutMs(
return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : undefined;
}
export function createOllamaStreamFn(
function createRawOllamaStreamFn(
baseUrl: string,
defaultHeaders?: Record<string, string>,
): StreamFn {
@@ -1409,6 +1410,13 @@ export function createOllamaStreamFn(
};
}
export function createOllamaStreamFn(
baseUrl: string,
defaultHeaders?: Record<string, string>,
): StreamFn {
return createPlainTextToolCallCompatWrapper(createRawOllamaStreamFn(baseUrl, defaultHeaders));
}
export function createConfiguredOllamaStreamFn(params: {
model: { baseUrl?: string; headers?: unknown };
providerBaseUrl?: string;