Merge pull request #346 from 0xff26b9a8/main
refactor(antigravity): 提取并同步 Schema 清理逻辑至 schema_cleaner.go
This commit is contained in:
@@ -7,13 +7,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -594,11 +592,14 @@ func buildTools(tools []ClaudeTool) []GeminiToolDeclaration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 清理 JSON Schema
|
// 清理 JSON Schema
|
||||||
params := cleanJSONSchema(inputSchema)
|
// 1. 深度清理 [undefined] 值
|
||||||
|
DeepCleanUndefined(inputSchema)
|
||||||
|
// 2. 转换为符合 Gemini v1internal 的 schema
|
||||||
|
params := CleanJSONSchema(inputSchema)
|
||||||
// 为 nil schema 提供默认值
|
// 为 nil schema 提供默认值
|
||||||
if params == nil {
|
if params == nil {
|
||||||
params = map[string]any{
|
params = map[string]any{
|
||||||
"type": "OBJECT",
|
"type": "object", // lowercase type
|
||||||
"properties": map[string]any{},
|
"properties": map[string]any{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,236 +632,3 @@ func buildTools(tools []ClaudeTool) []GeminiToolDeclaration {
|
|||||||
FunctionDeclarations: funcDecls,
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package antigravity
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -242,6 +243,14 @@ func (p *NonStreamingProcessor) buildResponse(geminiResp *GeminiResponse, respon
|
|||||||
var finishReason string
|
var finishReason string
|
||||||
if len(geminiResp.Candidates) > 0 {
|
if len(geminiResp.Candidates) > 0 {
|
||||||
finishReason = geminiResp.Candidates[0].FinishReason
|
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"
|
stopReason := "end_turn"
|
||||||
|
|||||||
519
backend/internal/pkg/antigravity/schema_cleaner.go
Normal file
519
backend/internal/pkg/antigravity/schema_cleaner.go
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
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 arr, isArr := v.([]any); isArr {
|
||||||
|
for _, item := range arr {
|
||||||
|
cleanJSONSchemaRecursive(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 validReqs []any
|
||||||
|
for _, r := range existReq {
|
||||||
|
if s, ok := r.(string); ok {
|
||||||
|
validReqs = append(validReqs, s)
|
||||||
|
delete(mergedReq, s) // already exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// append new
|
||||||
|
for r := range mergedReq {
|
||||||
|
validReqs = append(validReqs, r)
|
||||||
|
}
|
||||||
|
m["required"] = validReqs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,6 +103,14 @@ func (p *StreamingProcessor) ProcessLine(line string) []byte {
|
|||||||
// 检查是否结束
|
// 检查是否结束
|
||||||
if len(geminiResp.Candidates) > 0 {
|
if len(geminiResp.Candidates) > 0 {
|
||||||
finishReason := geminiResp.Candidates[0].FinishReason
|
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 != "" {
|
if finishReason != "" {
|
||||||
_, _ = result.Write(p.emitFinish(finishReason))
|
_, _ = result.Write(p.emitFinish(finishReason))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1305,6 +1305,14 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
|||||||
return nil, err
|
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)
|
wrappedBody, err := s.wrapV1InternalRequest(projectID, mappedModel, injectedBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1705,6 +1713,19 @@ func (s *AntigravityGatewayService) handleGeminiStreamingResponse(c *gin.Context
|
|||||||
if u := extractGeminiUsage(parsed); u != nil {
|
if u := extractGeminiUsage(parsed); u != nil {
|
||||||
usage = u
|
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 {
|
if firstTokenMs == nil {
|
||||||
@@ -1854,6 +1875,20 @@ func (s *AntigravityGatewayService) handleGeminiStreamToNonStreaming(c *gin.Cont
|
|||||||
usage = u
|
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 的响应
|
// 保留最后一个有 parts 的响应
|
||||||
if parts := extractGeminiParts(parsed); len(parts) > 0 {
|
if parts := extractGeminiParts(parsed); len(parts) > 0 {
|
||||||
lastWithParts = parsed
|
lastWithParts = parsed
|
||||||
@@ -2459,3 +2494,55 @@ func isImageGenerationModel(model string) bool {
|
|||||||
modelLower == "gemini-2.5-flash-image-preview" ||
|
modelLower == "gemini-2.5-flash-image-preview" ||
|
||||||
strings.HasPrefix(modelLower, "gemini-2.5-flash-image-")
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user