mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
This commit is contained in:
committed by
GitHub
parent
adb1fc5b27
commit
0a358c02ab
1
changelog.d/fixes/6276-toolcall-args-logs.md
Normal file
1
changelog.d/fixes/6276-toolcall-args-logs.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276)
|
||||
@@ -129,13 +129,29 @@ function buildOpenAISummary(events: StructuredSSEEvent[], fallbackModel?: string
|
||||
function: { name: string; arguments: string };
|
||||
};
|
||||
const toolCalls = new Map<string, ToolCall>();
|
||||
// Aliases every `idx:N` key we've seen to the `id:X` it was first observed with (and
|
||||
// vice versa), so a later delta chunk that only carries one of the two dimensions
|
||||
// (e.g. a continuation chunk with `id` but no `index` — a known quirk of some
|
||||
// OpenAI-compatible proxies) still resolves to the SAME accumulator entry instead of
|
||||
// splitting one logical tool call into two (#6276).
|
||||
const keyAliases = new Map<string, string>();
|
||||
let unknownToolCallSeq = 0;
|
||||
let finishReason = "stop";
|
||||
let usage: JsonRecord | null = null;
|
||||
|
||||
const getToolCallKey = (toolCall: JsonRecord) => {
|
||||
if (Number.isInteger(toolCall.index)) return `idx:${toolCall.index}`;
|
||||
if (toolCall.id) return `id:${toolCall.id}`;
|
||||
const idKey = typeof toolCall.id === "string" && toolCall.id ? `id:${toolCall.id}` : null;
|
||||
const idxKey = Number.isInteger(toolCall.index) ? `idx:${toolCall.index}` : null;
|
||||
|
||||
const resolvedKey = (idKey && keyAliases.get(idKey)) || (idxKey && keyAliases.get(idxKey));
|
||||
const key = resolvedKey || idKey || idxKey;
|
||||
|
||||
if (key) {
|
||||
if (idKey) keyAliases.set(idKey, key);
|
||||
if (idxKey) keyAliases.set(idxKey, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
unknownToolCallSeq += 1;
|
||||
return `seq:${unknownToolCallSeq}`;
|
||||
};
|
||||
|
||||
@@ -75,3 +75,143 @@ test("createStructuredSSECollector collector has expected methods", () => {
|
||||
const keys = Object.keys(c);
|
||||
assert.ok(keys.length > 0);
|
||||
});
|
||||
|
||||
// #6276 — tool_call arguments lost in request/response logs when a continuation
|
||||
// delta omits `index` (some OpenAI-compatible proxies only send `index` on the
|
||||
// FIRST tool_call delta chunk, then only `id` on subsequent chunks).
|
||||
|
||||
type ToolCallSummary = {
|
||||
choices: Array<{
|
||||
message: {
|
||||
tool_calls: Array<{ function: { name: string; arguments: string } }>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
function toolCallEvent(delta: Record<string, unknown>, finishReason?: string) {
|
||||
return {
|
||||
index: 0,
|
||||
data: {
|
||||
id: "chatcmpl-1",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "deepseek-v4-flash-free",
|
||||
choices: [{ index: 0, delta, ...(finishReason ? { finish_reason: finishReason } : {}) }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("buildStreamSummaryFromEvents merges tool_call deltas when every chunk carries `index` (happy path)", () => {
|
||||
const events = [
|
||||
toolCallEvent({
|
||||
role: "assistant",
|
||||
tool_calls: [
|
||||
{ index: 0, id: "call_a", type: "function", function: { name: "Bash", arguments: "" } },
|
||||
],
|
||||
}),
|
||||
toolCallEvent({
|
||||
tool_calls: [{ index: 0, id: "call_a", type: "function", function: { arguments: '{"x":1}' } }],
|
||||
}),
|
||||
toolCallEvent({}, "tool_calls"),
|
||||
];
|
||||
|
||||
const summary = collector.buildStreamSummaryFromEvents(
|
||||
events,
|
||||
"openai",
|
||||
"deepseek-v4-flash-free"
|
||||
) as ToolCallSummary;
|
||||
const toolCalls = summary.choices[0].message.tool_calls;
|
||||
|
||||
assert.equal(toolCalls.length, 1);
|
||||
assert.equal(toolCalls[0].function.name, "Bash");
|
||||
assert.equal(toolCalls[0].function.arguments, '{"x":1}');
|
||||
});
|
||||
|
||||
test("buildStreamSummaryFromEvents merges a continuation delta that carries only `id` (no `index`) into the initiating tool_call (#6276)", () => {
|
||||
const events = [
|
||||
toolCallEvent({
|
||||
role: "assistant",
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "call_00_xasdOvEWoeldzXAqFPQP2849",
|
||||
type: "function",
|
||||
function: { name: "Bash", arguments: "" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
// Continuation chunk omits `index`, carries only `id` + arguments fragment.
|
||||
toolCallEvent({
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_00_xasdOvEWoeldzXAqFPQP2849",
|
||||
type: "function",
|
||||
function: { arguments: '{"command": "date' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
toolCallEvent({
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_00_xasdOvEWoeldzXAqFPQP2849",
|
||||
type: "function",
|
||||
function: { arguments: '"}' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
toolCallEvent({}, "tool_calls"),
|
||||
];
|
||||
|
||||
const summary = collector.buildStreamSummaryFromEvents(
|
||||
events,
|
||||
"openai",
|
||||
"deepseek-v4-flash-free"
|
||||
) as ToolCallSummary;
|
||||
const toolCalls = summary.choices[0].message.tool_calls;
|
||||
|
||||
assert.equal(
|
||||
toolCalls.length,
|
||||
1,
|
||||
`expected 1 tool_call, got ${toolCalls.length}: ${JSON.stringify(toolCalls)}`
|
||||
);
|
||||
assert.equal(toolCalls[0].function.name, "Bash");
|
||||
assert.equal(toolCalls[0].function.arguments, '{"command": "date"}');
|
||||
});
|
||||
|
||||
test("buildStreamSummaryFromEvents keeps two genuinely different interleaved tool_calls separate", () => {
|
||||
const events = [
|
||||
toolCallEvent({
|
||||
role: "assistant",
|
||||
tool_calls: [
|
||||
{ index: 0, id: "call_a", type: "function", function: { name: "Bash", arguments: "" } },
|
||||
{ index: 1, id: "call_b", type: "function", function: { name: "Read", arguments: "" } },
|
||||
],
|
||||
}),
|
||||
toolCallEvent({
|
||||
tool_calls: [
|
||||
{ index: 0, id: "call_a", type: "function", function: { arguments: '{"cmd":"a"' } },
|
||||
{ index: 1, id: "call_b", type: "function", function: { arguments: '{"path":"b"' } },
|
||||
],
|
||||
}),
|
||||
toolCallEvent({
|
||||
tool_calls: [
|
||||
{ index: 0, id: "call_a", type: "function", function: { arguments: "}" } },
|
||||
{ index: 1, id: "call_b", type: "function", function: { arguments: "}" } },
|
||||
],
|
||||
}),
|
||||
toolCallEvent({}, "tool_calls"),
|
||||
];
|
||||
|
||||
const summary = collector.buildStreamSummaryFromEvents(
|
||||
events,
|
||||
"openai",
|
||||
"deepseek-v4-flash-free"
|
||||
) as ToolCallSummary;
|
||||
const toolCalls = summary.choices[0].message.tool_calls;
|
||||
|
||||
assert.equal(toolCalls.length, 2);
|
||||
assert.equal(toolCalls[0].function.name, "Bash");
|
||||
assert.equal(toolCalls[0].function.arguments, '{"cmd":"a"}');
|
||||
assert.equal(toolCalls[1].function.name, "Read");
|
||||
assert.equal(toolCalls[1].function.arguments, '{"path":"b"}');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user