mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-20 13:12:16 +08:00
fix(executor/xai): drop orphaned tool_choice when Claude tools array is empty
When Claude Code sends a stop-hook evaluator request (or any request without tools), the payload includes "tools": [] (empty array). The claude->codex translator unconditionally emits tools: [] + tool_choice: "auto" + parallel_tool_calls: true into the Codex Responses shape. When that payload is routed to xAI, the upstream rejects with HTTP 400: "A tool_choice was set on the request but no tools were specified." Fix entirely in the xAI executor (translator package is policy-locked): add normalizeXAIToolChoiceForTools() after normalizeXAITools() to drop tool_choice and parallel_tool_calls whenever tools end up absent or empty (covering both the empty-from-source case and the all-filtered-out case where every tool was an unsupported type such as tool_search or image_generation). Per code-review feedback: always remove parallel_tool_calls when tools are missing (not gated on tool_choice presence) and existence-check each key before sjson delete to avoid unnecessary JSON parse/copy. Verification: - go build -o test-output ./cmd/server - go test ./internal/runtime/executor/... -count=1 - 5 new regression tests cover empty / missing / present / orphaned parallel_tool_calls / no-op-when-both-absent.
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
|
||||
|
||||
Reference in New Issue
Block a user