mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
Merge pull request #3649 from intcua/fix/xai-empty-tools-orphan-tool-choice
fix(executor/xai): drop orphaned tool_choice when Claude tools array is empty
This commit is contained in:
@@ -506,6 +506,7 @@ func (e *XAIExecutor) prepareResponsesRequest(ctx context.Context, req cliproxye
|
||||
body, _ = sjson.DeleteBytes(body, "safety_identifier")
|
||||
body, _ = sjson.DeleteBytes(body, "stream_options")
|
||||
body = normalizeXAITools(body)
|
||||
body = normalizeXAIToolChoiceForTools(body)
|
||||
body = normalizeXAIInputReasoningItems(body)
|
||||
body = normalizeCodexInstructions(body)
|
||||
body = sanitizeXAIResponsesBody(body, baseModel)
|
||||
@@ -715,6 +716,28 @@ func normalizeXAITools(body []byte) []byte {
|
||||
return updated
|
||||
}
|
||||
|
||||
// normalizeXAIToolChoiceForTools drops tool_choice and parallel_tool_calls
|
||||
// when tools are absent or empty (including after normalizeXAITools filtering).
|
||||
// xAI rejects payloads that include tool_choice without any tools defined.
|
||||
// Existence checks avoid unnecessary sjson parse/copy passes.
|
||||
func normalizeXAIToolChoiceForTools(body []byte) []byte {
|
||||
tools := gjson.GetBytes(body, "tools")
|
||||
hasTools := tools.Exists() && tools.IsArray() && len(tools.Array()) > 0
|
||||
if hasTools {
|
||||
return body
|
||||
}
|
||||
if tools.Exists() {
|
||||
body, _ = sjson.DeleteBytes(body, "tools")
|
||||
}
|
||||
if gjson.GetBytes(body, "tool_choice").Exists() {
|
||||
body, _ = sjson.DeleteBytes(body, "tool_choice")
|
||||
}
|
||||
if gjson.GetBytes(body, "parallel_tool_calls").Exists() {
|
||||
body, _ = sjson.DeleteBytes(body, "parallel_tool_calls")
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func normalizeXAITool(tool gjson.Result) ([]byte, bool, bool) {
|
||||
toolType := tool.Get("type").String()
|
||||
changed := false
|
||||
|
||||
@@ -592,3 +592,57 @@ func TestXAIExecutorExecuteVideosUsesNativeEndpointFromRequestPath(t *testing.T)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeXAIToolChoiceForTools_DropsWhenToolsEmpty(t *testing.T) {
|
||||
body := []byte(`{"model":"grok-4","tools":[],"tool_choice":"auto","parallel_tool_calls":true,"input":"hi"}`)
|
||||
out := normalizeXAIToolChoiceForTools(body)
|
||||
|
||||
if gjson.GetBytes(out, "tools").Exists() {
|
||||
t.Fatalf("empty tools should be removed: %s", string(out))
|
||||
}
|
||||
if gjson.GetBytes(out, "tool_choice").Exists() {
|
||||
t.Fatalf("tool_choice should be removed when tools empty: %s", string(out))
|
||||
}
|
||||
if gjson.GetBytes(out, "parallel_tool_calls").Exists() {
|
||||
t.Fatalf("parallel_tool_calls should be removed when tools empty: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeXAIToolChoiceForTools_DropsWhenToolsMissing(t *testing.T) {
|
||||
body := []byte(`{"model":"grok-4","tool_choice":"auto","input":"hi"}`)
|
||||
out := normalizeXAIToolChoiceForTools(body)
|
||||
|
||||
if gjson.GetBytes(out, "tool_choice").Exists() {
|
||||
t.Fatalf("tool_choice should be removed when tools missing: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeXAIToolChoiceForTools_DropsOrphanedParallelToolCalls(t *testing.T) {
|
||||
body := []byte(`{"model":"grok-4","parallel_tool_calls":true,"input":"hi"}`)
|
||||
out := normalizeXAIToolChoiceForTools(body)
|
||||
|
||||
if gjson.GetBytes(out, "parallel_tool_calls").Exists() {
|
||||
t.Fatalf("parallel_tool_calls should be removed when tools missing even without tool_choice: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeXAIToolChoiceForTools_KeepsWhenToolsPresent(t *testing.T) {
|
||||
body := []byte(`{"model":"grok-4","tools":[{"type":"function","name":"Bash"}],"tool_choice":"auto","input":"hi"}`)
|
||||
out := normalizeXAIToolChoiceForTools(body)
|
||||
|
||||
if !gjson.GetBytes(out, "tools").Exists() {
|
||||
t.Fatalf("tools should be kept: %s", string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tool_choice").String(); got != "auto" {
|
||||
t.Fatalf("tool_choice = %q, want auto: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeXAIToolChoiceForTools_NoOpWhenBothAbsent(t *testing.T) {
|
||||
body := []byte(`{"model":"grok-4","input":"hi"}`)
|
||||
out := normalizeXAIToolChoiceForTools(body)
|
||||
|
||||
if gjson.GetBytes(out, "tool_choice").Exists() {
|
||||
t.Fatalf("tool_choice should not appear: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user