Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,15 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
arr := tools.Array()
for i := 0; i < len(arr); i++ {
t := arr[i]
if t.Get("type").String() == "function" {
toolType := t.Get("type").String()
// Pass through built-in tools (e.g. {"type":"web_search"}) directly for the Responses API.
// Only "function" needs structural conversion because Chat Completions nests details under "function".
if toolType != "" && toolType != "function" && t.IsObject() {
out, _ = sjson.SetRaw(out, "tools.-1", t.Raw)
continue
}

if toolType == "function" {
item := `{}`
item, _ = sjson.Set(item, "type", "function")
fn := t.Get("function")
Expand Down Expand Up @@ -304,6 +312,37 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
}
}

// Map tool_choice when present.
// Chat Completions: "tool_choice" can be a string ("auto"/"none") or an object (e.g. {"type":"function","function":{"name":"..."}}).
// Responses API: keep built-in tool choices as-is; flatten function choice to {"type":"function","name":"..."}.
if tc := gjson.GetBytes(rawJSON, "tool_choice"); tc.Exists() {
switch {
case tc.Type == gjson.String:
out, _ = sjson.Set(out, "tool_choice", tc.String())
case tc.IsObject():
tcType := tc.Get("type").String()
if tcType == "function" {
name := tc.Get("function.name").String()
if name != "" {
if short, ok := originalToolNameMap[name]; ok {
name = short
} else {
name = shortenNameIfNeeded(name)
}
}
Comment on lines +325 to +332
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic for shortening a tool name appears to be duplicated in a few places in this file (e.g., for handling tool_calls around line 208 and tools around line 292). To improve maintainability and reduce code duplication, consider extracting this into a helper function. This would also ensure consistent handling of tool name shortening across different parts of the request translation.

choice := `{}`
choice, _ = sjson.Set(choice, "type", "function")
if name != "" {
choice, _ = sjson.Set(choice, "name", name)
}
out, _ = sjson.SetRaw(out, "tool_choice", choice)
} else if tcType != "" {
// Built-in tool choices (e.g. {"type":"web_search"}) are already Responses-compatible.
out, _ = sjson.SetRaw(out, "tool_choice", tc.Raw)
}
}
}

out, _ = sjson.Set(out, "store", false)
return []byte(out)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
var chatCompletionsTools []interface{}

tools.ForEach(func(_, tool gjson.Result) bool {
// Built-in tools (e.g. {"type":"web_search"}) are already compatible with the Chat Completions schema.
// Only function tools need structural conversion because Chat Completions nests details under "function".
toolType := tool.Get("type").String()
if toolType != "" && toolType != "function" && tool.IsObject() {
chatCompletionsTools = append(chatCompletionsTools, tool.Value())
return true
}

chatTool := `{"type":"function","function":{}}`

// Convert tool structure from responses format to chat completions format
Expand Down
54 changes: 54 additions & 0 deletions test/builtin_tools_translation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The added tests are a great start for verifying the new functionality. To make them more robust and prevent future regressions, consider expanding them to cover scenarios with mixed tool types. For both TestOpenAIToCodex_PreservesBuiltinTools and TestOpenAIResponsesToOpenAI_PreservesBuiltinTools, adding a test case with both a built-in tool (e.g., web_search) and a function tool would ensure that the changes don't interfere with existing functionality. Using a table-driven test structure could help organize these cases cleanly.


import (
"testing"

_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator"

sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
"github.com/tidwall/gjson"
)

func TestOpenAIToCodex_PreservesBuiltinTools(t *testing.T) {
in := []byte(`{
"model":"gpt-5",
"messages":[{"role":"user","content":"hi"}],
"tools":[{"type":"web_search","search_context_size":"high"}],
"tool_choice":{"type":"web_search"}
}`)

out := sdktranslator.TranslateRequest(sdktranslator.FormatOpenAI, sdktranslator.FormatCodex, "gpt-5", in, false)

if got := gjson.GetBytes(out, "tools.#").Int(); got != 1 {
t.Fatalf("expected 1 tool, got %d: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tools.0.type").String(); got != "web_search" {
t.Fatalf("expected tools[0].type=web_search, got %q: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tools.0.search_context_size").String(); got != "high" {
t.Fatalf("expected tools[0].search_context_size=high, got %q: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tool_choice.type").String(); got != "web_search" {
t.Fatalf("expected tool_choice.type=web_search, got %q: %s", got, string(out))
}
}

func TestOpenAIResponsesToOpenAI_PreservesBuiltinTools(t *testing.T) {
in := []byte(`{
"model":"gpt-5",
"input":[{"role":"user","content":[{"type":"input_text","text":"hi"}]}],
"tools":[{"type":"web_search","search_context_size":"low"}]
}`)

out := sdktranslator.TranslateRequest(sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, "gpt-5", in, false)

if got := gjson.GetBytes(out, "tools.#").Int(); got != 1 {
t.Fatalf("expected 1 tool, got %d: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tools.0.type").String(); got != "web_search" {
t.Fatalf("expected tools[0].type=web_search, got %q: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tools.0.search_context_size").String(); got != "low" {
t.Fatalf("expected tools[0].search_context_size=low, got %q: %s", got, string(out))
}
}
Loading