mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 12:20:52 +08:00
fix(schema): remove Gemini-incompatible tool metadata fields
Sanitize tool schemas by stripping prefill, enumTitles, $id, and patternProperties to prevent Gemini INVALID_ARGUMENT 400 errors, and add unit and executor-level tests to lock in the behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAntigravityBuildRequest_SanitizesGeminiToolSchema(t *testing.T) {
|
||||||
|
body := buildRequestBodyFromPayload(t, "gemini-2.5-pro")
|
||||||
|
|
||||||
|
decl := extractFirstFunctionDeclaration(t, body)
|
||||||
|
if _, ok := decl["parametersJsonSchema"]; ok {
|
||||||
|
t.Fatalf("parametersJsonSchema should be renamed to parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
params, ok := decl["parameters"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("parameters missing or invalid type")
|
||||||
|
}
|
||||||
|
assertSchemaSanitizedAndPropertyPreserved(t, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAntigravityBuildRequest_SanitizesAntigravityToolSchema(t *testing.T) {
|
||||||
|
body := buildRequestBodyFromPayload(t, "claude-opus-4-6")
|
||||||
|
|
||||||
|
decl := extractFirstFunctionDeclaration(t, body)
|
||||||
|
params, ok := decl["parameters"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("parameters missing or invalid type")
|
||||||
|
}
|
||||||
|
assertSchemaSanitizedAndPropertyPreserved(t, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRequestBodyFromPayload(t *testing.T, modelName string) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
executor := &AntigravityExecutor{}
|
||||||
|
auth := &cliproxyauth.Auth{}
|
||||||
|
payload := []byte(`{
|
||||||
|
"request": {
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"function_declarations": [
|
||||||
|
{
|
||||||
|
"name": "tool_1",
|
||||||
|
"parametersJsonSchema": {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "root-schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"$id": {"type": "string"},
|
||||||
|
"arg": {
|
||||||
|
"type": "object",
|
||||||
|
"prefill": "hello",
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["a", "b"],
|
||||||
|
"enumTitles": ["A", "B"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patternProperties": {
|
||||||
|
"^x-": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
req, err := executor.buildRequest(context.Background(), auth, "token", modelName, payload, false, "", "https://example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildRequest error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := io.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read request body error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &body); err != nil {
|
||||||
|
t.Fatalf("unmarshal request body error: %v, body=%s", err, string(raw))
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFirstFunctionDeclaration(t *testing.T, body map[string]any) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
request, ok := body["request"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("request missing or invalid type")
|
||||||
|
}
|
||||||
|
tools, ok := request["tools"].([]any)
|
||||||
|
if !ok || len(tools) == 0 {
|
||||||
|
t.Fatalf("tools missing or empty")
|
||||||
|
}
|
||||||
|
tool, ok := tools[0].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("first tool invalid type")
|
||||||
|
}
|
||||||
|
decls, ok := tool["function_declarations"].([]any)
|
||||||
|
if !ok || len(decls) == 0 {
|
||||||
|
t.Fatalf("function_declarations missing or empty")
|
||||||
|
}
|
||||||
|
decl, ok := decls[0].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("first function declaration invalid type")
|
||||||
|
}
|
||||||
|
return decl
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSchemaSanitizedAndPropertyPreserved(t *testing.T, params map[string]any) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if _, ok := params["$id"]; ok {
|
||||||
|
t.Fatalf("root $id should be removed from schema")
|
||||||
|
}
|
||||||
|
if _, ok := params["patternProperties"]; ok {
|
||||||
|
t.Fatalf("patternProperties should be removed from schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
props, ok := params["properties"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("properties missing or invalid type")
|
||||||
|
}
|
||||||
|
if _, ok := props["$id"]; !ok {
|
||||||
|
t.Fatalf("property named $id should be preserved")
|
||||||
|
}
|
||||||
|
|
||||||
|
arg, ok := props["arg"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("arg property missing or invalid type")
|
||||||
|
}
|
||||||
|
if _, ok := arg["prefill"]; ok {
|
||||||
|
t.Fatalf("prefill should be removed from nested schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
argProps, ok := arg["properties"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("arg.properties missing or invalid type")
|
||||||
|
}
|
||||||
|
mode, ok := argProps["mode"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("mode property missing or invalid type")
|
||||||
|
}
|
||||||
|
if _, ok := mode["enumTitles"]; ok {
|
||||||
|
t.Fatalf("enumTitles should be removed from nested schema")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -428,8 +428,9 @@ func flattenTypeArrays(jsonStr string) string {
|
|||||||
|
|
||||||
func removeUnsupportedKeywords(jsonStr string) string {
|
func removeUnsupportedKeywords(jsonStr string) string {
|
||||||
keywords := append(unsupportedConstraints,
|
keywords := append(unsupportedConstraints,
|
||||||
"$schema", "$defs", "definitions", "const", "$ref", "additionalProperties",
|
"$schema", "$defs", "definitions", "const", "$ref", "$id", "additionalProperties",
|
||||||
"propertyNames", // Gemini doesn't support property name validation
|
"propertyNames", "patternProperties", // Gemini doesn't support these schema keywords
|
||||||
|
"enumTitles", "prefill", // Claude/OpenCode schema metadata fields unsupported by Gemini
|
||||||
)
|
)
|
||||||
|
|
||||||
deletePaths := make([]string, 0)
|
deletePaths := make([]string, 0)
|
||||||
|
|||||||
@@ -870,6 +870,57 @@ func TestCleanJSONSchemaForAntigravity_BooleanEnumToString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCleanJSONSchemaForGemini_RemovesGeminiUnsupportedMetadataFields(t *testing.T) {
|
||||||
|
input := `{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "root-schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"payload": {
|
||||||
|
"type": "object",
|
||||||
|
"prefill": "hello",
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["a", "b"],
|
||||||
|
"enumTitles": ["A", "B"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patternProperties": {
|
||||||
|
"^x-": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "property name should not be removed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"payload": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["a", "b"],
|
||||||
|
"description": "Allowed: a, b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "property name should not be removed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
result := CleanJSONSchemaForGemini(input)
|
||||||
|
compareJSON(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRemoveExtensionFields(t *testing.T) {
|
func TestRemoveExtensionFields(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user