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

Commit 33fd972

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 fd37472 commit 33fd972

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
@@ -581,9 +581,10 @@ describe("AI SDK conversion utilities", () => {
581581
const startChunks = [
582582
...processor({ type: "tool-input-start" as const, id: "call_1", toolName: "read_file" }),
583583
]
584+
// End is deferred since we haven't received deltas yet
584585
const endChunks = [...processor({ type: "tool-input-end" as const, id: "call_1" })]
585586

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

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

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

611616
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
@@ -390,6 +390,8 @@ export function createAiSdkToolStreamProcessor(): (
390390
const startedToolIds = new Set<string>()
391391
// Track tool IDs that have received actual argument deltas
392392
const toolsWithDeltas = new Set<string>()
393+
// Track tool IDs that have ended but are waiting for arguments from tool-call
394+
const pendingEndToolIds = new Set<string>()
393395

394396
return function* processStreamPart(part: ExtendedStreamPart): Generator<ApiStreamChunk> {
395397
switch (part.type) {
@@ -414,9 +416,16 @@ export function createAiSdkToolStreamProcessor(): (
414416
break
415417

416418
case "tool-input-end":
417-
yield {
418-
type: "tool_call_end",
419-
id: part.id,
419+
// If we already have deltas, we can emit the end now
420+
// Otherwise, defer the end until we get arguments from tool-call
421+
if (toolsWithDeltas.has(part.id)) {
422+
yield {
423+
type: "tool_call_end",
424+
id: part.id,
425+
}
426+
} else {
427+
// HuggingFace case: started but no deltas, arguments will come in tool-call
428+
pendingEndToolIds.add(part.id)
420429
}
421430
break
422431

@@ -435,14 +444,22 @@ export function createAiSdkToolStreamProcessor(): (
435444
}
436445

437446
// If tool was started but no deltas received (like HuggingFace),
438-
// emit the arguments from tool-call as a delta
447+
// emit the arguments from tool-call as a delta, then the pending end
439448
if (startedToolIds.has(toolCallPart.toolCallId)) {
440449
const args = JSON.stringify(toolCallPart.input)
441450
yield {
442451
type: "tool_call_delta",
443452
id: toolCallPart.toolCallId,
444453
delta: args,
445454
}
455+
// Now emit the deferred end
456+
if (pendingEndToolIds.has(toolCallPart.toolCallId)) {
457+
pendingEndToolIds.delete(toolCallPart.toolCallId)
458+
yield {
459+
type: "tool_call_end",
460+
id: toolCallPart.toolCallId,
461+
}
462+
}
446463
break
447464
}
448465

0 commit comments

Comments
 (0)