@@ -654,6 +654,86 @@ func SupportsAdaptiveThinking(model string) bool {
654654 return strings .Contains (model , "opus" ) || strings .Contains (model , "sonnet" )
655655}
656656
657+ // Computer-use tool generations.
658+ // - "20251124" — Opus 4.7, Opus 4.6, Sonnet 4.6, Opus 4.5
659+ // - "20250124" — everything else (Sonnet 4.5, Haiku 4.5, Opus 4.1, Sonnet 4, Opus 4, Sonnet 3.7)
660+ //
661+ // The bash tool is generation-invariant (always bash_20250124).
662+ const (
663+ ComputerUseGen20251124 = "20251124"
664+ ComputerUseGen20250124 = "20250124"
665+ )
666+
667+ // ComputerUseGeneration returns the tool-version generation a Claude model
668+ // uses for computer-use / text-editor tools. This drives:
669+ // - Which beta header to inject (computer-use-2025-11-24 vs 2025-01-24).
670+ // - Which computer_*/text_editor_* type the upstream API will accept.
671+ // - Which `name` literal Anthropic's Pydantic validator demands for text_editor.
672+ func ComputerUseGeneration (model string ) string {
673+ m := strings .ToLower (model )
674+ // Opus 4.7+ falls into the new generation.
675+ if IsOpus47 (m ) {
676+ return ComputerUseGen20251124
677+ }
678+ // Opus 4.6 / Sonnet 4.6 / Opus 4.5 also use the new generation.
679+ if strings .Contains (m , "opus" ) {
680+ if strings .Contains (m , "4-5" ) || strings .Contains (m , "4.5" ) ||
681+ strings .Contains (m , "4-6" ) || strings .Contains (m , "4.6" ) {
682+ return ComputerUseGen20251124
683+ }
684+ }
685+ if strings .Contains (m , "sonnet" ) {
686+ if strings .Contains (m , "4-6" ) || strings .Contains (m , "4.6" ) {
687+ return ComputerUseGen20251124
688+ }
689+ }
690+ return ComputerUseGen20250124
691+ }
692+
693+ // NormalizedToolSpec returns the canonical {type, name} pair Anthropic's API
694+ // expects for a server tool, given the model's computer-use generation.
695+ // baseTool is the family name with no version suffix: "computer", "text_editor", or "bash".
696+ // Returns ("", "") if baseTool is unknown.
697+ func NormalizedToolSpec (generation , baseTool string ) (toolType , toolName string ) {
698+ switch baseTool {
699+ case "computer" :
700+ if generation == ComputerUseGen20251124 {
701+ return string (AnthropicToolTypeComputer20251124 ), "computer"
702+ }
703+ return string (AnthropicToolTypeComputer20250124 ), "computer"
704+ case "bash" :
705+ // bash_20250124 is generation-invariant per Anthropic's docs.
706+ return string (AnthropicToolTypeBash20250124 ), "bash"
707+ case "text_editor" :
708+ if generation == ComputerUseGen20251124 {
709+ return string (AnthropicToolTypeTextEditor20250728 ), "str_replace_based_edit_tool"
710+ }
711+ return string (AnthropicToolTypeTextEditor20250124 ), "str_replace_editor"
712+ }
713+ return "" , ""
714+ }
715+
716+ // computerUseBaseTool extracts the family name from a versioned tool type.
717+ // Returns "" for tool types that are not part of the computer-use family.
718+ //
719+ // Examples:
720+ //
721+ // computer_20251124 -> "computer"
722+ // text_editor_20250728 -> "text_editor"
723+ // bash_20250124 -> "bash"
724+ // web_search_20250305 -> ""
725+ func computerUseBaseTool (toolType string ) string {
726+ switch {
727+ case strings .HasPrefix (toolType , "computer_" ):
728+ return "computer"
729+ case strings .HasPrefix (toolType , "text_editor_" ):
730+ return "text_editor"
731+ case strings .HasPrefix (toolType , "bash_" ):
732+ return "bash"
733+ }
734+ return ""
735+ }
736+
657737// MapBifrostEffortToAnthropic maps a Bifrost effort level to an Anthropic effort level.
658738// Anthropic supports "low", "medium", "high", "max"; Bifrost also has "minimal" which maps to "low".
659739func MapBifrostEffortToAnthropic (effort string ) string {
@@ -1080,9 +1160,16 @@ func StripAutoInjectableTools(jsonBody []byte) ([]byte, error) {
10801160}
10811161
10821162// RemapRawToolVersionsForProvider inspects tools in a raw JSON body and remaps
1083- // unsupported tool versions to supported ones for the target provider.
1084- // Returns an error if a tool type is fundamentally unsupported (no remap possible).
1085- func RemapRawToolVersionsForProvider (jsonBody []byte , provider schemas.ModelProvider ) ([]byte , error ) {
1163+ // unsupported tool versions to supported ones for the target provider, and
1164+ // normalizes computer-use / text-editor / bash tool {type, name} pairs to match
1165+ // the model's required generation. Returns an error if a tool type is
1166+ // fundamentally unsupported (no remap possible).
1167+ //
1168+ // model is the request's "model" field; it drives ComputerUseGeneration so that
1169+ // (e.g.) a request pairing claude-sonnet-4-6 with text_editor_20250124 gets
1170+ // rewritten to text_editor_20250728 + str_replace_based_edit_tool before
1171+ // hitting Anthropic's strict Pydantic validator.
1172+ func RemapRawToolVersionsForProvider (jsonBody []byte , provider schemas.ModelProvider , model string ) ([]byte , error ) {
10861173 toolsResult := providerUtils .GetJSONField (jsonBody , "tools" )
10871174 if ! toolsResult .Exists () || ! toolsResult .IsArray () {
10881175 return jsonBody , nil
@@ -1103,12 +1190,46 @@ func RemapRawToolVersionsForProvider(jsonBody []byte, provider schemas.ModelProv
11031190 }
11041191 }
11051192
1106- // Apply version remaps
1193+ // Normalize computer-use / text-editor / bash tools to the canonical
1194+ // (type, name) pair for the model's generation. Runs before
1195+ // providerToolVersionRemaps so downgrades still work for non-Anthropic
1196+ // providers that share the schema.
1197+ generation := ComputerUseGeneration (model )
1198+ for i , tool := range tools {
1199+ toolType := tool .Get ("type" ).String ()
1200+ baseTool := computerUseBaseTool (toolType )
1201+ if baseTool == "" {
1202+ continue
1203+ }
1204+ wantType , wantName := NormalizedToolSpec (generation , baseTool )
1205+ if wantType == "" {
1206+ continue
1207+ }
1208+ if toolType != wantType {
1209+ path := fmt .Sprintf ("tools.%d.type" , i )
1210+ jsonBody , err = providerUtils .SetJSONField (jsonBody , path , wantType )
1211+ if err != nil {
1212+ return nil , fmt .Errorf ("failed to normalize tool type: %w" , err )
1213+ }
1214+ }
1215+ // Only set name if the tool has one (custom tools use input_schema; computer-use family always has a name).
1216+ if existingName := tool .Get ("name" ).String (); existingName != "" && existingName != wantName {
1217+ path := fmt .Sprintf ("tools.%d.name" , i )
1218+ jsonBody , err = providerUtils .SetJSONField (jsonBody , path , wantName )
1219+ if err != nil {
1220+ return nil , fmt .Errorf ("failed to normalize tool name: %w" , err )
1221+ }
1222+ }
1223+ }
1224+
1225+ // Apply provider-specific version remaps (e.g. web_search downgrades for non-Anthropic providers)
11071226 remaps , ok := providerToolVersionRemaps [provider ]
11081227 if ! ok {
11091228 return jsonBody , nil
11101229 }
11111230
1231+ // Re-fetch tools array since paths may have changed via SetJSONField above
1232+ tools = providerUtils .GetJSONField (jsonBody , "tools" ).Array ()
11121233 for i , tool := range tools {
11131234 toolType := tool .Get ("type" ).String ()
11141235 for _ , remap := range remaps {
0 commit comments