From da48df06d204a59272e8963fe4d31554c42b59c1 Mon Sep 17 00:00:00 2001 From: 0xff26b9a8 <25315788+0xff26b9a8@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:41:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor(antigravity):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=B9=B6=E5=90=8C=E6=AD=A5=20Schema=20=E6=B8=85=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E8=87=B3=20schema=5Fcleaner.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要变更: 1. 重构代码结构: - 将 CleanJSONSchema 及其相关辅助函数从 request_transformer.go 提取到独立的 schema_cleaner.go 文件中,实现逻辑解耦。 2. 逻辑优化与修正: - 参考 Antigravity-Manager (json_schema.rs) 的实现逻辑,修正了 Schema 清洗策略。 --- .../pkg/antigravity/request_transformer.go | 242 +------- .../pkg/antigravity/response_transformer.go | 9 + .../pkg/antigravity/schema_cleaner.go | 526 ++++++++++++++++++ .../pkg/antigravity/stream_transformer.go | 9 + .../service/antigravity_gateway_service.go | 87 +++ 5 files changed, 636 insertions(+), 237 deletions(-) create mode 100644 backend/internal/pkg/antigravity/schema_cleaner.go diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index 637a4ea8..1b21bd58 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -7,13 +7,11 @@ import ( "fmt" "log" "math/rand" - "os" "strconv" "strings" "sync" "time" - "github.com/gin-gonic/gin" "github.com/google/uuid" ) @@ -594,11 +592,14 @@ func buildTools(tools []ClaudeTool) []GeminiToolDeclaration { } // 清理 JSON Schema - params := cleanJSONSchema(inputSchema) + // 1. 深度清理 [undefined] 值 + DeepCleanUndefined(inputSchema) + // 2. 转换为符合 Gemini v1internal 的 schema + params := CleanJSONSchema(inputSchema) // 为 nil schema 提供默认值 if params == nil { params = map[string]any{ - "type": "OBJECT", + "type": "object", // lowercase type "properties": map[string]any{}, } } @@ -631,236 +632,3 @@ func buildTools(tools []ClaudeTool) []GeminiToolDeclaration { FunctionDeclarations: funcDecls, }} } - -// cleanJSONSchema 清理 JSON Schema,移除 Antigravity/Gemini 不支持的字段 -// 参考 proxycast 的实现,确保 schema 符合 JSON Schema draft 2020-12 -func cleanJSONSchema(schema map[string]any) map[string]any { - if schema == nil { - return nil - } - cleaned := cleanSchemaValue(schema, "$") - result, ok := cleaned.(map[string]any) - if !ok { - return nil - } - - // 确保有 type 字段(默认 OBJECT) - if _, hasType := result["type"]; !hasType { - result["type"] = "OBJECT" - } - - // 确保有 properties 字段(默认空对象) - if _, hasProps := result["properties"]; !hasProps { - result["properties"] = make(map[string]any) - } - - // 验证 required 中的字段都存在于 properties 中 - if required, ok := result["required"].([]any); ok { - if props, ok := result["properties"].(map[string]any); ok { - validRequired := make([]any, 0, len(required)) - for _, r := range required { - if reqName, ok := r.(string); ok { - if _, exists := props[reqName]; exists { - validRequired = append(validRequired, r) - } - } - } - if len(validRequired) > 0 { - result["required"] = validRequired - } else { - delete(result, "required") - } - } - } - - return result -} - -var schemaValidationKeys = map[string]bool{ - "minLength": true, - "maxLength": true, - "pattern": true, - "minimum": true, - "maximum": true, - "exclusiveMinimum": true, - "exclusiveMaximum": true, - "multipleOf": true, - "uniqueItems": true, - "minItems": true, - "maxItems": true, - "minProperties": true, - "maxProperties": true, - "patternProperties": true, - "propertyNames": true, - "dependencies": true, - "dependentSchemas": true, - "dependentRequired": true, -} - -var warnedSchemaKeys sync.Map - -func schemaCleaningWarningsEnabled() bool { - // 可通过环境变量强制开关,方便排查:SUB2API_SCHEMA_CLEAN_WARN=true/false - if v := strings.TrimSpace(os.Getenv("SUB2API_SCHEMA_CLEAN_WARN")); v != "" { - switch strings.ToLower(v) { - case "1", "true", "yes", "on": - return true - case "0", "false", "no", "off": - return false - } - } - // 默认:非 release 模式下输出(debug/test) - return gin.Mode() != gin.ReleaseMode -} - -func warnSchemaKeyRemovedOnce(key, path string) { - if !schemaCleaningWarningsEnabled() { - return - } - if !schemaValidationKeys[key] { - return - } - if _, loaded := warnedSchemaKeys.LoadOrStore(key, struct{}{}); loaded { - return - } - log.Printf("[SchemaClean] removed unsupported JSON Schema validation field key=%q path=%q", key, path) -} - -// excludedSchemaKeys 不支持的 schema 字段 -// 基于 Claude API (Vertex AI) 的实际支持情况 -// 支持: type, description, enum, properties, required, additionalProperties, items -// 不支持: minItems, maxItems, minLength, maxLength, pattern, minimum, maximum 等验证字段 -var excludedSchemaKeys = map[string]bool{ - // 元 schema 字段 - "$schema": true, - "$id": true, - "$ref": true, - - // 字符串验证(Gemini 不支持) - "minLength": true, - "maxLength": true, - "pattern": true, - - // 数字验证(Claude API 通过 Vertex AI 不支持这些字段) - "minimum": true, - "maximum": true, - "exclusiveMinimum": true, - "exclusiveMaximum": true, - "multipleOf": true, - - // 数组验证(Claude API 通过 Vertex AI 不支持这些字段) - "uniqueItems": true, - "minItems": true, - "maxItems": true, - - // 组合 schema(Gemini 不支持) - "oneOf": true, - "anyOf": true, - "allOf": true, - "not": true, - "if": true, - "then": true, - "else": true, - "$defs": true, - "definitions": true, - - // 对象验证(仅保留 properties/required/additionalProperties) - "minProperties": true, - "maxProperties": true, - "patternProperties": true, - "propertyNames": true, - "dependencies": true, - "dependentSchemas": true, - "dependentRequired": true, - - // 其他不支持的字段 - "default": true, - "const": true, - "examples": true, - "deprecated": true, - "readOnly": true, - "writeOnly": true, - "contentMediaType": true, - "contentEncoding": true, - - // Claude 特有字段 - "strict": true, -} - -// cleanSchemaValue 递归清理 schema 值 -func cleanSchemaValue(value any, path string) any { - switch v := value.(type) { - case map[string]any: - result := make(map[string]any) - for k, val := range v { - // 跳过不支持的字段 - if excludedSchemaKeys[k] { - warnSchemaKeyRemovedOnce(k, path) - continue - } - - // 特殊处理 type 字段 - if k == "type" { - result[k] = cleanTypeValue(val) - continue - } - - // 特殊处理 format 字段:只保留 Gemini 支持的 format 值 - if k == "format" { - if formatStr, ok := val.(string); ok { - // Gemini 只支持 date-time, date, time - if formatStr == "date-time" || formatStr == "date" || formatStr == "time" { - result[k] = val - } - // 其他 format 值直接跳过 - } - continue - } - - // 特殊处理 additionalProperties:Claude API 只支持布尔值,不支持 schema 对象 - if k == "additionalProperties" { - if boolVal, ok := val.(bool); ok { - result[k] = boolVal - } else { - // 如果是 schema 对象,转换为 false(更安全的默认值) - result[k] = false - } - continue - } - - // 递归清理所有值 - result[k] = cleanSchemaValue(val, path+"."+k) - } - return result - - case []any: - // 递归处理数组中的每个元素 - cleaned := make([]any, 0, len(v)) - for i, item := range v { - cleaned = append(cleaned, cleanSchemaValue(item, fmt.Sprintf("%s[%d]", path, i))) - } - return cleaned - - default: - return value - } -} - -// cleanTypeValue 处理 type 字段,转换为大写 -func cleanTypeValue(value any) any { - switch v := value.(type) { - case string: - return strings.ToUpper(v) - case []any: - // 联合类型 ["string", "null"] -> 取第一个非 null 类型 - for _, t := range v { - if ts, ok := t.(string); ok && ts != "null" { - return strings.ToUpper(ts) - } - } - // 如果只有 null,返回 STRING - return "STRING" - default: - return value - } -} diff --git a/backend/internal/pkg/antigravity/response_transformer.go b/backend/internal/pkg/antigravity/response_transformer.go index 04424c03..a605fee2 100644 --- a/backend/internal/pkg/antigravity/response_transformer.go +++ b/backend/internal/pkg/antigravity/response_transformer.go @@ -3,6 +3,7 @@ package antigravity import ( "encoding/json" "fmt" + "log" "strings" ) @@ -242,6 +243,14 @@ func (p *NonStreamingProcessor) buildResponse(geminiResp *GeminiResponse, respon var finishReason string if len(geminiResp.Candidates) > 0 { finishReason = geminiResp.Candidates[0].FinishReason + if finishReason == "MALFORMED_FUNCTION_CALL" { + log.Printf("[Antigravity] MALFORMED_FUNCTION_CALL detected in response for model %s", originalModel) + if geminiResp.Candidates[0].Content != nil { + if b, err := json.Marshal(geminiResp.Candidates[0].Content); err == nil { + log.Printf("[Antigravity] Malformed content: %s", string(b)) + } + } + } } stopReason := "end_turn" diff --git a/backend/internal/pkg/antigravity/schema_cleaner.go b/backend/internal/pkg/antigravity/schema_cleaner.go new file mode 100644 index 00000000..1a06b2c8 --- /dev/null +++ b/backend/internal/pkg/antigravity/schema_cleaner.go @@ -0,0 +1,526 @@ +package antigravity + +import ( + "fmt" + "strings" +) + +// CleanJSONSchema 清理 JSON Schema,移除 Antigravity/Gemini 不支持的字段 +// 参考 Antigravity-Manager/src-tauri/src/proxy/common/json_schema.rs 实现 +// 确保 schema 符合 JSON Schema draft 2020-12 且适配 Gemini v1internal +func CleanJSONSchema(schema map[string]any) map[string]any { + if schema == nil { + return nil + } + // 0. 预处理:展开 $ref (Schema Flattening) + // (Go map 是引用的,直接修改 schema) + flattenRefs(schema, extractDefs(schema)) + + // 递归清理 + cleaned := cleanJSONSchemaRecursive(schema) + result, ok := cleaned.(map[string]any) + if !ok { + return nil + } + + return result +} + +// extractDefs 提取并移除定义的 helper +func extractDefs(schema map[string]any) map[string]any { + defs := make(map[string]any) + if d, ok := schema["$defs"].(map[string]any); ok { + for k, v := range d { + defs[k] = v + } + delete(schema, "$defs") + } + if d, ok := schema["definitions"].(map[string]any); ok { + for k, v := range d { + defs[k] = v + } + delete(schema, "definitions") + } + return defs +} + +// flattenRefs 递归展开 $ref +func flattenRefs(schema map[string]any, defs map[string]any) { + if len(defs) == 0 { + return // 无需展开 + } + + // 检查并替换 $ref + if ref, ok := schema["$ref"].(string); ok { + delete(schema, "$ref") + // 解析引用名 (例如 #/$defs/MyType -> MyType) + parts := strings.Split(ref, "/") + refName := parts[len(parts)-1] + + if defSchema, exists := defs[refName]; exists { + if defMap, ok := defSchema.(map[string]any); ok { + // 合并定义内容 (不覆盖现有 key) + for k, v := range defMap { + if _, has := schema[k]; !has { + schema[k] = deepCopy(v) // 需深拷贝避免共享引用 + } + } + // 递归处理刚刚合并进来的内容 + flattenRefs(schema, defs) + } + } + } + + // 遍历子节点 + for _, v := range schema { + if subMap, ok := v.(map[string]any); ok { + flattenRefs(subMap, defs) + } else if subArr, ok := v.([]any); ok { + for _, item := range subArr { + if itemMap, ok := item.(map[string]any); ok { + flattenRefs(itemMap, defs) + } + } + } + } +} + +// deepCopy 深拷贝 (简单实现,仅针对 JSON 类型) +func deepCopy(src any) any { + if src == nil { + return nil + } + switch v := src.(type) { + case map[string]any: + dst := make(map[string]any) + for k, val := range v { + dst[k] = deepCopy(val) + } + return dst + case []any: + dst := make([]any, len(v)) + for i, val := range v { + dst[i] = deepCopy(val) + } + return dst + default: + return src + } +} + +// cleanJSONSchemaRecursive 递归核心清理逻辑 +// 返回处理后的值 (通常是 input map,但可能修改内部结构) +func cleanJSONSchemaRecursive(value any) any { + schemaMap, ok := value.(map[string]any) + if !ok { + return value + } + + // 0. [NEW] 合并 allOf + mergeAllOf(schemaMap) + + // 1. [CRITICAL] 深度递归处理子项 + if props, ok := schemaMap["properties"].(map[string]any); ok { + for _, v := range props { + cleanJSONSchemaRecursive(v) + } + // Go 中不需要像 Rust 那样显式处理 nullable_keys remove required, + // 因为我们在子项处理中会正确设置 type 和 description + } else if items, ok := schemaMap["items"]; ok { + // [FIX] Gemini 期望 "items" 是单个 Schema 对象(列表验证),而不是数组(元组验证)。 + if itemsArr, ok := items.([]any); ok { + // 策略:将元组 [A, B] 视为 A、B 中的最佳匹配项。 + best := extractBestSchemaFromUnion(itemsArr) + if best == nil { + // 回退到通用字符串 + best = map[string]any{"type": "string"} + } + // 用处理后的对象替换原有数组 + cleanedBest := cleanJSONSchemaRecursive(best) + schemaMap["items"] = cleanedBest + } else { + cleanJSONSchemaRecursive(items) + } + } else { + // 遍历所有值递归 + for _, v := range schemaMap { + if _, isMap := v.(map[string]any); isMap { + cleanJSONSchemaRecursive(v) + } else if _, isArr := v.([]any); isArr { + // 数组内的对象也要递归 + } + } + // 稍微补全一下数组递归 + for k, v := range schemaMap { + if arr, ok := v.([]any); ok { + for _, item := range arr { + cleanJSONSchemaRecursive(item) + } + schemaMap[k] = arr + } + } + } + + // 2. [FIX] 处理 anyOf/oneOf 联合类型: 合并属性而非直接删除 + var unionArray []any + typeStr, _ := schemaMap["type"].(string) + if typeStr == "" || typeStr == "object" { + if anyOf, ok := schemaMap["anyOf"].([]any); ok { + unionArray = anyOf + } else if oneOf, ok := schemaMap["oneOf"].([]any); ok { + unionArray = oneOf + } + } + + if len(unionArray) > 0 { + if bestBranch := extractBestSchemaFromUnion(unionArray); bestBranch != nil { + if bestMap, ok := bestBranch.(map[string]any); ok { + // 合并分支内容 + for k, v := range bestMap { + if k == "properties" { + targetProps, _ := schemaMap["properties"].(map[string]any) + if targetProps == nil { + targetProps = make(map[string]any) + schemaMap["properties"] = targetProps + } + if sourceProps, ok := v.(map[string]any); ok { + for pk, pv := range sourceProps { + if _, exists := targetProps[pk]; !exists { + targetProps[pk] = deepCopy(pv) + } + } + } + } else if k == "required" { + targetReq, _ := schemaMap["required"].([]any) + if sourceReq, ok := v.([]any); ok { + for _, rv := range sourceReq { + // 简单的去重添加 + exists := false + for _, tr := range targetReq { + if tr == rv { + exists = true + break + } + } + if !exists { + targetReq = append(targetReq, rv) + } + } + schemaMap["required"] = targetReq + } + } else if _, exists := schemaMap[k]; !exists { + schemaMap[k] = deepCopy(v) + } + } + } + } + } + + // 3. [SAFETY] 检查当前对象是否为 JSON Schema 节点 + looksLikeSchema := hasKey(schemaMap, "type") || + hasKey(schemaMap, "properties") || + hasKey(schemaMap, "items") || + hasKey(schemaMap, "enum") || + hasKey(schemaMap, "anyOf") || + hasKey(schemaMap, "oneOf") || + hasKey(schemaMap, "allOf") + + if looksLikeSchema { + // 4. [ROBUST] 约束迁移 + migrateConstraints(schemaMap) + + // 5. [CRITICAL] 白名单过滤 + allowedFields := map[string]bool{ + "type": true, + "description": true, + "properties": true, + "required": true, + "items": true, + "enum": true, + "title": true, + } + for k := range schemaMap { + if !allowedFields[k] { + delete(schemaMap, k) + } + } + + // 6. [SAFETY] 处理空 Object + if t, _ := schemaMap["type"].(string); t == "object" { + hasProps := false + if props, ok := schemaMap["properties"].(map[string]any); ok && len(props) > 0 { + hasProps = true + } + if !hasProps { + schemaMap["properties"] = map[string]any{ + "reason": map[string]any{ + "type": "string", + "description": "Reason for calling this tool", + }, + } + schemaMap["required"] = []any{"reason"} + } + } + + // 7. [SAFETY] Required 字段对齐 + if props, ok := schemaMap["properties"].(map[string]any); ok { + if req, ok := schemaMap["required"].([]any); ok { + var validReq []any + for _, r := range req { + if rStr, ok := r.(string); ok { + if _, exists := props[rStr]; exists { + validReq = append(validReq, r) + } + } + } + if len(validReq) > 0 { + schemaMap["required"] = validReq + } else { + delete(schemaMap, "required") + } + } + } + + // 8. 处理 type 字段 (Lowercase + Nullable 提取) + isEffectivelyNullable := false + if typeVal, exists := schemaMap["type"]; exists { + var selectedType string + switch v := typeVal.(type) { + case string: + lower := strings.ToLower(v) + if lower == "null" { + isEffectivelyNullable = true + selectedType = "string" // fallback + } else { + selectedType = lower + } + case []any: + // ["string", "null"] + for _, t := range v { + if ts, ok := t.(string); ok { + lower := strings.ToLower(ts) + if lower == "null" { + isEffectivelyNullable = true + } else if selectedType == "" { + selectedType = lower + } + } + } + if selectedType == "" { + selectedType = "string" + } + } + schemaMap["type"] = selectedType + } else { + // 默认 object 如果有 properties (虽然上面白名单过滤可能删了 type 如果它不在... 但 type 必在 allowlist) + // 如果没有 type,但有 properties,补一个 + if hasKey(schemaMap, "properties") { + schemaMap["type"] = "object" + } else { + // 默认为 string ? or object? Gemini 通常需要明确 type + schemaMap["type"] = "object" + } + } + + if isEffectivelyNullable { + desc, _ := schemaMap["description"].(string) + if !strings.Contains(desc, "nullable") { + if desc != "" { + desc += " " + } + desc += "(nullable)" + schemaMap["description"] = desc + } + } + + // 9. Enum 值强制转字符串 + if enumVals, ok := schemaMap["enum"].([]any); ok { + hasNonString := false + for i, val := range enumVals { + if _, isStr := val.(string); !isStr { + hasNonString = true + if val == nil { + enumVals[i] = "null" + } else { + enumVals[i] = fmt.Sprintf("%v", val) + } + } + } + // If we mandated string values, we must ensure type is string + if hasNonString { + schemaMap["type"] = "string" + } + } + } + + return schemaMap +} + +func hasKey(m map[string]any, k string) bool { + _, ok := m[k] + return ok +} + +func migrateConstraints(m map[string]any) { + constraints := []struct { + key string + label string + }{ + {"minLength", "minLen"}, + {"maxLength", "maxLen"}, + {"pattern", "pattern"}, + {"minimum", "min"}, + {"maximum", "max"}, + {"multipleOf", "multipleOf"}, + {"exclusiveMinimum", "exclMin"}, + {"exclusiveMaximum", "exclMax"}, + {"minItems", "minItems"}, + {"maxItems", "maxItems"}, + {"propertyNames", "propertyNames"}, + {"format", "format"}, + } + + var hints []string + for _, c := range constraints { + if val, ok := m[c.key]; ok && val != nil { + hints = append(hints, fmt.Sprintf("%s: %v", c.label, val)) + } + } + + if len(hints) > 0 { + suffix := fmt.Sprintf(" [Constraint: %s]", strings.Join(hints, ", ")) + desc, _ := m["description"].(string) + if !strings.Contains(desc, suffix) { + m["description"] = desc + suffix + } + } +} + +// mergeAllOf 合并 allOf +func mergeAllOf(m map[string]any) { + allOf, ok := m["allOf"].([]any) + if !ok { + return + } + delete(m, "allOf") + + mergedProps := make(map[string]any) + mergedReq := make(map[string]bool) + otherFields := make(map[string]any) + + for _, sub := range allOf { + if subMap, ok := sub.(map[string]any); ok { + // Props + if props, ok := subMap["properties"].(map[string]any); ok { + for k, v := range props { + mergedProps[k] = v + } + } + // Required + if reqs, ok := subMap["required"].([]any); ok { + for _, r := range reqs { + if s, ok := r.(string); ok { + mergedReq[s] = true + } + } + } + // Others + for k, v := range subMap { + if k != "properties" && k != "required" && k != "allOf" { + if _, exists := otherFields[k]; !exists { + otherFields[k] = v + } + } + } + } + } + + // Apply + for k, v := range otherFields { + if _, exists := m[k]; !exists { + m[k] = v + } + } + if len(mergedProps) > 0 { + existProps, _ := m["properties"].(map[string]any) + if existProps == nil { + existProps = make(map[string]any) + m["properties"] = existProps + } + for k, v := range mergedProps { + if _, exists := existProps[k]; !exists { + existProps[k] = v + } + } + } + if len(mergedReq) > 0 { + existReq, _ := m["required"].([]any) + var currentReqs []string + for _, r := range existReq { + if s, ok := r.(string); ok { + currentReqs = append(currentReqs, s) + delete(mergedReq, s) // already exists + } + } + // append new + for r := range mergedReq { + existReq = append(existReq, r) + } + m["required"] = existReq + } +} + +// extractBestSchemaFromUnion 从 anyOf/oneOf 中选取最佳分支 +func extractBestSchemaFromUnion(unionArray []any) any { + var bestOption any + bestScore := -1 + + for _, item := range unionArray { + score := scoreSchemaOption(item) + if score > bestScore { + bestScore = score + bestOption = item + } + } + return bestOption +} + +func scoreSchemaOption(val any) int { + m, ok := val.(map[string]any) + if !ok { + return 0 + } + typeStr, _ := m["type"].(string) + + if hasKey(m, "properties") || typeStr == "object" { + return 3 + } + if hasKey(m, "items") || typeStr == "array" { + return 2 + } + if typeStr != "" && typeStr != "null" { + return 1 + } + return 0 +} + +// DeepCleanUndefined 深度清理值为 "[undefined]" 的字段 +func DeepCleanUndefined(value any) { + if value == nil { + return + } + switch v := value.(type) { + case map[string]any: + for k, val := range v { + if s, ok := val.(string); ok && s == "[undefined]" { + delete(v, k) + continue + } + DeepCleanUndefined(val) + } + case []any: + for _, val := range v { + DeepCleanUndefined(val) + } + } +} diff --git a/backend/internal/pkg/antigravity/stream_transformer.go b/backend/internal/pkg/antigravity/stream_transformer.go index da0c6f97..b384658a 100644 --- a/backend/internal/pkg/antigravity/stream_transformer.go +++ b/backend/internal/pkg/antigravity/stream_transformer.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "log" "strings" ) @@ -102,6 +103,14 @@ func (p *StreamingProcessor) ProcessLine(line string) []byte { // 检查是否结束 if len(geminiResp.Candidates) > 0 { finishReason := geminiResp.Candidates[0].FinishReason + if finishReason == "MALFORMED_FUNCTION_CALL" { + log.Printf("[Antigravity] MALFORMED_FUNCTION_CALL detected in stream for model %s", p.originalModel) + if geminiResp.Candidates[0].Content != nil { + if b, err := json.Marshal(geminiResp.Candidates[0].Content); err == nil { + log.Printf("[Antigravity] Malformed content: %s", string(b)) + } + } + } if finishReason != "" { _, _ = result.Write(p.emitFinish(finishReason)) } diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 043f338d..b951f634 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -1305,6 +1305,14 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co return nil, err } + // 清理 Schema + if cleanedBody, err := cleanGeminiRequest(injectedBody); err == nil { + injectedBody = cleanedBody + log.Printf("[Antigravity] Cleaned request schema in forwarded request for account %s", account.Name) + } else { + log.Printf("[Antigravity] Failed to clean schema: %v", err) + } + // 包装请求 wrappedBody, err := s.wrapV1InternalRequest(projectID, mappedModel, injectedBody) if err != nil { @@ -1705,6 +1713,19 @@ func (s *AntigravityGatewayService) handleGeminiStreamingResponse(c *gin.Context if u := extractGeminiUsage(parsed); u != nil { usage = u } + // Check for MALFORMED_FUNCTION_CALL + if candidates, ok := parsed["candidates"].([]any); ok && len(candidates) > 0 { + if cand, ok := candidates[0].(map[string]any); ok { + if fr, ok := cand["finishReason"].(string); ok && fr == "MALFORMED_FUNCTION_CALL" { + log.Printf("[Antigravity] MALFORMED_FUNCTION_CALL detected in forward stream") + if content, ok := cand["content"]; ok { + if b, err := json.Marshal(content); err == nil { + log.Printf("[Antigravity] Malformed content: %s", string(b)) + } + } + } + } + } } if firstTokenMs == nil { @@ -1854,6 +1875,20 @@ func (s *AntigravityGatewayService) handleGeminiStreamToNonStreaming(c *gin.Cont usage = u } + // Check for MALFORMED_FUNCTION_CALL + if candidates, ok := parsed["candidates"].([]any); ok && len(candidates) > 0 { + if cand, ok := candidates[0].(map[string]any); ok { + if fr, ok := cand["finishReason"].(string); ok && fr == "MALFORMED_FUNCTION_CALL" { + log.Printf("[Antigravity] MALFORMED_FUNCTION_CALL detected in forward non-stream collect") + if content, ok := cand["content"]; ok { + if b, err := json.Marshal(content); err == nil { + log.Printf("[Antigravity] Malformed content: %s", string(b)) + } + } + } + } + } + // 保留最后一个有 parts 的响应 if parts := extractGeminiParts(parsed); len(parts) > 0 { lastWithParts = parsed @@ -2459,3 +2494,55 @@ func isImageGenerationModel(model string) bool { modelLower == "gemini-2.5-flash-image-preview" || strings.HasPrefix(modelLower, "gemini-2.5-flash-image-") } + +// cleanGeminiRequest 清理 Gemini 请求体中的 Schema +func cleanGeminiRequest(body []byte) ([]byte, error) { + var payload map[string]any + if err := json.Unmarshal(body, &payload); err != nil { + return nil, err + } + + modified := false + + // 1. 清理 Tools + if tools, ok := payload["tools"].([]any); ok && len(tools) > 0 { + for _, t := range tools { + toolMap, ok := t.(map[string]any) + if !ok { + continue + } + + // function_declarations (snake_case) or functionDeclarations (camelCase) + var funcs []any + if f, ok := toolMap["functionDeclarations"].([]any); ok { + funcs = f + } else if f, ok := toolMap["function_declarations"].([]any); ok { + funcs = f + } + + if len(funcs) == 0 { + continue + } + + for _, f := range funcs { + funcMap, ok := f.(map[string]any) + if !ok { + continue + } + + if params, ok := funcMap["parameters"].(map[string]any); ok { + antigravity.DeepCleanUndefined(params) + cleaned := antigravity.CleanJSONSchema(params) + funcMap["parameters"] = cleaned + modified = true + } + } + } + } + + if !modified { + return body, nil + } + + return json.Marshal(payload) +}