


















@@ -106,6 +106,9 @@ describe("createOllamaStreamFn thinking events", () => {
106106async function streamOllamaEvents(
107107chunks: Array<Record<string, unknown>>,
108108options: Parameters<ReturnType<typeof createOllamaStreamFn>>[2] = {},
109+context: Parameters<ReturnType<typeof createOllamaStreamFn>>[1] = {
110+messages: [{ role: "user", content: "test" }],
111+} as never,
109112): Promise<Array<{ type: string; [key: string]: unknown }>> {
110113const body = makeNdjsonBody(chunks);
111114fetchWithSsrFGuardMock.mockResolvedValue({
@@ -116,7 +119,7 @@ describe("createOllamaStreamFn thinking events", () => {
116119const streamFn = createOllamaStreamFn("http://localhost:11434");
117120const stream = streamFn(
118121{ api: "ollama", provider: "ollama", id: "qwen3.5", contextWindow: 65536 } as never,
119-{ messages: [{ role: "user", content: "test" }] } as never,
122+context,
120123options,
121124);
122125@@ -249,4 +252,108 @@ describe("createOllamaStreamFn thinking events", () => {
249252auditContext: "ollama-stream.chat",
250253});
251254});
255+256+it("promotes standalone bracketed local-model tool text to a structured tool call", async () => {
257+const rawToolText = [
258+"[mempalace_mempalace_search]",
259+'{"query":"codename","wing":"personal","room":"identities"}',
260+"[END_TOOL_REQUEST]",
261+].join("\n");
262+263+const events = await streamOllamaEvents(
264+[
265+{
266+model: "qwen3.5",
267+created_at: "2026-01-01T00:00:00Z",
268+message: { role: "assistant", content: rawToolText },
269+done: false,
270+},
271+{
272+model: "qwen3.5",
273+created_at: "2026-01-01T00:00:01Z",
274+message: { role: "assistant", content: "" },
275+done: true,
276+done_reason: "stop",
277+prompt_eval_count: 10,
278+eval_count: 5,
279+},
280+],
281+{},
282+{
283+messages: [{ role: "user", content: "test" }],
284+tools: [
285+{
286+name: "mempalace_mempalace_search",
287+description: "Search MemPalace",
288+parameters: { type: "object", properties: {} },
289+},
290+],
291+} as never,
292+);
293+294+expect(events.map((event) => event.type)).toEqual([
295+"start",
296+"toolcall_start",
297+"toolcall_delta",
298+"done",
299+]);
300+const done = events.find((event) => event.type === "done") as {
301+message?: { content?: Array<Record<string, unknown>>; stopReason?: string };
302+reason?: string;
303+};
304+expect(done.reason).toBe("toolUse");
305+expect(done.message?.stopReason).toBe("toolUse");
306+expect(done.message?.content?.[0]).toMatchObject({
307+type: "toolCall",
308+name: "mempalace_mempalace_search",
309+arguments: { query: "codename", wing: "personal", room: "identities" },
310+});
311+});
312+313+it("promotes standalone Harmony local-model tool text to a structured tool call", async () => {
314+const rawToolText =
315+'commentary to=read code {"path":"/path/to/file","line_start":1,"line_end":400}';
316+317+const events = await streamOllamaEvents(
318+[
319+{
320+model: "qwen3.5",
321+created_at: "2026-01-01T00:00:00Z",
322+message: { role: "assistant", content: rawToolText },
323+done: false,
324+},
325+{
326+model: "qwen3.5",
327+created_at: "2026-01-01T00:00:01Z",
328+message: { role: "assistant", content: "" },
329+done: true,
330+done_reason: "stop",
331+prompt_eval_count: 10,
332+eval_count: 5,
333+},
334+],
335+{},
336+{
337+messages: [{ role: "user", content: "test" }],
338+tools: [{ name: "read", description: "Read files", parameters: { type: "object" } }],
339+} as never,
340+);
341+342+expect(events.map((event) => event.type)).toEqual([
343+"start",
344+"toolcall_start",
345+"toolcall_delta",
346+"done",
347+]);
348+const done = events.find((event) => event.type === "done") as {
349+message?: { content?: Array<Record<string, unknown>>; stopReason?: string };
350+reason?: string;
351+};
352+expect(done.reason).toBe("toolUse");
353+expect(done.message?.content?.[0]).toMatchObject({
354+type: "toolCall",
355+name: "read",
356+arguments: { path: "/path/to/file", line_start: 1, line_end: 400 },
357+});
358+});
252359});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。