@@ -40,6 +40,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
4040 template := []byte (`{"model":"","instructions":"","input":[]}` )
4141
4242 rootResult := gjson .ParseBytes (rawJSON )
43+ toolNameMap := buildReverseMapFromClaudeOriginalToShort (rawJSON )
4344 template , _ = sjson .SetBytes (template , "model" , modelName )
4445
4546 // Process system messages and convert them to input content format.
@@ -174,8 +175,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
174175 functionCallMessage , _ = sjson .SetBytes (functionCallMessage , "call_id" , messageContentResult .Get ("id" ).String ())
175176 {
176177 name := messageContentResult .Get ("name" ).String ()
177- toolMap := buildReverseMapFromClaudeOriginalToShort (rawJSON )
178- if short , ok := toolMap [name ]; ok {
178+ if short , ok := toolNameMap [name ]; ok {
179179 name = short
180180 } else {
181181 name = shortenNameIfNeeded (name )
@@ -249,31 +249,22 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
249249 toolsResult := rootResult .Get ("tools" )
250250 if toolsResult .IsArray () {
251251 template , _ = sjson .SetRawBytes (template , "tools" , []byte (`[]` ))
252- template , _ = sjson .SetBytes (template , "tool_choice" , `auto` )
252+ webSearchToolNames := buildClaudeWebSearchToolNameSet (toolsResult )
253+ template , _ = sjson .SetRawBytes (template , "tool_choice" , convertClaudeToolChoiceToCodex (rootResult .Get ("tool_choice" ), toolNameMap , webSearchToolNames ))
253254 toolResults := toolsResult .Array ()
254- // Build short name map from declared tools
255- var names []string
256- for i := 0 ; i < len (toolResults ); i ++ {
257- n := toolResults [i ].Get ("name" ).String ()
258- if n != "" {
259- names = append (names , n )
260- }
261- }
262- shortMap := buildShortNameMap (names )
263255 for i := 0 ; i < len (toolResults ); i ++ {
264256 toolResult := toolResults [i ]
265257 // Special handling: map Claude web search tool to Codex web_search
266- if toolResult .Get ("type" ).String () == "web_search_20250305" {
267- // Replace the tool content entirely with {"type":"web_search"}
268- template , _ = sjson .SetRawBytes (template , "tools.-1" , []byte (`{"type":"web_search"}` ))
258+ if isClaudeWebSearchToolType (toolResult .Get ("type" ).String ()) {
259+ template , _ = sjson .SetRawBytes (template , "tools.-1" , convertClaudeWebSearchToolToCodex (toolResult ))
269260 continue
270261 }
271262 tool := []byte (toolResult .Raw )
272263 tool , _ = sjson .SetBytes (tool , "type" , "function" )
273264 // Apply shortened name if needed
274265 if v := toolResult .Get ("name" ); v .Exists () {
275266 name := v .String ()
276- if short , ok := shortMap [name ]; ok {
267+ if short , ok := toolNameMap [name ]; ok {
277268 name = short
278269 } else {
279270 name = shortenNameIfNeeded (name )
@@ -370,6 +361,81 @@ func isFernetLikeReasoningSignature(signature string) bool {
370361 return ciphertextLen > 0 && ciphertextLen % aesBlockSize == 0
371362}
372363
364+ func isClaudeWebSearchToolType (toolType string ) bool {
365+ return toolType == "web_search_20250305" || toolType == "web_search_20260209"
366+ }
367+
368+ func buildClaudeWebSearchToolNameSet (tools gjson.Result ) map [string ]struct {} {
369+ names := map [string ]struct {}{}
370+ if ! tools .IsArray () {
371+ return names
372+ }
373+
374+ tools .ForEach (func (_ , tool gjson.Result ) bool {
375+ toolType := tool .Get ("type" ).String ()
376+ if ! isClaudeWebSearchToolType (toolType ) {
377+ return true
378+ }
379+
380+ if name := tool .Get ("name" ).String (); name != "" {
381+ names [name ] = struct {}{}
382+ }
383+ return true
384+ })
385+
386+ return names
387+ }
388+
389+ func convertClaudeToolChoiceToCodex (toolChoice gjson.Result , toolNameMap map [string ]string , webSearchToolNames map [string ]struct {}) []byte {
390+ if ! toolChoice .Exists () || toolChoice .Type == gjson .Null {
391+ return []byte (`"auto"` )
392+ }
393+
394+ choiceType := toolChoice .Get ("type" ).String ()
395+ if choiceType == "" && toolChoice .Type == gjson .String {
396+ choiceType = toolChoice .String ()
397+ }
398+
399+ switch choiceType {
400+ case "auto" , "" :
401+ return []byte (`"auto"` )
402+ case "any" :
403+ return []byte (`"required"` )
404+ case "none" :
405+ return []byte (`"none"` )
406+ case "tool" :
407+ name := toolChoice .Get ("name" ).String ()
408+ if _ , ok := webSearchToolNames [name ]; ok {
409+ return []byte (`{"type":"web_search"}` )
410+ }
411+ if short , ok := toolNameMap [name ]; ok {
412+ name = short
413+ } else {
414+ name = shortenNameIfNeeded (name )
415+ }
416+ if name == "" {
417+ return []byte (`"auto"` )
418+ }
419+
420+ choice := []byte (`{"type":"function","name":""}` )
421+ choice , _ = sjson .SetBytes (choice , "name" , name )
422+ return choice
423+ default :
424+ return []byte (`"auto"` )
425+ }
426+ }
427+
428+ func convertClaudeWebSearchToolToCodex (tool gjson.Result ) []byte {
429+ out := []byte (`{"type":"web_search"}` )
430+ if allowedDomains := tool .Get ("allowed_domains" ); allowedDomains .Exists () && allowedDomains .IsArray () {
431+ out , _ = sjson .SetRawBytes (out , "filters.allowed_domains" , []byte (allowedDomains .Raw ))
432+ }
433+ if userLocation := tool .Get ("user_location" ); userLocation .Exists () && userLocation .IsObject () {
434+ out , _ = sjson .SetRawBytes (out , "user_location" , []byte (userLocation .Raw ))
435+ }
436+ return out
437+ }
438+
373439// shortenNameIfNeeded applies a simple shortening rule for a single name.
374440func shortenNameIfNeeded (name string ) string {
375441 const limit = 64
0 commit comments