merge: resolve conflicts with upstream/main (Gemini 3→3.1 mappings)

This commit is contained in:
erio
2026-02-25 00:38:39 +08:00
8 changed files with 172 additions and 34 deletions

View File

@@ -61,7 +61,11 @@ func (h *GeminiOAuthHandler) GenerateAuthURL(c *gin.Context) {
if err != nil { if err != nil {
msg := err.Error() msg := err.Error()
// Treat missing/invalid OAuth client configuration as a user/config error. // Treat missing/invalid OAuth client configuration as a user/config error.
if strings.Contains(msg, "OAuth client not configured") || strings.Contains(msg, "requires your own OAuth Client") { if strings.Contains(msg, "OAuth client not configured") ||
strings.Contains(msg, "requires your own OAuth Client") ||
strings.Contains(msg, "requires a custom OAuth Client") ||
strings.Contains(msg, "GEMINI_CLI_OAUTH_CLIENT_SECRET_MISSING") ||
strings.Contains(msg, "built-in Gemini CLI OAuth client_secret is not configured") {
response.BadRequest(c, "Failed to generate auth URL: "+msg) response.BadRequest(c, "Failed to generate auth URL: "+msg)
return return
} }

View File

@@ -38,10 +38,8 @@ const (
// GeminiCLIOAuthClientID/Secret are the public OAuth client credentials used by Google Gemini CLI. // GeminiCLIOAuthClientID/Secret are the public OAuth client credentials used by Google Gemini CLI.
// They enable the "login without creating your own OAuth client" experience, but Google may // They enable the "login without creating your own OAuth client" experience, but Google may
// restrict which scopes are allowed for this client. // restrict which scopes are allowed for this client.
GeminiCLIOAuthClientID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" GeminiCLIOAuthClientID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
// GeminiCLIOAuthClientSecret is intentionally not embedded in this repository. GeminiCLIOAuthClientSecret = "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"
// If you rely on the built-in Gemini CLI OAuth client, you MUST provide its client_secret via config/env.
GeminiCLIOAuthClientSecret = ""
// GeminiCLIOAuthClientSecretEnv is the environment variable name for the built-in client secret. // GeminiCLIOAuthClientSecretEnv is the environment variable name for the built-in client secret.
GeminiCLIOAuthClientSecretEnv = "GEMINI_CLI_OAUTH_CLIENT_SECRET" GeminiCLIOAuthClientSecretEnv = "GEMINI_CLI_OAUTH_CLIENT_SECRET"

View File

@@ -408,11 +408,10 @@ func TestBuildAuthorizationURL_WithProjectID(t *testing.T) {
} }
} }
func TestBuildAuthorizationURL_OAuthConfigError(t *testing.T) { func TestBuildAuthorizationURL_UsesBuiltinSecretFallback(t *testing.T) {
// 不设置环境变量,也不提供 client 凭据EffectiveOAuthConfig 应该报错
t.Setenv(GeminiCLIOAuthClientSecretEnv, "") t.Setenv(GeminiCLIOAuthClientSecretEnv, "")
_, err := BuildAuthorizationURL( authURL, err := BuildAuthorizationURL(
OAuthConfig{}, OAuthConfig{},
"test-state", "test-state",
"test-challenge", "test-challenge",
@@ -420,8 +419,11 @@ func TestBuildAuthorizationURL_OAuthConfigError(t *testing.T) {
"", "",
"code_assist", "code_assist",
) )
if err == nil { if err != nil {
t.Error("当 EffectiveOAuthConfig 失败时BuildAuthorizationURL 应该返回错误") t.Fatalf("BuildAuthorizationURL() 不应报错: %v", err)
}
if !strings.Contains(authURL, "client_id="+GeminiCLIOAuthClientID) {
t.Errorf("应使用内置 Gemini CLI client_id实际 URL: %s", authURL)
} }
} }
@@ -685,15 +687,17 @@ func TestEffectiveOAuthConfig_WhitespaceTriming(t *testing.T) {
} }
func TestEffectiveOAuthConfig_NoEnvSecret(t *testing.T) { func TestEffectiveOAuthConfig_NoEnvSecret(t *testing.T) {
// 不设置环境变量且不提供凭据,应该报错
t.Setenv(GeminiCLIOAuthClientSecretEnv, "") t.Setenv(GeminiCLIOAuthClientSecretEnv, "")
_, err := EffectiveOAuthConfig(OAuthConfig{}, "code_assist") cfg, err := EffectiveOAuthConfig(OAuthConfig{}, "code_assist")
if err == nil { if err != nil {
t.Error("没有内置 secret 且未提供凭据时应该报错") t.Fatalf("不设置环境变量时应回退到内置 secret实际报错: %v", err)
} }
if !strings.Contains(err.Error(), GeminiCLIOAuthClientSecretEnv) { if strings.TrimSpace(cfg.ClientSecret) == "" {
t.Errorf("错误消息应提及环境变量 %s实际: %v", GeminiCLIOAuthClientSecretEnv, err) t.Error("ClientSecret 不应为空")
}
if cfg.ClientID != GeminiCLIOAuthClientID {
t.Errorf("ClientID 应回退为内置客户端 ID实际: %q", cfg.ClientID)
} }
} }

View File

@@ -372,6 +372,9 @@ func (a *Account) GetModelMapping() map[string]string {
} }
} }
if len(result) > 0 { if len(result) > 0 {
if a.Platform == domain.PlatformAntigravity {
ensureAntigravityDefaultPassthrough(result, "gemini-3-flash")
}
return result return result
} }
} }
@@ -382,6 +385,21 @@ func (a *Account) GetModelMapping() map[string]string {
return nil return nil
} }
func ensureAntigravityDefaultPassthrough(mapping map[string]string, model string) {
if mapping == nil || model == "" {
return
}
if _, exists := mapping[model]; exists {
return
}
for pattern := range mapping {
if matchWildcard(pattern, model) {
return
}
}
mapping[model] = model
}
// IsModelSupported 检查模型是否在 model_mapping 中(支持通配符) // IsModelSupported 检查模型是否在 model_mapping 中(支持通配符)
// 如果未配置 mapping返回 true允许所有模型 // 如果未配置 mapping返回 true允许所有模型
func (a *Account) IsModelSupported(requestedModel string) bool { func (a *Account) IsModelSupported(requestedModel string) bool {

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"strings"
"sync" "sync"
"time" "time"
@@ -217,12 +218,20 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U
} }
if account.Platform == PlatformGemini { if account.Platform == PlatformGemini {
return s.getGeminiUsage(ctx, account) usage, err := s.getGeminiUsage(ctx, account)
if err == nil {
s.tryClearRecoverableAccountError(ctx, account)
}
return usage, err
} }
// Antigravity 平台:使用 AntigravityQuotaFetcher 获取额度 // Antigravity 平台:使用 AntigravityQuotaFetcher 获取额度
if account.Platform == PlatformAntigravity { if account.Platform == PlatformAntigravity {
return s.getAntigravityUsage(ctx, account) usage, err := s.getAntigravityUsage(ctx, account)
if err == nil {
s.tryClearRecoverableAccountError(ctx, account)
}
return usage, err
} }
// 只有oauth类型账号可以通过API获取usage有profile scope // 只有oauth类型账号可以通过API获取usage有profile scope
@@ -256,6 +265,7 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U
// 4. 添加窗口统计有独立缓存1 分钟) // 4. 添加窗口统计有独立缓存1 分钟)
s.addWindowStats(ctx, account, usage) s.addWindowStats(ctx, account, usage)
s.tryClearRecoverableAccountError(ctx, account)
return usage, nil return usage, nil
} }
@@ -486,6 +496,32 @@ func parseTime(s string) (time.Time, error) {
return time.Time{}, fmt.Errorf("unable to parse time: %s", s) return time.Time{}, fmt.Errorf("unable to parse time: %s", s)
} }
func (s *AccountUsageService) tryClearRecoverableAccountError(ctx context.Context, account *Account) {
if account == nil || account.Status != StatusError {
return
}
msg := strings.ToLower(strings.TrimSpace(account.ErrorMessage))
if msg == "" {
return
}
if !strings.Contains(msg, "token refresh failed") &&
!strings.Contains(msg, "invalid_client") &&
!strings.Contains(msg, "missing_project_id") &&
!strings.Contains(msg, "unauthenticated") {
return
}
if err := s.accountRepo.ClearError(ctx, account.ID); err != nil {
log.Printf("[usage] failed to clear recoverable account error for account %d: %v", account.ID, err)
return
}
account.Status = StatusActive
account.ErrorMessage = ""
}
// buildUsageInfo 构建UsageInfo // buildUsageInfo 构建UsageInfo
func (s *AccountUsageService) buildUsageInfo(resp *ClaudeUsageResponse, updatedAt *time.Time) *UsageInfo { func (s *AccountUsageService) buildUsageInfo(resp *ClaudeUsageResponse, updatedAt *time.Time) *UsageInfo {
info := &UsageInfo{ info := &UsageInfo{

View File

@@ -267,3 +267,38 @@ func TestAccountGetMappedModel(t *testing.T) {
}) })
} }
} }
func TestAccountGetModelMapping_AntigravityEnsuresGemini3FlashPassthrough(t *testing.T) {
account := &Account{
Platform: PlatformAntigravity,
Credentials: map[string]any{
"model_mapping": map[string]any{
"gemini-3-pro-high": "gemini-3.1-pro-high",
},
},
}
mapping := account.GetModelMapping()
if mapping["gemini-3-flash"] != "gemini-3-flash" {
t.Fatalf("expected gemini-3-flash passthrough to be auto-filled, got: %q", mapping["gemini-3-flash"])
}
}
func TestAccountGetModelMapping_AntigravityRespectsWildcardOverride(t *testing.T) {
account := &Account{
Platform: PlatformAntigravity,
Credentials: map[string]any{
"model_mapping": map[string]any{
"gemini-3*": "gemini-3.1-pro-high",
},
},
}
mapping := account.GetModelMapping()
if _, exists := mapping["gemini-3-flash"]; exists {
t.Fatalf("did not expect explicit gemini-3-flash passthrough when wildcard already exists")
}
if mapped := account.GetMappedModel("gemini-3-flash"); mapped != "gemini-3.1-pro-high" {
t.Fatalf("expected wildcard mapping to stay effective, got: %q", mapped)
}
}

View File

@@ -768,6 +768,31 @@ const presetMappings = [
color: color:
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
}, },
{
label: 'Sonnet4→4.6',
from: 'claude-sonnet-4-20250514',
to: 'claude-sonnet-4-6',
color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400'
},
{
label: 'Sonnet4.5→4.6',
from: 'claude-sonnet-4-5-20250929',
to: 'claude-sonnet-4-6',
color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400'
},
{
label: 'Sonnet3.5→4.6',
from: 'claude-3-5-sonnet-20241022',
to: 'claude-sonnet-4-6',
color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400'
},
{
label: 'Opus4.5→4.6',
from: 'claude-opus-4-5-20251101',
to: 'claude-opus-4-6-thinking',
color:
'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400'
},
{ {
label: 'Opus->Sonnet', label: 'Opus->Sonnet',
from: 'claude-opus-4-5-20251101', from: 'claude-opus-4-5-20251101',
@@ -793,22 +818,34 @@ const presetMappings = [
color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400'
}, },
{ {
label: 'Gemini Flash 2.0', label: '3-Pro-Preview→3.1-Pro-High',
from: 'gemini-2.0-flash', from: 'gemini-3-pro-preview',
to: 'gemini-2.0-flash', to: 'gemini-3.1-pro-high',
color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400'
}, },
{ {
label: 'Gemini 2.5 Flash', label: '3-Pro-High→3.1-Pro-High',
from: 'gemini-2.5-flash', from: 'gemini-3-pro-high',
to: 'gemini-2.5-flash', to: 'gemini-3.1-pro-high',
color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400'
}, },
{ {
label: 'Gemini 2.5 Pro', label: '3-Pro-Low→3.1-Pro-Low',
from: 'gemini-2.5-pro', from: 'gemini-3-pro-low',
to: 'gemini-2.5-pro', to: 'gemini-3.1-pro-low',
color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400'
},
{
label: '3-Flash透传',
from: 'gemini-3-flash',
to: 'gemini-3-flash',
color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400'
},
{
label: '2.5-Flash-Lite透传',
from: 'gemini-2.5-flash-lite',
to: 'gemini-2.5-flash-lite',
color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400'
} }
] ]

View File

@@ -291,13 +291,19 @@ const antigravityPresetMappings = [
{ label: 'Sonnet→Sonnet', from: 'claude-sonnet-*', to: 'claude-sonnet-4-5', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' }, { label: 'Sonnet→Sonnet', from: 'claude-sonnet-*', to: 'claude-sonnet-4-5', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
{ label: 'Opus→Opus', from: 'claude-opus-*', to: 'claude-opus-4-6-thinking', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }, { label: 'Opus→Opus', from: 'claude-opus-*', to: 'claude-opus-4-6-thinking', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
{ label: 'Haiku→Sonnet', from: 'claude-haiku-*', to: 'claude-sonnet-4-5', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, { label: 'Haiku→Sonnet', from: 'claude-haiku-*', to: 'claude-sonnet-4-5', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' },
// Gemini 3 映射 { label: 'Sonnet4→4.6', from: 'claude-sonnet-4-20250514', to: 'claude-sonnet-4-6', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' },
{ label: '3-Pro-Preview→3-Pro-High', from: 'gemini-3-pro-preview', to: 'gemini-3-pro-high', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }, { label: 'Sonnet4.5→4.6', from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' },
{ label: '3-Pro-High', from: 'gemini-3-pro-high', to: 'gemini-3-pro-high', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, { label: 'Sonnet3.5→4.6', from: 'claude-3-5-sonnet-20241022', to: 'claude-sonnet-4-6', color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' },
{ label: '3-Pro-Low', from: 'gemini-3-pro-low', to: 'gemini-3-pro-low', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' }, { label: 'Opus4.5→4.6', from: 'claude-opus-4-5-20251101', to: 'claude-opus-4-6-thinking', color: 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400' },
// Gemini 3→3.1 映射
{ label: '3-Pro-Preview→3.1-Pro-High', from: 'gemini-3-pro-preview', to: 'gemini-3.1-pro-high', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' },
{ label: '3-Pro-High→3.1-Pro-High', from: 'gemini-3-pro-high', to: 'gemini-3.1-pro-high', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },
{ label: '3-Pro-Low→3.1-Pro-Low', from: 'gemini-3-pro-low', to: 'gemini-3.1-pro-low', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' },
// Gemini 通配符映射 // Gemini 通配符映射
{ label: 'Gemini 3→Flash', from: 'gemini-3*', to: 'gemini-3-flash', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' }, { label: 'Gemini 3→Flash', from: 'gemini-3*', to: 'gemini-3-flash', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' },
{ label: 'Gemini 2.5→Flash', from: 'gemini-2.5*', to: 'gemini-2.5-flash', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, { label: 'Gemini 2.5→Flash', from: 'gemini-2.5*', to: 'gemini-2.5-flash', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },
{ label: '3-Flash透传', from: 'gemini-3-flash', to: 'gemini-3-flash', color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' },
{ label: '2.5-Flash-Lite透传', from: 'gemini-2.5-flash-lite', to: 'gemini-2.5-flash-lite', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' },
// 精确映射 // 精确映射
{ label: 'Sonnet 4.6', from: 'claude-sonnet-4-6', to: 'claude-sonnet-4-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, { label: 'Sonnet 4.6', from: 'claude-sonnet-4-6', to: 'claude-sonnet-4-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' },
{ label: 'Sonnet 4.5', from: 'claude-sonnet-4-5', to: 'claude-sonnet-4-5', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, { label: 'Sonnet 4.5', from: 'claude-sonnet-4-5', to: 'claude-sonnet-4-5', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' },