mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-27 11:21:02 +08:00
fix(xai): normalize forced web_search tool_choice during Responses request prep
Closes: #4718
This commit is contained in:
@@ -97,6 +97,7 @@ func (e *XAIExecutor) prepareResponsesRequestTo(ctx context.Context, req cliprox
|
||||
// Drop choices that point at tools removed by normalizeXAITools before any
|
||||
// configured x_search injection, so no surviving choice references a deleted tool.
|
||||
body = normalizeXAINamespaceToolChoice(body)
|
||||
body = normalizeXAIForcedWebSearchToolChoice(body)
|
||||
body = pruneXAIOrphanedToolChoice(body)
|
||||
body = normalizeXAIToolChoiceForTools(body)
|
||||
if e.cfg != nil && e.cfg.XAI.InjectXSearch {
|
||||
@@ -584,6 +585,26 @@ func ensureXAINativeXSearchAllowedTools(body []byte) []byte {
|
||||
return body
|
||||
}
|
||||
|
||||
// normalizeXAIForcedWebSearchToolChoice rewrites Codex's hosted-tool choice
|
||||
// into the allowed_tools form accepted by xAI's ModelToolChoice schema.
|
||||
func normalizeXAIForcedWebSearchToolChoice(body []byte) []byte {
|
||||
choice := gjson.GetBytes(body, "tool_choice")
|
||||
if !choice.IsObject() || strings.TrimSpace(choice.Get("type").String()) != xaiWebSearchToolType {
|
||||
return body
|
||||
}
|
||||
|
||||
allowedChoice := []byte(`{"type":"allowed_tools","mode":"required","tools":[]}`)
|
||||
allowedChoice, errSetAllowed := sjson.SetRawBytes(allowedChoice, "tools.-1", []byte(choice.Raw))
|
||||
if errSetAllowed != nil {
|
||||
return body
|
||||
}
|
||||
updated, errSetChoice := sjson.SetRawBytes(body, "tool_choice", allowedChoice)
|
||||
if errSetChoice != nil {
|
||||
return body
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
// pruneXAIOrphanedToolChoice removes tool_choice entries that no longer match
|
||||
// any remaining tool after normalizeXAITools filtering. Forced choices that
|
||||
// reference a deleted tool are dropped entirely; allowed_tools lists keep only
|
||||
|
||||
@@ -802,6 +802,46 @@ func TestEnsureXAINativeXSearchTool(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestXAIExecutorPrepareNormalizesClaudeWebSearchToolChoice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
exec := NewXAIExecutor(&config.Config{})
|
||||
prepared, errPrepare := exec.prepareResponsesRequest(context.Background(), cliproxyexecutor.Request{
|
||||
Model: "grok-4.5",
|
||||
Payload: []byte(`{
|
||||
"model":"grok-4.5",
|
||||
"max_tokens":4096,
|
||||
"stream":true,
|
||||
"output_config":{"effort":"high"},
|
||||
"thinking":{"type":"disabled"},
|
||||
"messages":[{"role":"user","content":[{"type":"text","text":"Perform a web search"}]}],
|
||||
"tool_choice":{"type":"tool","name":"web_search"},
|
||||
"tools":[{"type":"web_search_20250305","name":"web_search","max_uses":8}]
|
||||
}`),
|
||||
}, cliproxyexecutor.Options{
|
||||
SourceFormat: sdktranslator.FormatClaude,
|
||||
Stream: true,
|
||||
}, true)
|
||||
if errPrepare != nil {
|
||||
t.Fatalf("prepareResponsesRequest() error = %v", errPrepare)
|
||||
}
|
||||
|
||||
choice := gjson.GetBytes(prepared.body, "tool_choice")
|
||||
if got := choice.Get("type").String(); got != "allowed_tools" {
|
||||
t.Fatalf("tool_choice.type = %q, want allowed_tools; body=%s", got, prepared.body)
|
||||
}
|
||||
if got := choice.Get("mode").String(); got != "required" {
|
||||
t.Fatalf("tool_choice.mode = %q, want required; body=%s", got, prepared.body)
|
||||
}
|
||||
allowed := choice.Get("tools").Array()
|
||||
if len(allowed) != 1 {
|
||||
t.Fatalf("tool_choice.tools length = %d, want 1; body=%s", len(allowed), prepared.body)
|
||||
}
|
||||
if got := allowed[0].Get("type").String(); got != "web_search" {
|
||||
t.Fatalf("tool_choice.tools.0.type = %q, want web_search; body=%s", got, prepared.body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPruneXAIOrphanedToolChoice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user