Files
sub2api/backend/internal/service/antigravity_gemini_thinking_variant.go
T
HuntercodeT 0f4d8acaa8 fix(antigravity): Gemini 原生请求的裸模型名按 thinkingConfig 解析到 -low/-medium/-high 变体
Antigravity CLI / go-genai 发裸 Gemini 模型名(gemini-3.8-flash)并用
generationConfig.thinkingConfig 表达思考深度,而 Antigravity 上游目录只有
-low/-medium/-high/-tiered 变体。v0.2.5 起 resolveModelMapping 会给 antigravity
账号运行时补 gemini-3.x-flash 裸名自映射,裸名因此被当成显式透传直发上游,
上游回 404 "Requested entity was not found."。

在 ForwardGemini 取映射之前加 resolveGeminiThinkingVariant:
- 仅对 gemini- 前缀且不带后缀的裸名生效;
- 用户在 credentials.model_mapping 亲写的裸名条目一律不介入,运行时注入的
  默认自映射不算用户意图;
- thinkingLevel 优先;否则 thinkingBudget <0→high,≤1024→low,≤8192→medium,
  其余 high;未携带 thinkingConfig→high;所选变体不存在时按
  high→medium→low→tiered 降级;一个变体都没有则回退原逻辑;
- thinkingConfig 仍原样透传,上游按 budget 决定实际思考量。
2026-09-18 09:03:40 +08:00

138 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"encoding/json"
"strings"
)
// Gemini 原生请求(/v1beta/models/{model}:generateContent 等)经 Antigravity 账号转发时,
// 客户端(如 Antigravity CLI / go-genai)习惯发"裸"模型名(gemini-3.8-flash)并用
// generationConfig.thinkingConfig 表达思考深度;而 Antigravity 上游的模型目录只有
// gemini-3.8-flash-low / -medium / -high / -tiered 这类带后缀的变体,裸名直接转发会被上游
// 以 404 "Requested entity was not found." 拒绝。
//
// 这里在账号 model_mapping 没有为裸名配置显式条目、但配置了它的后缀变体时,
// 按 thinkingConfig 自动挑一个变体:
// - thinkingLevel: "low" / "medium" / "high" → 同名后缀
// - thinkingBudget: -1(动态)→ high;0 → low;1..1024 → low;1025..8192 → medium;>8192 → high
// - 未携带 thinkingConfig → high(与 Gemini 3 系列默认开启动态思考一致)
// 选中的后缀在映射表里不存在时按 high → medium → low → tiered 的顺序降级到存在的变体。
// 显式映射(裸名本身在 model_mapping 里)始终优先,行为与现在完全一致。
var geminiThinkingVariantSuffixes = []string{"-low", "-medium", "-high", "-tiered"}
const (
geminiThinkingBudgetLowMax = 1024
geminiThinkingBudgetMediumMax = 8192
)
type geminiThinkingConfigProbe struct {
GenerationConfig struct {
ThinkingConfig *struct {
ThinkingBudget *json.Number `json:"thinkingBudget"`
ThinkingLevel string `json:"thinkingLevel"`
} `json:"thinkingConfig"`
} `json:"generationConfig"`
}
// hasGeminiThinkingVariantSuffix 判断模型名是否已经带了思考深度后缀。
func hasGeminiThinkingVariantSuffix(model string) bool {
for _, suffix := range geminiThinkingVariantSuffixes {
if strings.HasSuffix(model, suffix) {
return true
}
}
return false
}
// geminiThinkingLevelFromBody 从 Gemini 原生请求体推导期望的思考档位(low/medium/high)。
// 解析失败或未携带 thinkingConfig 时返回 "high"。
func geminiThinkingLevelFromBody(body []byte) string {
if len(body) == 0 {
return "high"
}
var probe geminiThinkingConfigProbe
if err := json.Unmarshal(body, &probe); err != nil || probe.GenerationConfig.ThinkingConfig == nil {
return "high"
}
tc := probe.GenerationConfig.ThinkingConfig
switch strings.ToLower(strings.TrimSpace(tc.ThinkingLevel)) {
case "low":
return "low"
case "medium":
return "medium"
case "high":
return "high"
}
if tc.ThinkingBudget == nil {
return "high"
}
budget, err := tc.ThinkingBudget.Float64()
if err != nil {
return "high"
}
switch {
case budget < 0:
return "high" // -1 = 动态思考
case budget <= geminiThinkingBudgetLowMax:
return "low" // 含 0(关闭思考):上游没有"无思考"变体,取最浅档,budget 本身仍原样透传
case budget <= geminiThinkingBudgetMediumMax:
return "medium"
default:
return "high"
}
}
// accountRawModelMappingHasKey 判断 key 是否由用户写在账号 credentials.model_mapping 里
// (而不是 resolveModelMapping 运行时补进来的默认透传条目)。
func accountRawModelMappingHasKey(account *Account, key string) bool {
if account == nil || account.Credentials == nil {
return false
}
raw, _ := account.Credentials["model_mapping"].(map[string]any)
_, ok := raw[key]
return ok
}
// resolveGeminiThinkingVariant 为裸 Gemini 模型名挑选账号映射表里存在的思考深度变体。
// 返回 (映射后的上游模型名, 是否命中)。未命中时调用方应回退到常规 getMappedModel 流程。
func resolveGeminiThinkingVariant(account *Account, requestedModel string, body []byte) (string, bool) {
if account == nil {
return "", false
}
model := strings.TrimSpace(strings.TrimPrefix(requestedModel, "models/"))
if !strings.HasPrefix(model, "gemini-") || hasGeminiThinkingVariantSuffix(model) {
return "", false
}
mapping := account.GetModelMapping()
if len(mapping) == 0 {
return "", false
}
// 裸名本身已有"真正的"映射 → 尊重现有配置,不做推导。判定分两层:
// 1. 映射目标不是自己(如 gemini-3.8-flash → gemini-3.8-flash-tiered):用户明确指定了目标;
// 2. 映射目标是自己(原样透传):仅当这条是用户亲手写进 credentials.model_mapping 的才尊重。
// resolveModelMapping 会给每个 Antigravity 账号自动补 gemini-3.x-flash 裸名自映射
// (ensureAntigravityDefaultPassthroughs),而上游目录里并没有裸名模型,那条自映射
// 正是导致 404 的来源,不能算作用户意图。
if mapped, matched := resolveRequestedModelInMapping(mapping, model); matched {
if strings.TrimSpace(mapped) != model || accountRawModelMappingHasKey(account, model) {
return "", false
}
}
preferred := geminiThinkingLevelFromBody(body)
order := []string{preferred}
for _, level := range []string{"high", "medium", "low", "tiered"} {
if level != preferred {
order = append(order, level)
}
}
for _, level := range order {
candidate := model + "-" + level
if mapped, matched := resolveRequestedModelInMapping(mapping, candidate); matched && strings.TrimSpace(mapped) != "" {
return mapped, true
}
}
return "", false
}