mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50: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()
|
arr := tools.Array()
|
||||||
for i := 0; i < len(arr); i++ {
|
for i := 0; i < len(arr); i++ {
|
||||||
t := 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 := `{}`
|
||||||
item, _ = sjson.Set(item, "type", "function")
|
item, _ = sjson.Set(item, "type", "function")
|
||||||
fn := t.Get("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)
|
out, _ = sjson.Set(out, "store", false)
|
||||||
return []byte(out)
|
return []byte(out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,14 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
|||||||
var chatCompletionsTools []interface{}
|
var chatCompletionsTools []interface{}
|
||||||
|
|
||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
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":{}}`
|
chatTool := `{"type":"function","function":{}}`
|
||||||
|
|
||||||
// Convert tool structure from responses format to chat completions format
|
// 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