@@ -271,15 +271,6 @@ func (c *Client) CreateChatCompletionStream(
271271 slog .Error ("Failed to convert messages for Anthropic request" , "error" , err )
272272 return nil , err
273273 }
274- // Preflight validation to ensure tool_use/tool_result sequencing is valid
275- if err := validateAnthropicSequencing (converted ); err != nil {
276- slog .Warn ("Invalid message sequencing for Anthropic detected, attempting self-repair" , "error" , err )
277- converted = repairAnthropicSequencing (converted )
278- if err2 := validateAnthropicSequencing (converted ); err2 != nil {
279- slog .Error ("Failed to self-repair Anthropic sequencing" , "error" , err2 )
280- return nil , err
281- }
282- }
283274 if len (converted ) == 0 {
284275 return nil , errors .New ("no messages to send after conversion: all messages were filtered out" )
285276 }
@@ -735,27 +726,6 @@ func (c *Client) FileManager() *FileManager {
735726 return c .fileManager
736727}
737728
738- // validateAnthropicSequencing verifies that for every assistant message that includes
739- // one or more tool_use blocks, the immediately following message is a user message
740- // that includes tool_result blocks for all those tool_use IDs (grouped into that single message).
741- func validateAnthropicSequencing (msgs []anthropic.MessageParam ) error {
742- return validateSequencing (msgs )
743- }
744-
745- // repairAnthropicSequencing inserts a synthetic user message containing tool_result blocks
746- // immediately after any assistant message that has tool_use blocks missing a corresponding
747- // tool_result in the next user message. This is a best-effort local repair to keep the
748- // conversation valid for Anthropic while preserving original messages, to keep the agent loop running.
749- func repairAnthropicSequencing (msgs []anthropic.MessageParam ) []anthropic.MessageParam {
750- return repairSequencing (msgs , func (toolUseIDs map [string ]struct {}) anthropic.MessageParam {
751- blocks := make ([]anthropic.ContentBlockParamUnion , 0 , len (toolUseIDs ))
752- for id := range toolUseIDs {
753- blocks = append (blocks , anthropic .NewToolResultBlock (id , "(tool execution failed)" , false ))
754- }
755- return anthropic .NewUserMessage (blocks ... )
756- })
757- }
758-
759729// marshalToMap is a helper that converts any value to a map[string]any via JSON marshaling.
760730// This is used to inspect SDK union types without depending on their internal structure.
761731// It's shared by both standard and Beta API validation/repair code.
@@ -780,125 +750,6 @@ func contentArray(m map[string]any) []any {
780750 return nil
781751}
782752
783- // validateSequencing generically validates that every assistant message with tool_use blocks
784- // is immediately followed by a user message with corresponding tool_result blocks.
785- // It works on both standard (MessageParam) and Beta (BetaMessageParam) types by
786- // marshaling to map[string]any for inspection.
787- func validateSequencing [T any ](msgs []T ) error {
788- for i := range msgs {
789- m , ok := marshalToMap (msgs [i ])
790- if ! ok || m ["role" ] != "assistant" {
791- continue
792- }
793-
794- toolUseIDs := collectToolUseIDs (contentArray (m ))
795- if len (toolUseIDs ) == 0 {
796- continue
797- }
798-
799- if i + 1 >= len (msgs ) {
800- slog .Warn ("Anthropic sequencing invalid: assistant tool_use present but no next user tool_result message" , "assistant_index" , i )
801- return errors .New ("assistant tool_use present but no subsequent user message with tool_result blocks" )
802- }
803-
804- next , ok := marshalToMap (msgs [i + 1 ])
805- if ! ok || next ["role" ] != "user" {
806- slog .Warn ("Anthropic sequencing invalid: next message after assistant tool_use is not user" , "assistant_index" , i , "next_role" , next ["role" ])
807- return errors .New ("assistant tool_use must be followed by a user message containing corresponding tool_result blocks" )
808- }
809-
810- toolResultIDs := collectToolResultIDs (contentArray (next ))
811- missing := differenceIDs (toolUseIDs , toolResultIDs )
812- if len (missing ) > 0 {
813- slog .Warn ("Anthropic sequencing invalid: missing tool_result for tool_use id in next user message" , "assistant_index" , i , "tool_use_id" , missing [0 ], "missing_count" , len (missing ))
814- return fmt .Errorf ("missing tool_result for tool_use id %s in the next user message" , missing [0 ])
815- }
816- }
817- return nil
818- }
819-
820- // repairSequencing generically inserts a synthetic user message after any assistant
821- // tool_use message that is missing corresponding tool_result blocks. The makeSynthetic
822- // callback builds the appropriate user message type for the remaining tool_use IDs.
823- func repairSequencing [T any ](msgs []T , makeSynthetic func (toolUseIDs map [string ]struct {}) T ) []T {
824- if len (msgs ) == 0 {
825- return msgs
826- }
827- repaired := make ([]T , 0 , len (msgs )+ 2 )
828- for i := range msgs {
829- repaired = append (repaired , msgs [i ])
830-
831- m , ok := marshalToMap (msgs [i ])
832- if ! ok || m ["role" ] != "assistant" {
833- continue
834- }
835-
836- toolUseIDs := collectToolUseIDs (contentArray (m ))
837- if len (toolUseIDs ) == 0 {
838- continue
839- }
840-
841- // Remove any IDs that already have results in the next user message
842- if i + 1 < len (msgs ) {
843- if next , ok := marshalToMap (msgs [i + 1 ]); ok && next ["role" ] == "user" {
844- toolResultIDs := collectToolResultIDs (contentArray (next ))
845- for id := range toolResultIDs {
846- delete (toolUseIDs , id )
847- }
848- }
849- }
850-
851- if len (toolUseIDs ) > 0 {
852- slog .Debug ("Inserting synthetic user message for missing tool_results" ,
853- "assistant_index" , i ,
854- "missing_count" , len (toolUseIDs ))
855- repaired = append (repaired , makeSynthetic (toolUseIDs ))
856- }
857- }
858- return repaired
859- }
860-
861- func collectToolUseIDs (content []any ) map [string ]struct {} {
862- ids := make (map [string ]struct {})
863- for _ , c := range content {
864- if cb , ok := c .(map [string ]any ); ok {
865- if t , _ := cb ["type" ].(string ); t == "tool_use" {
866- if id , _ := cb ["id" ].(string ); id != "" {
867- ids [id ] = struct {}{}
868- }
869- }
870- }
871- }
872- return ids
873- }
874-
875- func collectToolResultIDs (content []any ) map [string ]struct {} {
876- ids := make (map [string ]struct {})
877- for _ , c := range content {
878- if cb , ok := c .(map [string ]any ); ok {
879- if t , _ := cb ["type" ].(string ); t == "tool_result" {
880- if id , _ := cb ["tool_use_id" ].(string ); id != "" {
881- ids [id ] = struct {}{}
882- }
883- }
884- }
885- }
886- return ids
887- }
888-
889- func differenceIDs (a , b map [string ]struct {}) []string {
890- if len (a ) == 0 {
891- return nil
892- }
893- var missing []string
894- for id := range a {
895- if _ , ok := b [id ]; ! ok {
896- missing = append (missing , id )
897- }
898- }
899- return missing
900- }
901-
902753// validThinkingTokens validates that the token budget is within the
903754// acceptable range for Anthropic (>= 1024 and < maxTokens).
904755// Returns (tokens, true) if valid, or (0, false) with a warning log if not.
0 commit comments