mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 14:28:35 +00:00
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:
@@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user