Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 180e8b2

Browse files
committed
fix(huggingface): defer tool_call_end until arguments received
The sequence must be start → delta → end. For HuggingFace: - tool-input-start emits tool_call_start - tool-input-end is DEFERRED (no deltas yet, args not received) - tool-call emits tool_call_delta + deferred tool_call_end This ensures arguments appear before the end event.
1 parent 4afcd5e commit 180e8b2

2 files changed

Lines changed: 31 additions & 9 deletions

File tree

src/api/transform/__tests__/ai-sdk.spec.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -580,9 +580,10 @@ describe("AI SDK conversion utilities", () => {
580580
const startChunks = [
581581
...processor({ type: "tool-input-start" as const, id: "call_1", toolName: "read_file" }),
582582
]
583+
// End is deferred since we haven't received deltas yet
583584
const endChunks = [...processor({ type: "tool-input-end" as const, id: "call_1" })]
584585

585-
// tool-call should emit just the delta (arguments) since start/end were already emitted
586+
// tool-call should emit the delta (arguments) AND the deferred end
586587
const toolCallChunks = [
587588
...processor({
588589
type: "tool-call" as const,
@@ -595,16 +596,20 @@ describe("AI SDK conversion utilities", () => {
595596
expect(startChunks).toHaveLength(1)
596597
expect(startChunks[0]).toEqual({ type: "tool_call_start", id: "call_1", name: "read_file" })
597598

598-
expect(endChunks).toHaveLength(1)
599-
expect(endChunks[0]).toEqual({ type: "tool_call_end", id: "call_1" })
599+
// End is deferred when no deltas received
600+
expect(endChunks).toHaveLength(0)
600601

601-
// The tool-call should emit just the delta with arguments
602-
expect(toolCallChunks).toHaveLength(1)
602+
// tool-call emits delta followed by the deferred end
603+
expect(toolCallChunks).toHaveLength(2)
603604
expect(toolCallChunks[0]).toEqual({
604605
type: "tool_call_delta",
605606
id: "call_1",
606607
delta: '{"path":"test.ts"}',
607608
})
609+
expect(toolCallChunks[1]).toEqual({
610+
type: "tool_call_end",
611+
id: "call_1",
612+
})
608613
})
609614

610615
it("handles multiple tool calls correctly", () => {

src/api/transform/ai-sdk.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ export function createAiSdkToolStreamProcessor(): (
300300
const startedToolIds = new Set<string>()
301301
// Track tool IDs that have received actual argument deltas
302302
const toolsWithDeltas = new Set<string>()
303+
// Track tool IDs that have ended but are waiting for arguments from tool-call
304+
const pendingEndToolIds = new Set<string>()
303305

304306
return function* processStreamPart(part: ExtendedStreamPart): Generator<ApiStreamChunk> {
305307
switch (part.type) {
@@ -324,9 +326,16 @@ export function createAiSdkToolStreamProcessor(): (
324326
break
325327

326328
case "tool-input-end":
327-
yield {
328-
type: "tool_call_end",
329-
id: part.id,
329+
// If we already have deltas, we can emit the end now
330+
// Otherwise, defer the end until we get arguments from tool-call
331+
if (toolsWithDeltas.has(part.id)) {
332+
yield {
333+
type: "tool_call_end",
334+
id: part.id,
335+
}
336+
} else {
337+
// HuggingFace case: started but no deltas, arguments will come in tool-call
338+
pendingEndToolIds.add(part.id)
330339
}
331340
break
332341

@@ -345,14 +354,22 @@ export function createAiSdkToolStreamProcessor(): (
345354
}
346355

347356
// If tool was started but no deltas received (like HuggingFace),
348-
// emit the arguments from tool-call as a delta
357+
// emit the arguments from tool-call as a delta, then the pending end
349358
if (startedToolIds.has(toolCallPart.toolCallId)) {
350359
const args = JSON.stringify(toolCallPart.input)
351360
yield {
352361
type: "tool_call_delta",
353362
id: toolCallPart.toolCallId,
354363
delta: args,
355364
}
365+
// Now emit the deferred end
366+
if (pendingEndToolIds.has(toolCallPart.toolCallId)) {
367+
pendingEndToolIds.delete(toolCallPart.toolCallId)
368+
yield {
369+
type: "tool_call_end",
370+
id: toolCallPart.toolCallId,
371+
}
372+
}
356373
break
357374
}
358375

0 commit comments

Comments
 (0)