merge: resolve conflicts with upstream/main (Gemini 3→3.1 mappings)
This commit is contained in:
@@ -61,7 +61,11 @@ func (h *GeminiOAuthHandler) GenerateAuthURL(c *gin.Context) {
|
||||
if err != nil {
|
||||
msg := err.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)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,10 +38,8 @@ const (
|
||||
// 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
|
||||
// restrict which scopes are allowed for this client.
|
||||
GeminiCLIOAuthClientID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
|
||||
// GeminiCLIOAuthClientSecret is intentionally not embedded in this repository.
|
||||
// If you rely on the built-in Gemini CLI OAuth client, you MUST provide its client_secret via config/env.
|
||||
GeminiCLIOAuthClientSecret = ""
|
||||
GeminiCLIOAuthClientID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
|
||||
GeminiCLIOAuthClientSecret = "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"
|
||||
|
||||
// GeminiCLIOAuthClientSecretEnv is the environment variable name for the built-in client secret.
|
||||
GeminiCLIOAuthClientSecretEnv = "GEMINI_CLI_OAUTH_CLIENT_SECRET"
|
||||
|
||||
@@ -408,11 +408,10 @@ func TestBuildAuthorizationURL_WithProjectID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAuthorizationURL_OAuthConfigError(t *testing.T) {
|
||||
// 不设置环境变量,也不提供 client 凭据,EffectiveOAuthConfig 应该报错
|
||||
func TestBuildAuthorizationURL_UsesBuiltinSecretFallback(t *testing.T) {
|
||||
t.Setenv(GeminiCLIOAuthClientSecretEnv, "")
|
||||
|
||||
_, err := BuildAuthorizationURL(
|
||||
authURL, err := BuildAuthorizationURL(
|
||||
OAuthConfig{},
|
||||
"test-state",
|
||||
"test-challenge",
|
||||
@@ -420,8 +419,11 @@ func TestBuildAuthorizationURL_OAuthConfigError(t *testing.T) {
|
||||
"",
|
||||
"code_assist",
|
||||
)
|
||||
if err == nil {
|
||||
t.Error("当 EffectiveOAuthConfig 失败时,BuildAuthorizationURL 应该返回错误")
|
||||
if err != nil {
|
||||
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) {
|
||||
// 不设置环境变量且不提供凭据,应该报错
|
||||
t.Setenv(GeminiCLIOAuthClientSecretEnv, "")
|
||||
|
||||
_, err := EffectiveOAuthConfig(OAuthConfig{}, "code_assist")
|
||||
if err == nil {
|
||||
t.Error("没有内置 secret 且未提供凭据时应该报错")
|
||||
cfg, err := EffectiveOAuthConfig(OAuthConfig{}, "code_assist")
|
||||
if err != nil {
|
||||
t.Fatalf("不设置环境变量时应回退到内置 secret,实际报错: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), GeminiCLIOAuthClientSecretEnv) {
|
||||
t.Errorf("错误消息应提及环境变量 %s,实际: %v", GeminiCLIOAuthClientSecretEnv, err)
|
||||
if strings.TrimSpace(cfg.ClientSecret) == "" {
|
||||
t.Error("ClientSecret 不应为空")
|
||||
}
|
||||
if cfg.ClientID != GeminiCLIOAuthClientID {
|
||||
t.Errorf("ClientID 应回退为内置客户端 ID,实际: %q", cfg.ClientID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -372,6 +372,9 @@ func (a *Account) GetModelMapping() map[string]string {
|
||||
}
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if a.Platform == domain.PlatformAntigravity {
|
||||
ensureAntigravityDefaultPassthrough(result, "gemini-3-flash")
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -382,6 +385,21 @@ func (a *Account) GetModelMapping() map[string]string {
|
||||
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 中(支持通配符)
|
||||
// 如果未配置 mapping,返回 true(允许所有模型)
|
||||
func (a *Account) IsModelSupported(requestedModel string) bool {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -217,12 +218,20 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U
|
||||
}
|
||||
|
||||
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 获取额度
|
||||
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)
|
||||
@@ -256,6 +265,7 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U
|
||||
// 4. 添加窗口统计(有独立缓存,1 分钟)
|
||||
s.addWindowStats(ctx, account, usage)
|
||||
|
||||
s.tryClearRecoverableAccountError(ctx, account)
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
func (s *AccountUsageService) buildUsageInfo(resp *ClaudeUsageResponse, updatedAt *time.Time) *UsageInfo {
|
||||
info := &UsageInfo{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user