mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
fix(translator): preserve built-in tools across openai<->responses
- Pass through non-function tool definitions like web_search - Translate tool_choice for built-in tools and function tools - Add regression tests for built-in tool passthrough
This commit is contained in:
@@ -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")
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
54
test/builtin_tools_translation_test.go
Normal file
54
test/builtin_tools_translation_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package test
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user