mirror of
https://mirror.skon.top/github.com/router-for-me/CLIProxyAPI
synced 2026-04-22 01:30:37 +08:00
fix(antigravity): skip full schema cleanup for empty tool requests
Avoid whole-payload schema sanitization when translated Antigravity requests have no actual tool schemas, including missing and empty tools arrays. Add regression coverage so image-heavy no-tool requests keep bypassing the old memory amplification path. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1946,17 +1946,46 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
|
||||
payload, _ = sjson.SetBytes(payload, "model", modelName)
|
||||
|
||||
useAntigravitySchema := strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro") || strings.Contains(modelName, "gemini-3.1-pro")
|
||||
payloadStr := string(payload)
|
||||
paths := make([]string, 0)
|
||||
util.Walk(gjson.Parse(payloadStr), "", "parametersJsonSchema", &paths)
|
||||
for _, p := range paths {
|
||||
payloadStr, _ = util.RenameKey(payloadStr, p, p[:len(p)-len("parametersJsonSchema")]+"parameters")
|
||||
}
|
||||
var (
|
||||
bodyReader io.Reader
|
||||
payloadLog []byte
|
||||
)
|
||||
if antigravityRequestNeedsSchemaSanitization(payload) {
|
||||
payloadStr := string(payload)
|
||||
paths := make([]string, 0)
|
||||
util.Walk(gjson.Parse(payloadStr), "", "parametersJsonSchema", &paths)
|
||||
for _, p := range paths {
|
||||
payloadStr, _ = util.RenameKey(payloadStr, p, p[:len(p)-len("parametersJsonSchema")]+"parameters")
|
||||
}
|
||||
|
||||
if useAntigravitySchema {
|
||||
payloadStr = util.CleanJSONSchemaForAntigravity(payloadStr)
|
||||
if useAntigravitySchema {
|
||||
payloadStr = util.CleanJSONSchemaForAntigravity(payloadStr)
|
||||
} else {
|
||||
payloadStr = util.CleanJSONSchemaForGemini(payloadStr)
|
||||
}
|
||||
|
||||
if strings.Contains(modelName, "claude") {
|
||||
updated, _ := sjson.SetBytes([]byte(payloadStr), "request.toolConfig.functionCallingConfig.mode", "VALIDATED")
|
||||
payloadStr = string(updated)
|
||||
} else {
|
||||
payloadStr, _ = sjson.Delete(payloadStr, "request.generationConfig.maxOutputTokens")
|
||||
}
|
||||
|
||||
bodyReader = strings.NewReader(payloadStr)
|
||||
if e.cfg != nil && e.cfg.RequestLog {
|
||||
payloadLog = []byte(payloadStr)
|
||||
}
|
||||
} else {
|
||||
payloadStr = util.CleanJSONSchemaForGemini(payloadStr)
|
||||
if strings.Contains(modelName, "claude") {
|
||||
payload, _ = sjson.SetBytes(payload, "request.toolConfig.functionCallingConfig.mode", "VALIDATED")
|
||||
} else {
|
||||
payload, _ = sjson.DeleteBytes(payload, "request.generationConfig.maxOutputTokens")
|
||||
}
|
||||
|
||||
bodyReader = bytes.NewReader(payload)
|
||||
if e.cfg != nil && e.cfg.RequestLog {
|
||||
payloadLog = append([]byte(nil), payload...)
|
||||
}
|
||||
}
|
||||
|
||||
// if useAntigravitySchema {
|
||||
@@ -1972,14 +2001,7 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
|
||||
// }
|
||||
// }
|
||||
|
||||
if strings.Contains(modelName, "claude") {
|
||||
updated, _ := sjson.SetBytes([]byte(payloadStr), "request.toolConfig.functionCallingConfig.mode", "VALIDATED")
|
||||
payloadStr = string(updated)
|
||||
} else {
|
||||
payloadStr, _ = sjson.Delete(payloadStr, "request.generationConfig.maxOutputTokens")
|
||||
}
|
||||
|
||||
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), strings.NewReader(payloadStr))
|
||||
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bodyReader)
|
||||
if errReq != nil {
|
||||
return nil, errReq
|
||||
}
|
||||
@@ -2002,10 +2024,6 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
|
||||
authLabel = auth.Label
|
||||
authType, authValue = auth.AccountInfo()
|
||||
}
|
||||
var payloadLog []byte
|
||||
if e.cfg != nil && e.cfg.RequestLog {
|
||||
payloadLog = []byte(payloadStr)
|
||||
}
|
||||
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||
URL: requestURL.String(),
|
||||
Method: http.MethodPost,
|
||||
@@ -2021,6 +2039,19 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
|
||||
return httpReq, nil
|
||||
}
|
||||
|
||||
func antigravityRequestNeedsSchemaSanitization(payload []byte) bool {
|
||||
if gjson.GetBytes(payload, "request.tools.0").Exists() {
|
||||
return true
|
||||
}
|
||||
if gjson.GetBytes(payload, "request.generationConfig.responseJsonSchema").Exists() {
|
||||
return true
|
||||
}
|
||||
if gjson.GetBytes(payload, "request.generationConfig.responseSchema").Exists() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tokenExpiry(metadata map[string]any) time.Time {
|
||||
if metadata == nil {
|
||||
return time.Time{}
|
||||
|
||||
@@ -35,12 +35,102 @@ func TestAntigravityBuildRequest_SanitizesAntigravityToolSchema(t *testing.T) {
|
||||
assertSchemaSanitizedAndPropertyPreserved(t, params)
|
||||
}
|
||||
|
||||
func buildRequestBodyFromPayload(t *testing.T, modelName string) map[string]any {
|
||||
func TestAntigravityBuildRequest_SkipsSchemaSanitizationWithoutToolsField(t *testing.T) {
|
||||
body := buildRequestBodyFromRawPayload(t, "gemini-3.1-flash-image", []byte(`{
|
||||
"request": {
|
||||
"contents": [
|
||||
{
|
||||
"role": "user",
|
||||
"x-debug": "keep-me",
|
||||
"parts": [
|
||||
{
|
||||
"text": "hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nonSchema": {
|
||||
"nullable": true,
|
||||
"x-extra": "keep-me"
|
||||
},
|
||||
"generationConfig": {
|
||||
"maxOutputTokens": 128
|
||||
}
|
||||
}
|
||||
}`))
|
||||
|
||||
assertNonSchemaRequestPreserved(t, body)
|
||||
}
|
||||
|
||||
func TestAntigravityBuildRequest_SkipsSchemaSanitizationWithEmptyToolsArray(t *testing.T) {
|
||||
body := buildRequestBodyFromRawPayload(t, "gemini-3.1-flash-image", []byte(`{
|
||||
"request": {
|
||||
"tools": [],
|
||||
"contents": [
|
||||
{
|
||||
"role": "user",
|
||||
"x-debug": "keep-me",
|
||||
"parts": [
|
||||
{
|
||||
"text": "hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nonSchema": {
|
||||
"nullable": true,
|
||||
"x-extra": "keep-me"
|
||||
},
|
||||
"generationConfig": {
|
||||
"maxOutputTokens": 128
|
||||
}
|
||||
}
|
||||
}`))
|
||||
|
||||
assertNonSchemaRequestPreserved(t, body)
|
||||
}
|
||||
|
||||
func assertNonSchemaRequestPreserved(t *testing.T, body map[string]any) {
|
||||
t.Helper()
|
||||
|
||||
executor := &AntigravityExecutor{}
|
||||
auth := &cliproxyauth.Auth{}
|
||||
payload := []byte(`{
|
||||
request, ok := body["request"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("request missing or invalid type")
|
||||
}
|
||||
|
||||
contents, ok := request["contents"].([]any)
|
||||
if !ok || len(contents) == 0 {
|
||||
t.Fatalf("contents missing or empty")
|
||||
}
|
||||
content, ok := contents[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("content missing or invalid type")
|
||||
}
|
||||
if got, ok := content["x-debug"].(string); !ok || got != "keep-me" {
|
||||
t.Fatalf("x-debug should be preserved when no tool schema exists, got=%v", content["x-debug"])
|
||||
}
|
||||
|
||||
nonSchema, ok := request["nonSchema"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("nonSchema missing or invalid type")
|
||||
}
|
||||
if _, ok := nonSchema["nullable"]; !ok {
|
||||
t.Fatalf("nullable should be preserved outside schema cleanup path")
|
||||
}
|
||||
if got, ok := nonSchema["x-extra"].(string); !ok || got != "keep-me" {
|
||||
t.Fatalf("x-extra should be preserved outside schema cleanup path, got=%v", nonSchema["x-extra"])
|
||||
}
|
||||
|
||||
if generationConfig, ok := request["generationConfig"].(map[string]any); ok {
|
||||
if _, ok := generationConfig["maxOutputTokens"]; ok {
|
||||
t.Fatalf("maxOutputTokens should still be removed for non-Claude requests")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildRequestBodyFromPayload(t *testing.T, modelName string) map[string]any {
|
||||
t.Helper()
|
||||
return buildRequestBodyFromRawPayload(t, modelName, []byte(`{
|
||||
"request": {
|
||||
"tools": [
|
||||
{
|
||||
@@ -75,7 +165,14 @@ func buildRequestBodyFromPayload(t *testing.T, modelName string) map[string]any
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
}`))
|
||||
}
|
||||
|
||||
func buildRequestBodyFromRawPayload(t *testing.T, modelName string, payload []byte) map[string]any {
|
||||
t.Helper()
|
||||
|
||||
executor := &AntigravityExecutor{}
|
||||
auth := &cliproxyauth.Auth{}
|
||||
|
||||
req, err := executor.buildRequest(context.Background(), auth, "token", modelName, payload, false, "", "https://example.com")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user