mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-02 21:13:44 +08:00
feat(xai): support namespace tools and enhance tool normalization logic
- Added `namespace` tool type support, enabling nested tools to be normalized and moved to the top level. - Refactored tool normalization logic into `normalizeXAITool` for reusability and clarity. - Updated `xai_executor` test cases to validate namespace tool handling and nested tool normalization.
This commit is contained in:
@@ -34,6 +34,7 @@ const (
|
||||
xaiCustomToolType = "custom"
|
||||
xaiFunctionToolType = "function"
|
||||
xaiImageGenerationToolType = "image_generation"
|
||||
xaiNamespaceToolType = "namespace"
|
||||
xaiToolSearchType = "tool_search"
|
||||
xaiWebSearchToolType = "web_search"
|
||||
xaiImagesGenerationsPath = "/images/generations"
|
||||
@@ -664,30 +665,34 @@ func normalizeXAITools(body []byte) []byte {
|
||||
filtered := []byte(`[]`)
|
||||
for _, tool := range tools.Array() {
|
||||
toolType := tool.Get("type").String()
|
||||
if toolType == xaiToolSearchType || toolType == xaiImageGenerationToolType {
|
||||
if toolType == xaiNamespaceToolType {
|
||||
changed = true
|
||||
if namespaceTools := tool.Get("tools"); namespaceTools.IsArray() {
|
||||
for _, nestedTool := range namespaceTools.Array() {
|
||||
nestedRaw, nestedChanged, ok := normalizeXAITool(nestedTool)
|
||||
if !ok {
|
||||
return body
|
||||
}
|
||||
changed = changed || nestedChanged
|
||||
if len(nestedRaw) == 0 {
|
||||
continue
|
||||
}
|
||||
updated, errSet := sjson.SetRawBytes(filtered, "-1", nestedRaw)
|
||||
if errSet != nil {
|
||||
return body
|
||||
}
|
||||
filtered = updated
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
raw := []byte(tool.Raw)
|
||||
if toolType == xaiCustomToolType {
|
||||
if tool.Get("name").String() == "apply_patch" {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
updatedTool, errSet := sjson.SetBytes(raw, "type", xaiFunctionToolType)
|
||||
if errSet != nil {
|
||||
return body
|
||||
}
|
||||
raw = updatedTool
|
||||
changed = true
|
||||
raw, toolChanged, ok := normalizeXAITool(tool)
|
||||
if !ok {
|
||||
return body
|
||||
}
|
||||
if toolType == xaiWebSearchToolType && tool.Get("external_web_access").Exists() {
|
||||
updatedTool, errDel := sjson.DeleteBytes(raw, "external_web_access")
|
||||
if errDel != nil {
|
||||
return body
|
||||
}
|
||||
raw = updatedTool
|
||||
changed = true
|
||||
changed = changed || toolChanged
|
||||
if len(raw) == 0 {
|
||||
continue
|
||||
}
|
||||
updated, errSet := sjson.SetRawBytes(filtered, "-1", raw)
|
||||
if errSet != nil {
|
||||
@@ -705,6 +710,35 @@ func normalizeXAITools(body []byte) []byte {
|
||||
return updated
|
||||
}
|
||||
|
||||
func normalizeXAITool(tool gjson.Result) ([]byte, bool, bool) {
|
||||
toolType := tool.Get("type").String()
|
||||
changed := false
|
||||
if toolType == xaiToolSearchType || toolType == xaiImageGenerationToolType {
|
||||
return nil, true, true
|
||||
}
|
||||
raw := []byte(tool.Raw)
|
||||
if toolType == xaiCustomToolType {
|
||||
if tool.Get("name").String() == "apply_patch" {
|
||||
return nil, true, true
|
||||
}
|
||||
updatedTool, errSet := sjson.SetBytes(raw, "type", xaiFunctionToolType)
|
||||
if errSet != nil {
|
||||
return nil, false, false
|
||||
}
|
||||
raw = updatedTool
|
||||
changed = true
|
||||
}
|
||||
if toolType == xaiWebSearchToolType && tool.Get("external_web_access").Exists() {
|
||||
updatedTool, errDel := sjson.DeleteBytes(raw, "external_web_access")
|
||||
if errDel != nil {
|
||||
return nil, false, false
|
||||
}
|
||||
raw = updatedTool
|
||||
changed = true
|
||||
}
|
||||
return raw, changed, true
|
||||
}
|
||||
|
||||
func normalizeXAIInputReasoningItems(body []byte) []byte {
|
||||
input := gjson.GetBytes(body, "input")
|
||||
if !input.Exists() || !input.IsArray() {
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestXAIExecutorExecuteShapesResponsesRequest(t *testing.T) {
|
||||
|
||||
_, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
||||
Model: "grok-4.3",
|
||||
Payload: []byte(`{"model":"grok-4.3","input":[{"type":"reasoning","summary":[{"type":"summary_text","text":"test"}],"content":null,"encrypted_content":null},{"role":"user","content":"hello"}],"include":["reasoning.encrypted_content"],"reasoning":{"effort":"high"},"tools":[{"type":"tool_search"},{"type":"image_generation"},{"type":"custom","name":"apply_patch"},{"type":"custom","name":"custom_lookup"},{"type":"function","name":"lookup"},{"type":"web_search","external_web_access":true,"search_content_types":["text","image"]}]}`),
|
||||
Payload: []byte(`{"model":"grok-4.3","input":[{"type":"reasoning","summary":[{"type":"summary_text","text":"test"}],"content":null,"encrypted_content":null},{"role":"user","content":"hello"}],"include":["reasoning.encrypted_content"],"reasoning":{"effort":"high"},"tools":[{"type":"tool_search"},{"type":"image_generation"},{"type":"custom","name":"apply_patch"},{"type":"custom","name":"custom_lookup"},{"type":"function","name":"lookup"},{"type":"web_search","external_web_access":true,"search_content_types":["text","image"]},{"type":"namespace","name":"codex_app","description":"Tools in the codex_app namespace.","tools":[{"type":"function","name":"automation_update"},{"type":"custom","name":"namespace_custom"},{"type":"tool_search"}]}]}`),
|
||||
}, cliproxyexecutor.Options{
|
||||
SourceFormat: sdktranslator.FormatOpenAIResponse,
|
||||
Stream: false,
|
||||
@@ -101,9 +101,11 @@ func TestXAIExecutorExecuteShapesResponsesRequest(t *testing.T) {
|
||||
t.Fatalf("input.0.summary.0.text = %q, want test; body=%s", got, string(gotBody))
|
||||
}
|
||||
tools := gjson.GetBytes(gotBody, "tools").Array()
|
||||
if len(tools) != 3 {
|
||||
t.Fatalf("tools length = %d, want 3; body=%s", len(tools), string(gotBody))
|
||||
if len(tools) != 5 {
|
||||
t.Fatalf("tools length = %d, want 5; body=%s", len(tools), string(gotBody))
|
||||
}
|
||||
foundAutomationUpdate := false
|
||||
foundNamespaceCustom := false
|
||||
for i, tool := range tools {
|
||||
toolType := tool.Get("type").String()
|
||||
if toolType == "image_generation" {
|
||||
@@ -115,6 +117,12 @@ func TestXAIExecutorExecuteShapesResponsesRequest(t *testing.T) {
|
||||
if got := tool.Get("name").String(); got == "apply_patch" {
|
||||
t.Fatalf("tools.%d.name = apply_patch, want removed; body=%s", i, string(gotBody))
|
||||
}
|
||||
switch tool.Get("name").String() {
|
||||
case "automation_update":
|
||||
foundAutomationUpdate = true
|
||||
case "namespace_custom":
|
||||
foundNamespaceCustom = true
|
||||
}
|
||||
if toolType == "web_search" {
|
||||
if tool.Get("external_web_access").Exists() {
|
||||
t.Fatalf("tools.%d.external_web_access exists, want removed; body=%s", i, string(gotBody))
|
||||
@@ -124,6 +132,12 @@ func TestXAIExecutorExecuteShapesResponsesRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundAutomationUpdate {
|
||||
t.Fatalf("namespace function tool was not moved to top-level tools; body=%s", string(gotBody))
|
||||
}
|
||||
if !foundNamespaceCustom {
|
||||
t.Fatalf("namespace custom tool was not moved to top-level tools; body=%s", string(gotBody))
|
||||
}
|
||||
for _, include := range gjson.GetBytes(gotBody, "include").Array() {
|
||||
if include.String() == "reasoning.encrypted_content" {
|
||||
t.Fatalf("xai request must not ask for encrypted reasoning content: %s", string(gotBody))
|
||||
@@ -192,7 +206,7 @@ func TestXAIExecutorExecuteStreamFiltersToolSearchTool(t *testing.T) {
|
||||
|
||||
result, err := exec.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
|
||||
Model: "grok-4.3",
|
||||
Payload: []byte(`{"model":"grok-4.3","input":[{"type":"reasoning","summary":[{"type":"summary_text","text":"test"}],"content":null,"encrypted_content":null},{"role":"user","content":"hello"}],"tools":[{"type":"tool_search"},{"type":"image_generation"},{"type":"custom","name":"apply_patch"},{"type":"custom","name":"custom_lookup"},{"type":"function","name":"lookup"},{"type":"web_search","external_web_access":true,"search_content_types":["text","image"]}]}`),
|
||||
Payload: []byte(`{"model":"grok-4.3","input":[{"type":"reasoning","summary":[{"type":"summary_text","text":"test"}],"content":null,"encrypted_content":null},{"role":"user","content":"hello"}],"tools":[{"type":"tool_search"},{"type":"image_generation"},{"type":"custom","name":"apply_patch"},{"type":"custom","name":"custom_lookup"},{"type":"function","name":"lookup"},{"type":"web_search","external_web_access":true,"search_content_types":["text","image"]},{"type":"namespace","name":"codex_app","description":"Tools in the codex_app namespace.","tools":[{"type":"function","name":"automation_update"},{"type":"custom","name":"namespace_custom"},{"type":"tool_search"}]}]}`),
|
||||
}, cliproxyexecutor.Options{
|
||||
SourceFormat: sdktranslator.FormatOpenAIResponse,
|
||||
Stream: true,
|
||||
@@ -207,8 +221,8 @@ func TestXAIExecutorExecuteStreamFiltersToolSearchTool(t *testing.T) {
|
||||
}
|
||||
|
||||
tools := gjson.GetBytes(gotBody, "tools").Array()
|
||||
if len(tools) != 3 {
|
||||
t.Fatalf("tools length = %d, want 3; body=%s", len(tools), string(gotBody))
|
||||
if len(tools) != 5 {
|
||||
t.Fatalf("tools length = %d, want 5; body=%s", len(tools), string(gotBody))
|
||||
}
|
||||
if gjson.GetBytes(gotBody, "input.0.content").Exists() {
|
||||
t.Fatalf("input.0.content exists, want removed; body=%s", string(gotBody))
|
||||
@@ -219,6 +233,8 @@ func TestXAIExecutorExecuteStreamFiltersToolSearchTool(t *testing.T) {
|
||||
if got := gjson.GetBytes(gotBody, "input.0.summary.0.text").String(); got != "test" {
|
||||
t.Fatalf("input.0.summary.0.text = %q, want test; body=%s", got, string(gotBody))
|
||||
}
|
||||
foundAutomationUpdate := false
|
||||
foundNamespaceCustom := false
|
||||
for i, tool := range tools {
|
||||
toolType := tool.Get("type").String()
|
||||
if toolType == "image_generation" {
|
||||
@@ -230,6 +246,12 @@ func TestXAIExecutorExecuteStreamFiltersToolSearchTool(t *testing.T) {
|
||||
if got := tool.Get("name").String(); got == "apply_patch" {
|
||||
t.Fatalf("tools.%d.name = apply_patch, want removed; body=%s", i, string(gotBody))
|
||||
}
|
||||
switch tool.Get("name").String() {
|
||||
case "automation_update":
|
||||
foundAutomationUpdate = true
|
||||
case "namespace_custom":
|
||||
foundNamespaceCustom = true
|
||||
}
|
||||
if toolType == "web_search" {
|
||||
if tool.Get("external_web_access").Exists() {
|
||||
t.Fatalf("tools.%d.external_web_access exists, want removed; body=%s", i, string(gotBody))
|
||||
@@ -239,6 +261,12 @@ func TestXAIExecutorExecuteStreamFiltersToolSearchTool(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundAutomationUpdate {
|
||||
t.Fatalf("namespace function tool was not moved to top-level tools; body=%s", string(gotBody))
|
||||
}
|
||||
if !foundNamespaceCustom {
|
||||
t.Fatalf("namespace custom tool was not moved to top-level tools; body=%s", string(gotBody))
|
||||
}
|
||||
}
|
||||
|
||||
func TestXAIExecutorExecuteImagesUsesImagesEndpoint(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user