Merge b0ca2112ceca035f89f09c2b46e6f262c8386094 into 9fb78453e088cd7b553d7779faa0de5c83708e70

This commit is contained in:
Jepson Liu 2026-03-20 22:18:48 -07:00 committed by GitHub
commit f1f33a1a63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 137 additions and 7 deletions

View File

@ -54,6 +54,77 @@ describe("convertToOllamaMessages", () => {
]);
});
it("parses OpenAI-style toolCall arguments JSON strings for Ollama", () => {
const messages = [
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_1",
name: "get_weather",
arguments: '{"location": "Beijing"}',
},
],
},
];
const result = convertToOllamaMessages(messages);
expect(result[0].tool_calls).toEqual([
{ function: { name: "get_weather", arguments: { location: "Beijing" } } },
]);
});
it("preserves unsafe integer literals in OpenAI-style toolCall argument JSON strings", () => {
const messages = [
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_1",
name: "send",
arguments: '{"target":12345678901234567890}',
},
],
},
];
const result = convertToOllamaMessages(messages);
expect(result[0].tool_calls?.[0].function.arguments).toEqual({
target: "12345678901234567890",
});
});
it("parses tool_use input JSON strings for Ollama", () => {
const messages = [
{
role: "assistant",
content: [
{
type: "tool_use",
id: "call_1",
name: "get_weather",
input: '{"location":"Paris"}',
},
],
},
];
const result = convertToOllamaMessages(messages);
expect(result[0].tool_calls).toEqual([
{ function: { name: "get_weather", arguments: { location: "Paris" } } },
]);
});
it("treats invalid toolCall arguments JSON as empty object", () => {
const messages = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "broken", arguments: "{not json" }],
},
];
const result = convertToOllamaMessages(messages);
expect(result[0].tool_calls).toEqual([{ function: { name: "broken", arguments: {} } }]);
});
it("converts tool result messages with 'tool' role", () => {
const messages = [{ role: "tool", content: "file1.txt\nfile2.txt" }];
const result = convertToOllamaMessages(messages);
@ -166,6 +237,29 @@ describe("buildAssistantMessage", () => {
expect(toolCall.id).toMatch(/^ollama_call_[0-9a-f-]{36}$/);
});
it("normalizes string tool call arguments from Ollama wire format", () => {
const response = {
model: "qwen3:32b",
created_at: "2026-01-01T00:00:00Z",
message: {
role: "assistant" as const,
content: "",
tool_calls: [{ function: { name: "get_weather", arguments: '{"location": "Beijing"}' } }],
},
done: true,
prompt_eval_count: 20,
eval_count: 10,
};
const result = buildAssistantMessage(response, modelInfo);
const toolCall = result.content[0] as {
type: "toolCall";
name: string;
arguments: Record<string, unknown>;
};
expect(toolCall.name).toBe("get_weather");
expect(toolCall.arguments).toEqual({ location: "Beijing" });
});
it("sets all costs to zero for local models", () => {
const response = {
model: "qwen3:32b",
@ -261,7 +355,7 @@ describe("parseNdjsonStream", () => {
// Simulate the accumulation logic from createOllamaStreamFn
const accumulatedToolCalls: Array<{
function: { name: string; arguments: Record<string, unknown> };
function: { name: string; arguments: Record<string, unknown> | string };
}> = [];
const chunks = [];
for await (const chunk of parseNdjsonStream(reader)) {

View File

@ -66,7 +66,8 @@ interface OllamaTool {
interface OllamaToolCall {
function: {
name: string;
arguments: Record<string, unknown>;
/** Wire format may be a JSON string; normalize before re-sending to Ollama. */
arguments: Record<string, unknown> | string;
};
}
@ -221,8 +222,39 @@ interface OllamaChatResponse {
type InputContentPart =
| { type: "text"; text: string }
| { type: "image"; data: string }
| { type: "toolCall"; id: string; name: string; arguments: Record<string, unknown> }
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> };
| {
type: "toolCall";
id: string;
name: string;
arguments: Record<string, unknown> | string;
}
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> | string };
/**
* Normalize tool `arguments` / `input` for Ollama: OpenAI-style messages use a JSON string;
* Ollama expects an object. Uses the same unsafe-integer quoting as NDJSON parsing.
*/
function normalizeOllamaToolArguments(raw: unknown): Record<string, unknown> {
if (typeof raw === "string") {
const trimmed = raw.trim();
if (!trimmed) {
return {};
}
try {
const parsed = parseJsonPreservingUnsafeIntegers(trimmed);
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
}
return {};
} catch {
return {};
}
}
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
return raw as Record<string, unknown>;
}
return {};
}
function extractTextContent(content: unknown): string {
if (typeof content === "string") {
@ -254,9 +286,13 @@ function extractToolCalls(content: unknown): OllamaToolCall[] {
const result: OllamaToolCall[] = [];
for (const part of parts) {
if (part.type === "toolCall") {
result.push({ function: { name: part.name, arguments: part.arguments } });
result.push({
function: { name: part.name, arguments: normalizeOllamaToolArguments(part.arguments) },
});
} else if (part.type === "tool_use") {
result.push({ function: { name: part.name, arguments: part.input } });
result.push({
function: { name: part.name, arguments: normalizeOllamaToolArguments(part.input) },
});
}
}
return result;
@ -355,7 +391,7 @@ export function buildAssistantMessage(
type: "toolCall",
id: `ollama_call_${randomUUID()}`,
name: tc.function.name,
arguments: tc.function.arguments,
arguments: normalizeOllamaToolArguments(tc.function.arguments),
});
}
}