mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 00:24:38 +00:00
Merge pull request #7432 from doublepi123/fix/antigravity-compat-bare-gemini-model
fix(antigravity): 裸 Gemini 模型名在所有转发入口解析到思考变体,修复 404/502
This commit is contained in:
@@ -49,7 +49,11 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
|
||||
}
|
||||
|
||||
originalModel := claudeReq.Model
|
||||
mappedModel := s.getMappedModel(account, claudeReq.Model)
|
||||
mappedModel := s.getMappedModelForThinkingLevel(
|
||||
account,
|
||||
claudeReq.Model,
|
||||
geminiThinkingLevelFromClaudeThinking(claudeReq.Thinking),
|
||||
)
|
||||
if mappedModel == "" {
|
||||
MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalFeatureGate)
|
||||
return nil, s.writeClaudeError(c, http.StatusForbidden, "permission_error", fmt.Sprintf("model %s not in whitelist", claudeReq.Model))
|
||||
|
||||
@@ -212,7 +212,11 @@ func (s *AntigravityGatewayService) prepareAntigravityCompatCall(
|
||||
return nil, s.writeAntigravityCompatError(c, http.StatusBadRequest, "invalid_request_error", "Invalid request body")
|
||||
}
|
||||
|
||||
mappedModel := s.getMappedModel(account, request.originalModel)
|
||||
mappedModel := s.getMappedModelForThinkingLevel(
|
||||
account,
|
||||
request.originalModel,
|
||||
geminiThinkingLevelFromClaudeThinking(claudeRequest.Thinking),
|
||||
)
|
||||
if mappedModel == "" {
|
||||
MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalFeatureGate)
|
||||
message := fmt.Sprintf("model %s not in whitelist", request.originalModel)
|
||||
|
||||
@@ -86,13 +86,11 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
||||
return nil, s.writeGoogleError(c, http.StatusNotFound, "Unsupported action: "+action)
|
||||
}
|
||||
|
||||
// 裸模型名(gemini-3.8-flash)+ thinkingConfig → 账号映射表里对应的 -low/-medium/-high 变体;
|
||||
// 裸模型名(gemini-3.8-flash)按 thinkingConfig 解析到 -low/-medium/-high 变体;
|
||||
// 裸名有显式映射时保持原行为。
|
||||
mappedModel, variantResolved := resolveGeminiThinkingVariant(account, originalModel, body)
|
||||
if !variantResolved {
|
||||
mappedModel = s.getMappedModel(account, originalModel)
|
||||
} else {
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "%s resolved bare Gemini model %s to thinking variant %s", prefix, originalModel, mappedModel)
|
||||
mappedModel := s.getMappedModelForThinkingLevel(account, originalModel, geminiThinkingLevelFromBody(body))
|
||||
if mappedModel != "" && mappedModel != originalModel {
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "%s mapped Gemini model %s to %s", prefix, originalModel, mappedModel)
|
||||
}
|
||||
if mappedModel == "" {
|
||||
MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalFeatureGate)
|
||||
|
||||
@@ -294,6 +294,18 @@ func mapAntigravityModel(account *Account, requestedModel string) string {
|
||||
// getMappedModel 获取映射后的模型名
|
||||
// 完全依赖映射配置:账户映射(通配符)→ 默认映射兜底
|
||||
func (s *AntigravityGatewayService) getMappedModel(account *Account, requestedModel string) string {
|
||||
return s.getMappedModelForThinkingLevel(account, requestedModel, "")
|
||||
}
|
||||
|
||||
// getMappedModelForThinkingLevel 在常规映射之前先把裸 Gemini 模型名
|
||||
// (gemini-3.x-flash)解析到上游目录里真实存在的 -low/-medium/-high/-tiered 变体。
|
||||
// Antigravity 目录只登记带后缀的变体,裸名直接转发会被上游以
|
||||
// 404 "Requested entity was not found." 拒绝,因此所有转发入口都必须经过这一步。
|
||||
// thinkingLevel 为空时按 high 兜底;调用方可按自身协议传入推导出的档位。
|
||||
func (s *AntigravityGatewayService) getMappedModelForThinkingLevel(account *Account, requestedModel string, thinkingLevel string) string {
|
||||
if mapped, ok := resolveGeminiThinkingVariantForLevel(account, requestedModel, thinkingLevel); ok {
|
||||
return mapped
|
||||
}
|
||||
return mapAntigravityModel(account, requestedModel)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
)
|
||||
|
||||
// Gemini 原生请求(/v1beta/models/{model}:generateContent 等)经 Antigravity 账号转发时,
|
||||
@@ -83,6 +85,29 @@ func geminiThinkingLevelFromBody(body []byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
// geminiThinkingLevelFromClaudeThinking 用 Claude Messages 协议的 thinking 配置推导档位,
|
||||
// 阈值与 geminiThinkingLevelFromBody 保持一致,使同一请求无论走 Gemini 原生还是
|
||||
// Chat Completions / Messages 兼容层都落到同一个上游变体。
|
||||
func geminiThinkingLevelFromClaudeThinking(thinking *antigravity.ThinkingConfig) string {
|
||||
if thinking == nil {
|
||||
return "high"
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(thinking.Type), "disabled") {
|
||||
return "low"
|
||||
}
|
||||
budget := thinking.BudgetTokens
|
||||
switch {
|
||||
case budget <= 0:
|
||||
return "high" // 动态思考 / 未指定预算
|
||||
case budget <= geminiThinkingBudgetLowMax:
|
||||
return "low"
|
||||
case budget <= geminiThinkingBudgetMediumMax:
|
||||
return "medium"
|
||||
default:
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
|
||||
// accountRawModelMappingHasKey 判断 key 是否由用户写在账号 credentials.model_mapping 里
|
||||
// (而不是 resolveModelMapping 运行时补进来的默认透传条目)。
|
||||
func accountRawModelMappingHasKey(account *Account, key string) bool {
|
||||
@@ -97,6 +122,13 @@ func accountRawModelMappingHasKey(account *Account, key string) bool {
|
||||
// resolveGeminiThinkingVariant 为裸 Gemini 模型名挑选账号映射表里存在的思考深度变体。
|
||||
// 返回 (映射后的上游模型名, 是否命中)。未命中时调用方应回退到常规 getMappedModel 流程。
|
||||
func resolveGeminiThinkingVariant(account *Account, requestedModel string, body []byte) (string, bool) {
|
||||
return resolveGeminiThinkingVariantForLevel(account, requestedModel, geminiThinkingLevelFromBody(body))
|
||||
}
|
||||
|
||||
// resolveGeminiThinkingVariantForLevel 是 resolveGeminiThinkingVariant 的协议无关内核:
|
||||
// 调用方负责按自身协议推导 preferred 档位(Gemini 原生读 generationConfig.thinkingConfig,
|
||||
// Claude/OpenAI 兼容层读 thinking.budget_tokens),此处只做映射查找与降级。
|
||||
func resolveGeminiThinkingVariantForLevel(account *Account, requestedModel string, preferred string) (string, bool) {
|
||||
if account == nil {
|
||||
return "", false
|
||||
}
|
||||
@@ -120,7 +152,9 @@ func resolveGeminiThinkingVariant(account *Account, requestedModel string, body
|
||||
}
|
||||
}
|
||||
|
||||
preferred := geminiThinkingLevelFromBody(body)
|
||||
if preferred == "" {
|
||||
preferred = "high"
|
||||
}
|
||||
order := []string{preferred}
|
||||
for _, level := range []string{"high", "medium", "low", "tiered"} {
|
||||
if level != preferred {
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -116,3 +117,96 @@ func TestResolveGeminiThinkingVariant(t *testing.T) {
|
||||
require.Empty(t, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGeminiThinkingLevelFromClaudeThinking(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
thinking *antigravity.ThinkingConfig
|
||||
want string
|
||||
}{
|
||||
{"nil thinking", nil, "high"},
|
||||
{"disabled", &antigravity.ThinkingConfig{Type: "disabled"}, "low"},
|
||||
{"enabled without budget", &antigravity.ThinkingConfig{Type: "enabled"}, "high"},
|
||||
{"budget 1024 -> low", &antigravity.ThinkingConfig{Type: "enabled", BudgetTokens: 1024}, "low"},
|
||||
{"budget 4000 -> medium", &antigravity.ThinkingConfig{Type: "enabled", BudgetTokens: 4000}, "medium"},
|
||||
{"budget 20000 -> high", &antigravity.ThinkingConfig{Type: "enabled", BudgetTokens: 20000}, "high"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, geminiThinkingLevelFromClaudeThinking(tt.thinking))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 锁定线上 404 回归:Chat Completions / Messages 兼容层收到裸 Gemini 模型名时,
|
||||
// 必须和 Gemini 原生路径一样解析到带后缀的上游变体,而不是把裸名透传给上游。
|
||||
func TestResolveGeminiThinkingVariantForLevel_CompatPathResolvesBareModel(t *testing.T) {
|
||||
catalog := map[string]string{
|
||||
"gemini-3.8-flash-low": "gemini-3.8-flash-low",
|
||||
"gemini-3.8-flash-medium": "gemini-3.8-flash-medium",
|
||||
"gemini-3.8-flash-high": "gemini-3.8-flash-high",
|
||||
}
|
||||
account := newAntigravityAccountWithMapping(catalog)
|
||||
|
||||
for _, tc := range []struct{ level, want string }{
|
||||
{"low", "gemini-3.8-flash-low"},
|
||||
{"medium", "gemini-3.8-flash-medium"},
|
||||
{"high", "gemini-3.8-flash-high"},
|
||||
{"", "gemini-3.8-flash-high"},
|
||||
} {
|
||||
t.Run("level="+tc.level, func(t *testing.T) {
|
||||
got, matched := resolveGeminiThinkingVariantForLevel(account, "gemini-3.8-flash", tc.level)
|
||||
require.True(t, matched)
|
||||
require.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("already suffixed model is untouched", func(t *testing.T) {
|
||||
got, matched := resolveGeminiThinkingVariantForLevel(account, "gemini-3.8-flash-high", "low")
|
||||
require.False(t, matched)
|
||||
require.Empty(t, got)
|
||||
})
|
||||
}
|
||||
|
||||
// 同一请求无论走 Gemini 原生还是兼容层,都必须落到同一个上游变体。
|
||||
func TestGeminiThinkingLevel_NativeAndCompatAgree(t *testing.T) {
|
||||
native := geminiThinkingLevelFromBody([]byte(`{"generationConfig":{"thinkingConfig":{"thinkingBudget":4000}}}`))
|
||||
compat := geminiThinkingLevelFromClaudeThinking(&antigravity.ThinkingConfig{Type: "enabled", BudgetTokens: 4000})
|
||||
require.Equal(t, native, compat)
|
||||
}
|
||||
|
||||
// 回归锁:裸名解析必须发生在 getMappedModel 这一层,从而覆盖全部 Antigravity
|
||||
// 转发入口(/v1beta 原生、/v1/chat/completions、/v1/messages)以及账号测试。
|
||||
// 早前只在单个入口打补丁,导致 /v1/messages 仍把裸名透传给上游并触发
|
||||
// 404 "Requested entity was not found."
|
||||
func TestGetMappedModelResolvesBareGeminiModelForAllEntrypoints(t *testing.T) {
|
||||
svc := &AntigravityGatewayService{}
|
||||
account := newAntigravityAccountWithMapping(map[string]string{
|
||||
"gemini-3.8-flash-low": "gemini-3.8-flash-low",
|
||||
"gemini-3.8-flash-medium": "gemini-3.8-flash-medium",
|
||||
"gemini-3.8-flash-high": "gemini-3.8-flash-high",
|
||||
})
|
||||
|
||||
t.Run("default entrypoint falls back to high", func(t *testing.T) {
|
||||
require.Equal(t, "gemini-3.8-flash-high", svc.getMappedModel(account, "gemini-3.8-flash"))
|
||||
})
|
||||
|
||||
t.Run("caller supplied level wins", func(t *testing.T) {
|
||||
require.Equal(t, "gemini-3.8-flash-low",
|
||||
svc.getMappedModelForThinkingLevel(account, "gemini-3.8-flash", "low"))
|
||||
require.Equal(t, "gemini-3.8-flash-medium",
|
||||
svc.getMappedModelForThinkingLevel(account, "gemini-3.8-flash", "medium"))
|
||||
})
|
||||
|
||||
t.Run("models/ prefix is handled", func(t *testing.T) {
|
||||
require.Equal(t, "gemini-3.8-flash-high", svc.getMappedModel(account, "models/gemini-3.8-flash"))
|
||||
})
|
||||
|
||||
t.Run("suffixed model keeps normal mapping", func(t *testing.T) {
|
||||
require.Equal(t, "gemini-3.8-flash-low", svc.getMappedModel(account, "gemini-3.8-flash-low"))
|
||||
})
|
||||
|
||||
t.Run("unsupported model still reports unsupported", func(t *testing.T) {
|
||||
require.Empty(t, svc.getMappedModel(account, "gemini-9.9-flash"))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user