@@ -728,14 +728,63 @@ func dedupeInputItemsByID(rawArray string) (string, error) {
728728 return "" , errUnmarshal
729729 }
730730
731- lastIndexByID := make (map [string ]int , len (items ))
731+ // Parse each item's type, id and call_id once; gjson is a scan-based
732+ // parser, so reusing this metadata avoids rescanning every item in each of
733+ // the loops below as the conversation history grows.
734+ type itemMetadata struct {
735+ itemType string
736+ id string
737+ callID string
738+ }
739+ meta := make ([]itemMetadata , len (items ))
732740 for i , item := range items {
733741 if len (item ) == 0 {
734742 continue
735743 }
736- itemID := strings .TrimSpace (gjson .GetBytes (item , "id" ).String ())
737- if itemID != "" {
738- lastIndexByID [itemID ] = i
744+ res := gjson .GetManyBytes (item , "type" , "id" , "call_id" )
745+ meta [i ] = itemMetadata {
746+ itemType : strings .TrimSpace (res [0 ].String ()),
747+ id : strings .TrimSpace (res [1 ].String ()),
748+ callID : strings .TrimSpace (res [2 ].String ()),
749+ }
750+ }
751+
752+ // Collect the call_ids that are still referenced by tool-call output
753+ // items. When several input items share the same id, the one we keep must
754+ // preserve any call_id that has a matching output; otherwise the upstream
755+ // rejects the request with "No tool call found for function call output".
756+ referencedCallIDs := make (map [string ]struct {}, len (items ))
757+ for i := range items {
758+ switch meta [i ].itemType {
759+ case "function_call_output" , "custom_tool_call_output" :
760+ if meta [i ].callID != "" {
761+ referencedCallIDs [meta [i ].callID ] = struct {}{}
762+ }
763+ }
764+ }
765+
766+ // For each id, choose the index to keep. The default is the last
767+ // occurrence (matching the original dedupe behavior), but we never replace
768+ // an item whose call_id still has a matching output with one that does not.
769+ // This keeps a single item per id while ensuring retained tool calls stay
770+ // paired with their outputs.
771+ keepIndexByID := make (map [string ]int , len (items ))
772+ keepReferencedByID := make (map [string ]bool , len (items ))
773+ for i := range items {
774+ itemID := meta [i ].id
775+ if itemID == "" {
776+ continue
777+ }
778+ _ , referenced := referencedCallIDs [meta [i ].callID ]
779+ referenced = referenced && meta [i ].callID != ""
780+ if _ , seen := keepIndexByID [itemID ]; ! seen {
781+ keepIndexByID [itemID ] = i
782+ keepReferencedByID [itemID ] = referenced
783+ continue
784+ }
785+ if referenced || ! keepReferencedByID [itemID ] {
786+ keepIndexByID [itemID ] = i
787+ keepReferencedByID [itemID ] = referenced
739788 }
740789 }
741790
@@ -744,9 +793,9 @@ func dedupeInputItemsByID(rawArray string) (string, error) {
744793 if len (item ) == 0 {
745794 continue
746795 }
747- itemID := strings . TrimSpace ( gjson . GetBytes ( item , "id" ). String ())
796+ itemID := meta [ i ]. id
748797 if itemID != "" {
749- if lastIndexByID [itemID ] != i {
798+ if keepIndexByID [itemID ] != i {
750799 continue
751800 }
752801 }
0 commit comments