fix(antigravity): preserve tool_result images, native Gemini functionCall/thoughtSignature, and composite /v1beta routing

This commit is contained in:
Long Li
2026-10-15 06:55:32 +09:00
parent 373d43533a
commit feab6d80dc
7 changed files with 397 additions and 101 deletions
@@ -39,9 +39,11 @@ func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) {
googleError(c, http.StatusUnauthorized, "Invalid API key")
return
}
// 检查平台:优先使用强制平台(/antigravity 路由),否则要求 gemini 分组
// 检查平台:优先使用强制平台(/antigravity 路由),否则要求 gemini 分组(composite 分组允许路由到 antigravity)
forcePlatform, hasForcePlatform := middleware.GetForcePlatformFromContext(c)
if !hasForcePlatform && effectiveAPIKeyPlatform(c, apiKey) != service.PlatformGemini {
effectivePlatform := effectiveAPIKeyPlatform(c, apiKey)
if !hasForcePlatform && effectivePlatform != service.PlatformGemini &&
!servesCompositeAntigravityGemini(apiKey, effectivePlatform) {
googleError(c, http.StatusBadRequest, "API key group platform is not gemini")
return
}
@@ -223,9 +225,11 @@ func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) {
googleError(c, http.StatusUnauthorized, "Invalid API key")
return
}
// 检查平台:优先使用强制平台(/antigravity 路由),否则要求 gemini 分组
// 检查平台:优先使用强制平台(/antigravity 路由),否则要求 gemini 分组(composite 分组允许路由到 antigravity)
forcePlatform, hasForcePlatform := middleware.GetForcePlatformFromContext(c)
if !hasForcePlatform && effectiveAPIKeyPlatform(c, apiKey) != service.PlatformGemini {
effectivePlatform := effectiveAPIKeyPlatform(c, apiKey)
if !hasForcePlatform && effectivePlatform != service.PlatformGemini &&
!servesCompositeAntigravityGemini(apiKey, effectivePlatform) {
googleError(c, http.StatusBadRequest, "API key group platform is not gemini")
return
}
@@ -244,9 +248,13 @@ func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) {
if resolvedModel, ok := service.ResolvedUpstreamModelFromContext(c.Request.Context()); ok && strings.TrimSpace(resolvedModel) != "" {
modelName = strings.TrimSpace(resolvedModel)
}
if !hasForcePlatform && servesCompositeAntigravityGemini(apiKey, effectivePlatform) && !isAntigravityGeminiModel(modelName) {
googleError(c, http.StatusBadRequest, "API key group platform is not gemini")
return
}
// 强制 antigravity 模式:返回 antigravity 模型信息
if forcePlatform == service.PlatformAntigravity {
// 强制 antigravity 模式或 composite 路由到 antigravity:返回 antigravity 模型信息
if forcePlatform == service.PlatformAntigravity || effectivePlatform == service.PlatformAntigravity {
c.JSON(http.StatusOK, antigravity.FallbackGeminiModel(modelName))
return
}
@@ -299,9 +307,11 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
zap.Any("group_id", apiKey.GroupID),
)
// 检查平台:优先使用强制平台(/antigravity 路由,中间件已设置 request.Context),否则要求 gemini 分组
// 检查平台:优先使用强制平台(/antigravity 路由,中间件已设置 request.Context),否则要求 gemini 分组(composite 分组允许路由到 antigravity)
effectivePlatform := effectiveAPIKeyPlatform(c, apiKey)
if !middleware.HasForcePlatform(c) {
if effectiveAPIKeyPlatform(c, apiKey) != service.PlatformGemini {
if effectivePlatform != service.PlatformGemini &&
!servesCompositeAntigravityGemini(apiKey, effectivePlatform) {
googleError(c, http.StatusBadRequest, "API key group platform is not gemini")
return
}
@@ -321,6 +331,10 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
if resolvedModel, ok := service.ResolvedUpstreamModelFromContext(c.Request.Context()); ok && strings.TrimSpace(resolvedModel) != "" {
modelName = strings.TrimSpace(resolvedModel)
}
if !middleware.HasForcePlatform(c) && servesCompositeAntigravityGemini(apiKey, effectivePlatform) && !isAntigravityGeminiModel(modelName) {
googleError(c, http.StatusBadRequest, "API key group platform is not gemini")
return
}
stream := action == "streamGenerateContent"
reqLog = reqLog.With(zap.String("model", modelName), zap.String("action", action), zap.Bool("stream", stream))
@@ -567,7 +581,11 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
return
}
if len(fs.FailedAccountIDs) == 0 {
cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, modelName, modelName, service.PlatformGemini)
diagPlatform := service.PlatformGemini
if p := effectiveAPIKeyPlatform(c, apiKey); p == service.PlatformAntigravity {
diagPlatform = service.PlatformAntigravity
}
cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, modelName, modelName, diagPlatform)
if !cls.ModelNotFound {
markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
}
@@ -1053,3 +1071,16 @@ func derefGroupID(groupID *int64) int64 {
}
return *groupID
}
func servesCompositeAntigravityGemini(apiKey *service.APIKey, effectivePlatform string) bool {
return apiKey.Group != nil && apiKey.Group.Platform == service.PlatformComposite && effectivePlatform == service.PlatformAntigravity
}
func isAntigravityGeminiModel(model string) bool {
trimmed := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(model, "models/")))
if trimmed == "" || strings.HasPrefix(trimmed, "claude-") {
return false
}
detected, ok := service.DetectModelPlatform(trimmed)
return ok && detected == service.PlatformGemini
}
@@ -216,3 +216,75 @@ func TestShouldFallbackGeminiModel_DelegatesScopeFallback(t *testing.T) {
}
require.True(t, shouldFallbackGeminiModel("gemini-future-model", res))
}
func TestGeminiV1BetaGetModel_CompositeAntigravityRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Run("composite route to antigravity gemini model succeeds", func(t *testing.T) {
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
req := httptest.NewRequest(http.MethodGet, "/v1beta/models/gemini-4-argon", nil)
req = req.WithContext(service.WithCompositeRouteDecision(req.Context(), service.CompositeRouteDecision{
Matched: true,
TargetPlatform: service.PlatformAntigravity,
UpstreamModel: "gemini-4-argon-high",
}))
c.Request = req
c.Params = gin.Params{{Key: "model", Value: "gemini-4-argon"}}
groupID := int64(4)
c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{
ID: 1,
GroupID: &groupID,
Group: &service.Group{ID: groupID, Platform: service.PlatformComposite},
})
(&GatewayHandler{}).GeminiV1BetaGetModel(c)
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
require.Contains(t, rec.Body.String(), "models/gemini-4-argon-high")
})
t.Run("composite route to antigravity claude model is rejected on v1beta", func(t *testing.T) {
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
req := httptest.NewRequest(http.MethodGet, "/v1beta/models/claude-sonnet-5-5-high", nil)
req = req.WithContext(service.WithCompositeRouteDecision(req.Context(), service.CompositeRouteDecision{
Matched: true,
TargetPlatform: service.PlatformAntigravity,
UpstreamModel: "claude-sonnet-5-5-high",
}))
c.Request = req
c.Params = gin.Params{{Key: "model", Value: "claude-sonnet-5-5-high"}}
groupID := int64(4)
c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{
ID: 1,
GroupID: &groupID,
Group: &service.Group{ID: groupID, Platform: service.PlatformComposite},
})
(&GatewayHandler{}).GeminiV1BetaGetModel(c)
require.Equal(t, http.StatusBadRequest, rec.Code, rec.Body.String())
})
t.Run("composite route to antigravity claude model is rejected on generateContent", func(t *testing.T) {
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
req := httptest.NewRequest(http.MethodPost, "/v1beta/models/claude-sonnet-5-5-high:generateContent", nil)
req = req.WithContext(service.WithCompositeRouteDecision(req.Context(), service.CompositeRouteDecision{
Matched: true,
TargetPlatform: service.PlatformAntigravity,
UpstreamModel: "claude-sonnet-5-5-high",
}))
c.Request = req
c.Params = gin.Params{{Key: "modelAction", Value: "/claude-sonnet-5-5-high:generateContent"}}
groupID := int64(4)
c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{
ID: 1,
GroupID: &groupID,
Group: &service.Group{ID: groupID, Platform: service.PlatformComposite},
})
c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{UserID: 1, Concurrency: 1})
(&GatewayHandler{}).GeminiV1BetaModels(c)
require.Equal(t, http.StatusBadRequest, rec.Code, rec.Body.String())
})
}