11import crypto from "node:crypto" ;
2+ import { toolArgumentSuffix } from "./tool-arguments.js" ;
23import type {
34 AnthropicRequest ,
45 AnthropicContentBlock ,
@@ -245,8 +246,12 @@ export function toCCRequest(
245246export class AnthropicStreamEncoder {
246247 readonly messageId : string ;
247248 private blockIndex = 0 ;
249+ private currentBlockIndex = 0 ;
248250 private currentBlockType : "text" | "thinking" | "tool_use" | null = null ;
249- private currentToolCallId : string | null = null ;
251+ private readonly toolBlocks = new Map <
252+ string ,
253+ { index : number ; arguments : string ; closed : boolean }
254+ > ( ) ;
250255 private pendingStart : CCEvent | null = null ;
251256 private started = false ;
252257 private pinged = false ;
@@ -281,6 +286,7 @@ export class AnthropicStreamEncoder {
281286 records . push ( this . makeMessageStart ( 0 ) ) ;
282287 }
283288 this . closeCurrentBlock ( records ) ;
289+ this . closeToolBlocks ( records ) ;
284290 records . push ( {
285291 event : "error" ,
286292 data : { type : "error" , error : { type : "api_error" , message : msg } } ,
@@ -328,6 +334,7 @@ export class AnthropicStreamEncoder {
328334 }
329335
330336 this . closeCurrentBlock ( records ) ;
337+ this . closeToolBlocks ( records ) ;
331338
332339 const finishReason = ( event . data . finishReason as string ) ?? "stop" ;
333340 const usage = extractUsage ( event . data as Record < string , unknown > ) ;
@@ -381,21 +388,18 @@ export class AnthropicStreamEncoder {
381388 case "tool-call-delta" : {
382389 const tcId = ( event . data . toolCallId as string ) ?? "" ;
383390 const tcName = ( event . data . name as string ) ?? "" ;
384- if ( this . currentBlockType !== "tool_use" || this . currentToolCallId !== tcId ) {
385- this . closeCurrentBlock ( records ) ;
386- this . ensureBlockOpenWith ( records , "tool_use" , {
387- type : "tool_use" ,
388- id : tcId ,
389- name : tcName ,
390- input : { } ,
391- } ) ;
392- this . currentToolCallId = tcId ;
393- }
391+ const block = this . ensureToolBlock ( records , tcId , tcName ) ;
392+ if ( block . closed ) throw new Error ( "Inconsistent upstream tool arguments" ) ;
393+ const args = ( event . data . arguments as string ) ?? "" ;
394+ block . arguments += args ;
394395 records . push (
395- this . makeDelta ( {
396- type : "input_json_delta" ,
397- partial_json : ( event . data . arguments as string ) ?? "" ,
398- } ) ,
396+ this . makeDelta (
397+ {
398+ type : "input_json_delta" ,
399+ partial_json : args ,
400+ } ,
401+ block . index ,
402+ ) ,
399403 ) ;
400404 break ;
401405 }
@@ -406,33 +410,51 @@ export class AnthropicStreamEncoder {
406410 const input = event . data . input ?? event . data . arguments ;
407411 const argsStr =
408412 typeof input === "string" ? input : input != null ? JSON . stringify ( input ) : "" ;
409- // If the upstream streamed deltas for this tool call first and then
410- // sent the final `tool-call` event with the same id, reuse the block
411- // it already opened instead of creating a duplicate `tool_use`.
412- if ( this . currentBlockType === "tool_use" && this . currentToolCallId === tcId ) {
413- if ( argsStr ) {
414- records . push ( this . makeDelta ( { type : "input_json_delta" , partial_json : argsStr } ) ) ;
415- }
416- break ;
417- }
418- this . closeCurrentBlock ( records ) ;
419- this . ensureBlockOpenWith ( records , "tool_use" , {
420- type : "tool_use" ,
421- id : tcId ,
422- name : tcName ,
423- input : { } ,
424- } ) ;
425- if ( argsStr ) {
426- records . push ( this . makeDelta ( { type : "input_json_delta" , partial_json : argsStr } ) ) ;
413+ const block = this . ensureToolBlock ( records , tcId , tcName ) ;
414+ const suffix = toolArgumentSuffix ( block . arguments , argsStr ) ;
415+ if ( suffix ) {
416+ if ( block . closed ) throw new Error ( "Inconsistent upstream tool arguments" ) ;
417+ records . push (
418+ this . makeDelta ( { type : "input_json_delta" , partial_json : suffix } , block . index ) ,
419+ ) ;
420+ block . arguments += suffix ;
427421 }
428- this . closeCurrentBlock ( records ) ;
422+ this . closeToolBlock ( records , block ) ;
429423 break ;
430424 }
431425 }
432426
433427 return records ;
434428 }
435429
430+ private ensureToolBlock ( records : AnthropicSSERecord [ ] , id : string , name : string ) {
431+ const existing = this . toolBlocks . get ( id ) ;
432+ if ( existing ) return existing ;
433+ this . closeCurrentBlock ( records ) ;
434+ this . ensureBlockOpenWith ( records , "tool_use" , { type : "tool_use" , id, name, input : { } } ) ;
435+ const block = { index : this . currentBlockIndex , arguments : "" , closed : false } ;
436+ this . toolBlocks . set ( id , block ) ;
437+ // Tool blocks have independent lifetimes: interleaved deltas retain indices.
438+ this . currentBlockType = null ;
439+ return block ;
440+ }
441+
442+ private closeToolBlock (
443+ records : AnthropicSSERecord [ ] ,
444+ block : { index : number ; closed : boolean } ,
445+ ) : void {
446+ if ( block . closed ) return ;
447+ records . push ( {
448+ event : "content_block_stop" ,
449+ data : { type : "content_block_stop" , index : block . index } ,
450+ } ) ;
451+ block . closed = true ;
452+ }
453+
454+ private closeToolBlocks ( records : AnthropicSSERecord [ ] ) : void {
455+ for ( const block of this . toolBlocks . values ( ) ) this . closeToolBlock ( records , block ) ;
456+ }
457+
436458 private ensureBlockOpen (
437459 records : AnthropicSSERecord [ ] ,
438460 type : "text" | "thinking" | "tool_use" ,
@@ -450,11 +472,12 @@ export class AnthropicStreamEncoder {
450472 block : ContentBlockStartShape ,
451473 ) : void {
452474 this . currentBlockType = type ;
475+ this . currentBlockIndex = this . blockIndex ++ ;
453476 records . push ( {
454477 event : "content_block_start" ,
455478 data : {
456479 type : "content_block_start" ,
457- index : this . blockIndex ,
480+ index : this . currentBlockIndex ,
458481 content_block : block ,
459482 } ,
460483 } ) ;
@@ -477,17 +500,16 @@ export class AnthropicStreamEncoder {
477500
478501 records . push ( {
479502 event : "content_block_stop" ,
480- data : { type : "content_block_stop" , index : this . blockIndex } ,
503+ data : { type : "content_block_stop" , index : this . currentBlockIndex } ,
481504 } ) ;
482505
483- this . blockIndex ++ ;
484506 this . currentBlockType = null ;
485507 }
486508
487- private makeDelta ( delta : DeltaShape ) : AnthropicSSERecord {
509+ private makeDelta ( delta : DeltaShape , index = this . currentBlockIndex ) : AnthropicSSERecord {
488510 return {
489511 event : "content_block_delta" ,
490- data : { type : "content_block_delta" , index : this . blockIndex , delta } ,
512+ data : { type : "content_block_delta" , index, delta } ,
491513 } ;
492514 }
493515
@@ -529,6 +551,7 @@ export class AnthropicStreamEncoder {
529551 this . started = true ;
530552 }
531553 this . closeCurrentBlock ( records ) ;
554+ this . closeToolBlocks ( records ) ;
532555 records . push ( {
533556 event : "message_delta" ,
534557 data : {
@@ -555,6 +578,9 @@ export function buildAnthropicResponse(
555578
556579 for ( const event of events ) {
557580 switch ( event . type ) {
581+ case "error" :
582+ // Do not turn failed generations (or private diagnostics) into content.
583+ throw new Error ( "CC upstream generation failed" ) ;
558584 case "text-delta" :
559585 textContent += ( event . data . text as string ) ?? "" ;
560586 break ;
0 commit comments