fix: harden ollama tool-call replay (#52253) (thanks @Adam-Researchh)

This commit is contained in:
Peter Steinberger
2026-04-03 20:45:53 +09:00
parent e116e7d584
commit 87abcfd6a6
5 changed files with 408 additions and 128 deletions

View File

@@ -73,6 +73,70 @@ describe("convertToOllamaMessages", () => {
]);
});
it("deserializes string arguments back to objects for Ollama (round-trip fix)", () => {
// When tool calls round-trip through OpenAI-format storage, arguments
// are serialized as a JSON string. Ollama expects an object.
const messages = [
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_2",
name: "Read",
arguments: '{"file_path":"/tmp/test.txt"}',
},
],
},
];
const result = convertToOllamaMessages(messages);
expect(result[0].tool_calls).toEqual([
{ function: { name: "Read", arguments: { file_path: "/tmp/test.txt" } } },
]);
});
it("handles tool_use blocks with string input (Anthropic format round-trip)", () => {
const messages = [
{
role: "assistant",
content: [
{ type: "tool_use", id: "toolu_1", name: "exec", input: '{"command":"echo hello"}' },
],
},
];
const result = convertToOllamaMessages(messages);
expect(result[0].tool_calls).toEqual([
{ function: { name: "exec", arguments: { command: "echo hello" } } },
]);
});
it("preserves unsafe integers as strings when replay args are deserialized", () => {
const messages = [
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_3",
name: "read",
arguments: '{"path":9223372036854775807,"nested":{"thread":1234567890123456789}}',
},
],
},
];
const result = convertToOllamaMessages(messages);
expect(result[0].tool_calls).toEqual([
{
function: {
name: "read",
arguments: {
path: "9223372036854775807",
nested: { thread: "1234567890123456789" },
},
},
},
]);
});
it("converts tool result messages with 'tool' role", () => {
const messages = [{ role: "tool", content: "file1.txt\nfile2.txt" }];
const result = convertToOllamaMessages(messages);

View File

@@ -1728,6 +1728,151 @@ describe("wrapOllamaCompatNumCtx", () => {
expect((payloadSeen?.options as Record<string, unknown> | undefined)?.num_ctx).toBe(202752);
expect(downstream).toHaveBeenCalledTimes(1);
});
it("deserializes assistant tool_call arguments for Ollama OpenAI-compatible payloads", () => {
let payloadSeen: Record<string, unknown> | undefined;
const baseFn = vi.fn((_model, _context, options) => {
const payload: Record<string, unknown> = {
messages: [
{
role: "assistant",
tool_calls: [
{
id: "call_1",
type: "function",
function: {
name: "read",
arguments: '{"path":"/tmp/test.txt"}',
},
},
],
},
],
};
options?.onPayload?.(payload, _model);
payloadSeen = payload;
return {} as never;
});
const wrapped = wrapOllamaCompatNumCtx(baseFn as never, 8192);
void wrapped({} as never, {} as never, undefined as never);
const messageRecord = (
payloadSeen?.messages as Array<Record<string, unknown>> | undefined
)?.[0];
const toolCall = (messageRecord?.tool_calls as Array<Record<string, unknown>> | undefined)?.[0];
expect(toolCall?.function).toEqual({
name: "read",
arguments: { path: "/tmp/test.txt" },
});
});
it("deserializes assistant function_call arguments for Ollama OpenAI-compatible payloads", () => {
let payloadSeen: Record<string, unknown> | undefined;
const baseFn = vi.fn((_model, _context, options) => {
const payload: Record<string, unknown> = {
messages: [
{
role: "assistant",
function_call: {
name: "exec",
arguments: '{"command":"pwd"}',
},
},
],
};
options?.onPayload?.(payload, _model);
payloadSeen = payload;
return {} as never;
});
const wrapped = wrapOllamaCompatNumCtx(baseFn as never, 8192);
void wrapped({} as never, {} as never, undefined as never);
const messageRecord = (
payloadSeen?.messages as Array<Record<string, unknown>> | undefined
)?.[0];
expect(messageRecord?.function_call).toEqual({
name: "exec",
arguments: { command: "pwd" },
});
});
it("preserves unsafe integers when deserializing assistant tool_call arguments", () => {
let payloadSeen: Record<string, unknown> | undefined;
const baseFn = vi.fn((_model, _context, options) => {
const payload: Record<string, unknown> = {
messages: [
{
role: "assistant",
tool_calls: [
{
id: "call_1",
type: "function",
function: {
name: "read",
arguments: '{"path":9223372036854775807,"nested":{"thread":1234567890123456789}}',
},
},
],
},
],
};
options?.onPayload?.(payload, _model);
payloadSeen = payload;
return {} as never;
});
const wrapped = wrapOllamaCompatNumCtx(baseFn as never, 8192);
void wrapped({} as never, {} as never, undefined as never);
const messageRecord = (
payloadSeen?.messages as Array<Record<string, unknown>> | undefined
)?.[0];
const toolCall = (messageRecord?.tool_calls as Array<Record<string, unknown>> | undefined)?.[0];
expect(toolCall?.function).toEqual({
name: "read",
arguments: {
path: "9223372036854775807",
nested: { thread: "1234567890123456789" },
},
});
});
it("preserves unsafe integers when deserializing assistant function_call arguments", () => {
let payloadSeen: Record<string, unknown> | undefined;
const baseFn = vi.fn((_model, _context, options) => {
const payload: Record<string, unknown> = {
messages: [
{
role: "assistant",
function_call: {
name: "exec",
arguments: '{"thread":9223372036854775807}',
},
},
],
};
options?.onPayload?.(payload, _model);
payloadSeen = payload;
return {} as never;
});
const wrapped = wrapOllamaCompatNumCtx(baseFn as never, 8192);
void wrapped({} as never, {} as never, undefined as never);
const messageRecord = (
payloadSeen?.messages as Array<Record<string, unknown>> | undefined
)?.[0];
expect(messageRecord?.function_call).toEqual({
name: "exec",
arguments: { thread: "9223372036854775807" },
});
});
});
describe("resolveOllamaCompatNumCtxEnabled", () => {