mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 17:36:47 +08:00
fix(xai): qualify namespaced tool choices
This commit is contained in:
@@ -861,6 +861,7 @@ func (e *XAIExecutor) prepareResponsesRequestTo(ctx context.Context, req cliprox
|
||||
body, _ = sjson.DeleteBytes(body, "stream_options")
|
||||
namespaceTools := collectXAINamespaceToolRefs(body)
|
||||
body = normalizeXAITools(body)
|
||||
body = normalizeXAINamespaceToolChoice(body)
|
||||
body = normalizeXAIToolChoiceForTools(body)
|
||||
var replayScope xaiReasoningReplayScope
|
||||
body, replayScope, err = applyXAIReasoningReplayCacheRequired(ctx, from, req, opts, body)
|
||||
@@ -1246,6 +1247,34 @@ func normalizeXAIToolChoiceForTools(body []byte) []byte {
|
||||
return body
|
||||
}
|
||||
|
||||
// normalizeXAINamespaceToolChoice qualifies a forced namespaced function choice
|
||||
// using the same name sent in the flattened tools list. xAI does not accept the
|
||||
// Responses namespace field on tool_choice.
|
||||
func normalizeXAINamespaceToolChoice(body []byte) []byte {
|
||||
if !gjson.ValidBytes(body) {
|
||||
return body
|
||||
}
|
||||
toolChoice := gjson.GetBytes(body, "tool_choice")
|
||||
if !toolChoice.IsObject() || toolChoice.Get("type").String() != xaiFunctionToolType {
|
||||
return body
|
||||
}
|
||||
namespaceName := strings.TrimSpace(toolChoice.Get("namespace").String())
|
||||
toolName := strings.TrimSpace(toolChoice.Get("name").String())
|
||||
qualifiedName := qualifyXAINamespaceToolName(namespaceName, toolName)
|
||||
if namespaceName == "" || qualifiedName == "" {
|
||||
return body
|
||||
}
|
||||
updated, errSet := sjson.SetBytes(body, "tool_choice.name", qualifiedName)
|
||||
if errSet != nil {
|
||||
return body
|
||||
}
|
||||
updated, errDelete := sjson.DeleteBytes(updated, "tool_choice.namespace")
|
||||
if errDelete != nil {
|
||||
return body
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func normalizeXAITool(tool gjson.Result, namespaceName string) ([]byte, bool, bool) {
|
||||
toolType := tool.Get("type").String()
|
||||
changed := false
|
||||
|
||||
@@ -1136,6 +1136,44 @@ func TestNormalizeXAITools_QualifiesSameNamedNamespaceTools(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeXAINamespaceToolChoice(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"tools":[{"type":"namespace","name":"mcp__exa","tools":[{"type":"function","name":"search","parameters":{"type":"object"}}]}],
|
||||
"tool_choice":{"type":"function","name":"search","namespace":"mcp__exa"}
|
||||
}`)
|
||||
out := normalizeXAITools(body)
|
||||
out = normalizeXAINamespaceToolChoice(out)
|
||||
|
||||
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "mcp__exa__search" {
|
||||
t.Fatalf("tools.0.name = %q, want mcp__exa__search; body=%s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tool_choice.name").String(); got != "mcp__exa__search" {
|
||||
t.Fatalf("tool_choice.name = %q, want mcp__exa__search; body=%s", got, string(out))
|
||||
}
|
||||
if gjson.GetBytes(out, "tool_choice.namespace").Exists() {
|
||||
t.Fatalf("tool_choice.namespace should be removed for xAI upstream: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeXAINamespaceToolChoice_PreservesOtherChoices(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body []byte
|
||||
}{
|
||||
{name: "automatic choice", body: []byte(`{"tool_choice":"auto"}`)},
|
||||
{name: "top-level function", body: []byte(`{"tool_choice":{"type":"function","name":"search"}}`)},
|
||||
{name: "non-function choice", body: []byte(`{"tool_choice":{"type":"web_search","name":"search","namespace":"mcp__exa"}}`)},
|
||||
{name: "malformed payload", body: []byte(`{"tool_choice":{"type":"function"`)},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := normalizeXAINamespaceToolChoice(tt.body); !bytes.Equal(got, tt.body) {
|
||||
t.Fatalf("payload changed: got=%q want=%q", got, tt.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualifyXAINamespaceToolNamePreservesQualifiedNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user