Merge b0ca2112ceca035f89f09c2b46e6f262c8386094 into 9fb78453e088cd7b553d7779faa0de5c83708e70
This commit is contained in:
commit
f1f33a1a63
@ -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)) {
|
||||
|
||||
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user