mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 17:36:47 +08:00
feat(executor): simplify xAI function schema handling and rejection logic
- Refactored `xaiFunctionParametersNeedSimplification` to identify schemas xAI cannot process, including those with non-object types in unions. - Limited parameter simplifications to problematic schemas instead of specific tools or namespaces. - Clarified and updated debug messages to reflect schema rejection and hang prevention. Closes: #4343
This commit is contained in:
@@ -1579,9 +1579,8 @@ func normalizeXAITool(tool gjson.Result, namespaceName string) ([]byte, bool, bo
|
||||
raw = updatedTool
|
||||
changed = true
|
||||
}
|
||||
// Codex Desktop's codex_app.automation_update schema hangs xAI free/build
|
||||
// streaming. Limit the workaround to that exact namespaced tool so unrelated
|
||||
// tools keep their parameter contracts.
|
||||
// Simplify the Codex Desktop automation schema and root unions that xAI
|
||||
// rejects because function parameters must resolve exclusively to objects.
|
||||
if toolType == xaiFunctionToolType && xaiFunctionParametersNeedSimplification(tool, namespaceName) {
|
||||
updatedTool, errSet := sjson.SetRawBytes(raw, "parameters", []byte(xaiSafeFunctionParameters))
|
||||
if errSet != nil {
|
||||
@@ -1596,7 +1595,7 @@ func normalizeXAITool(tool gjson.Result, namespaceName string) ([]byte, bool, bo
|
||||
raw = updatedTool
|
||||
}
|
||||
changed = true
|
||||
log.Debugf("xai: simplified parameters for tool %s.%s to avoid upstream hang", namespaceName, tool.Get("name").String())
|
||||
log.Debugf("xai: simplified parameters for tool %s.%s to avoid upstream schema rejection or hang", namespaceName, tool.Get("name").String())
|
||||
}
|
||||
if toolType == xaiFunctionToolType && strings.TrimSpace(namespaceName) != "" {
|
||||
qualifiedName := qualifyXAINamespaceToolName(namespaceName, tool.Get("name").String())
|
||||
@@ -2086,12 +2085,55 @@ func restoreXAINamespaceToolCallAtPath(data []byte, path string, refs map[string
|
||||
return updated
|
||||
}
|
||||
|
||||
// xaiFunctionParametersNeedSimplification reports whether a function tool is
|
||||
// the Codex Desktop automation tool known to hang xAI Responses streaming.
|
||||
// xaiFunctionParametersNeedSimplification reports whether a function tool, or
|
||||
// a custom tool normalized to a function, has a schema that xAI cannot accept.
|
||||
func xaiFunctionParametersNeedSimplification(tool gjson.Result, namespaceName string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(tool.Get("type").String()), xaiFunctionToolType) &&
|
||||
strings.EqualFold(strings.TrimSpace(namespaceName), xaiCodexAppNamespaceName) &&
|
||||
strings.EqualFold(strings.TrimSpace(tool.Get("name").String()), xaiAutomationUpdateToolName)
|
||||
toolType := strings.TrimSpace(tool.Get("type").String())
|
||||
isFunction := strings.EqualFold(toolType, xaiFunctionToolType)
|
||||
isNormalizedCustom := strings.EqualFold(toolType, xaiCustomToolType)
|
||||
if !isFunction && !isNormalizedCustom {
|
||||
return false
|
||||
}
|
||||
|
||||
toolName := strings.TrimSpace(tool.Get("name").String())
|
||||
qualifiedAutomationName := xaiCodexAppNamespaceName + "__" + xaiAutomationUpdateToolName
|
||||
if isFunction && (strings.EqualFold(toolName, qualifiedAutomationName) ||
|
||||
(strings.EqualFold(strings.TrimSpace(namespaceName), xaiCodexAppNamespaceName) &&
|
||||
strings.EqualFold(toolName, xaiAutomationUpdateToolName))) {
|
||||
return true
|
||||
}
|
||||
|
||||
parameters := tool.Get("parameters")
|
||||
for _, unionName := range []string{"anyOf", "oneOf"} {
|
||||
union := parameters.Get(unionName)
|
||||
if !union.IsArray() {
|
||||
continue
|
||||
}
|
||||
for _, branch := range union.Array() {
|
||||
branchType := branch.Get("type")
|
||||
if branchType.Type == gjson.String {
|
||||
if !strings.EqualFold(strings.TrimSpace(branchType.String()), "object") {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !branchType.IsArray() {
|
||||
// Without an explicit object type, the branch may accept non-object
|
||||
// values even when it only declares object-specific keywords.
|
||||
return true
|
||||
}
|
||||
allowedTypes := branchType.Array()
|
||||
if len(allowedTypes) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, allowedType := range allowedTypes {
|
||||
if allowedType.Type != gjson.String || !strings.EqualFold(strings.TrimSpace(allowedType.String()), "object") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sanitizeXAIInputEncryptedContent(body []byte) []byte {
|
||||
|
||||
@@ -2313,6 +2313,45 @@ func TestNormalizeXAITools_SimplifiesCodexAppAutomationUpdateSchema(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeXAITools_SimplifiesFlattenedAndInvalidRootSchemas(t *testing.T) {
|
||||
body := []byte(`{"tools":[{"type":"function","name":"codex_app__automation_update","strict":true,"parameters":{"oneOf":[{"type":"object","properties":{"action":{"type":"string"}},"required":["action"]},{"type":"null"}]}},{"type":"function","name":"nullable_lookup","strict":true,"parameters":{"anyOf":[{"type":"object","properties":{"query":{"type":"string"}}},{"type":["object","null"]}]}},{"type":"custom","name":"nullable_custom","strict":true,"parameters":{"oneOf":[{"type":"object"},{"type":"null"}]}},{"type":"function","name":"echo_tool","strict":true,"parameters":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}]}`)
|
||||
out := normalizeXAITools(body)
|
||||
|
||||
tools := gjson.GetBytes(out, "tools").Array()
|
||||
if len(tools) != 4 {
|
||||
t.Fatalf("tools length = %d, want 4; body=%s", len(tools), string(out))
|
||||
}
|
||||
for index, wantName := range []string{"codex_app__automation_update", "nullable_lookup", "nullable_custom"} {
|
||||
tool := tools[index]
|
||||
if got := tool.Get("name").String(); got != wantName {
|
||||
t.Fatalf("tools.%d.name = %q, want %q; body=%s", index, got, wantName, string(out))
|
||||
}
|
||||
if got := tool.Get("type").String(); got != xaiFunctionToolType {
|
||||
t.Fatalf("tools.%d type = %q, want function; body=%s", index, got, string(out))
|
||||
}
|
||||
if got := tool.Get("parameters.type").String(); got != "object" {
|
||||
t.Fatalf("tools.%d parameters.type = %q, want object; body=%s", index, got, string(out))
|
||||
}
|
||||
if tool.Get("parameters.additionalProperties").Type != gjson.True {
|
||||
t.Fatalf("tools.%d parameters should allow additionalProperties: %s", index, string(out))
|
||||
}
|
||||
if tool.Get("strict").Type != gjson.False {
|
||||
t.Fatalf("tools.%d strict = %s, want false; body=%s", index, tool.Get("strict").Raw, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
echoTool := tools[3]
|
||||
if got := echoTool.Get("parameters.properties.message.type").String(); got != "string" {
|
||||
t.Fatalf("echo_tool schema changed, message type = %q; body=%s", got, string(out))
|
||||
}
|
||||
if echoTool.Get("strict").Type != gjson.True {
|
||||
t.Fatalf("echo_tool strict changed: %s", string(out))
|
||||
}
|
||||
if echoTool.Get("parameters.additionalProperties").Type != gjson.False {
|
||||
t.Fatalf("echo_tool additionalProperties changed: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeXAITools_QualifiesSameNamedNamespaceTools(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"tools":[
|
||||
@@ -2508,9 +2547,37 @@ func TestXAIFunctionParametersNeedSimplification(t *testing.T) {
|
||||
if xaiFunctionParametersNeedSimplification(auto, "") {
|
||||
t.Fatal("top-level automation_update should not need simplification")
|
||||
}
|
||||
flattened := gjson.Parse(`{"type":"function","name":"codex_app__automation_update","parameters":{"type":"object"}}`)
|
||||
if !xaiFunctionParametersNeedSimplification(flattened, "") {
|
||||
t.Fatal("flattened codex_app__automation_update should need simplification")
|
||||
}
|
||||
custom := gjson.Parse(`{"type":"custom","name":"automation_update","parameters":{"type":"object"}}`)
|
||||
if xaiFunctionParametersNeedSimplification(custom, "codex_app") {
|
||||
t.Fatal("custom codex_app.automation_update should not need simplification")
|
||||
t.Fatal("custom codex_app.automation_update with an object schema should not need simplification")
|
||||
}
|
||||
invalidCustom := gjson.Parse(`{"type":"custom","name":"nullable_lookup","parameters":{"oneOf":[{"type":"object"},{"type":"null"}]}}`)
|
||||
if !xaiFunctionParametersNeedSimplification(invalidCustom, "") {
|
||||
t.Fatal("custom tool normalized to a function should simplify an invalid root union")
|
||||
}
|
||||
invalidOneOf := gjson.Parse(`{"type":"function","name":"nullable_lookup","parameters":{"oneOf":[{"type":"object"},{"type":"null"}]}}`)
|
||||
if !xaiFunctionParametersNeedSimplification(invalidOneOf, "") {
|
||||
t.Fatal("root oneOf with a non-object branch should need simplification")
|
||||
}
|
||||
invalidAnyOf := gjson.Parse(`{"type":"function","name":"nullable_lookup","parameters":{"anyOf":[{"type":"object"},{"type":["object","null"]}]}}`)
|
||||
if !xaiFunctionParametersNeedSimplification(invalidAnyOf, "") {
|
||||
t.Fatal("root anyOf with a non-object type should need simplification")
|
||||
}
|
||||
untypedBranch := gjson.Parse(`{"type":"function","name":"nullable_lookup","parameters":{"oneOf":[{"type":"object"},{"const":null}]}}`)
|
||||
if !xaiFunctionParametersNeedSimplification(untypedBranch, "") {
|
||||
t.Fatal("root union with an untyped branch should need simplification")
|
||||
}
|
||||
objectUnion := gjson.Parse(`{"type":"function","name":"lookup","parameters":{"oneOf":[{"type":"object"},{"type":"object"}]}}`)
|
||||
if xaiFunctionParametersNeedSimplification(objectUnion, "") {
|
||||
t.Fatal("root union containing only object branches should not need simplification")
|
||||
}
|
||||
nestedUnion := gjson.Parse(`{"type":"function","name":"lookup","parameters":{"type":"object","properties":{"value":{"oneOf":[{"type":"string"},{"type":"null"}]}}}}`)
|
||||
if xaiFunctionParametersNeedSimplification(nestedUnion, "") {
|
||||
t.Fatal("nested union should not need root schema simplification")
|
||||
}
|
||||
safe := gjson.Parse(`{"type":"function","name":"exec_command","parameters":{"type":"object","properties":{"cmd":{"type":"string"}}}}`)
|
||||
if xaiFunctionParametersNeedSimplification(safe, "codex_app") {
|
||||
|
||||
Reference in New Issue
Block a user