Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
17dfb0af
Unverified
Commit
17dfb0af
authored
Jan 22, 2026
by
Wesley Liddick
Committed by
GitHub
Jan 22, 2026
Browse files
Merge pull request #346 from 0xff26b9a8/main
refactor(antigravity): 提取并同步 Schema 清理逻辑至 schema_cleaner.go
parents
39fad63c
477a9a18
Changes
5
Show whitespace changes
Inline
Side-by-side
backend/internal/pkg/antigravity/request_transformer.go
View file @
17dfb0af
...
...
@@ -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
}
}
backend/internal/pkg/antigravity/response_transformer.go
View file @
17dfb0af
...
...
@@ -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"
...
...
backend/internal/pkg/antigravity/schema_cleaner.go
0 → 100644
View file @
17dfb0af
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
)
}
}
}
backend/internal/pkg/antigravity/stream_transformer.go
View file @
17dfb0af
...
...
@@ -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
))
}
...
...
backend/internal/service/antigravity_gateway_service.go
View file @
17dfb0af
...
...
@@ -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
)
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment