mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
refactor: align Antigravity/ClaudeMax/Sora code with upstream main
- Restore 14 Antigravity/model-mapping files to upstream/main versions - Remove fork-specific credits pre-check and degraded retry logic - Revert sonnet 4.5→4.6 model mapping back to upstream defaults - Remove accountUsageService dependency from AntigravityGatewayService - Restore original applyThinkingModelSuffix behavior - Delete 6 fork-only migration files: - 056/057 (Antigravity model mapping) - 060 (Claude Max simulation) - 070 (Sora tasks table) - 077/078 (sonnet rollback/migration) - Fix wire_gen.go to match updated constructor signature
This commit is contained in:
@@ -143,7 +143,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig)
|
||||
antigravityTokenProvider := service.ProvideAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService, oAuthRefreshAPI, tempUnschedCache)
|
||||
internal500CounterCache := repository.NewInternal500CounterCache(redisClient)
|
||||
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService, internal500CounterCache, accountUsageService)
|
||||
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService, internal500CounterCache)
|
||||
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService)
|
||||
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, rpmCache, compositeTokenCacheInvalidator)
|
||||
|
||||
@@ -75,11 +75,11 @@ var DefaultAntigravityModelMapping = map[string]string{
|
||||
"claude-opus-4-6": "claude-opus-4-6-thinking", // 简称映射
|
||||
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking", // 迁移旧模型
|
||||
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
||||
"claude-sonnet-4-5": "claude-sonnet-4-6", // Sonnet 4.5 → 4.6
|
||||
"claude-sonnet-4-5-thinking": "claude-sonnet-4-6", // Sonnet 4.5 → 4.6
|
||||
"claude-sonnet-4-5": "claude-sonnet-4-5",
|
||||
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
||||
// Claude 详细版本 ID 映射
|
||||
"claude-opus-4-5-20251101": "claude-opus-4-6-thinking", // 迁移旧模型
|
||||
"claude-sonnet-4-5-20250929": "claude-sonnet-4-6", // Sonnet 4.5 → 4.6
|
||||
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
|
||||
// Claude Haiku → Sonnet(无 Haiku 支持)
|
||||
"claude-haiku-4-5": "claude-sonnet-4-6",
|
||||
"claude-haiku-4-5-20251001": "claude-sonnet-4-6",
|
||||
|
||||
@@ -846,22 +846,6 @@ func (s *AccountUsageService) getAntigravityUsage(ctx context.Context, account *
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
// GetAntigravityCredits 返回账号的 AI Credits 信息,复用 getAntigravityUsage 的缓存。
|
||||
// 如果缓存存在且 TTL 充足则直接返回;TTL 不足时自动刷新。
|
||||
func (s *AccountUsageService) GetAntigravityCredits(ctx context.Context, account *Account) (*UsageInfo, error) {
|
||||
if account == nil || account.Platform != PlatformAntigravity {
|
||||
return nil, nil
|
||||
}
|
||||
return s.getAntigravityUsage(ctx, account)
|
||||
}
|
||||
|
||||
// InvalidateAntigravityCreditsCache 清除指定账号的 Antigravity 用量缓存,
|
||||
// 使下次调用 GetAntigravityCredits 时强制重新拉取。
|
||||
// 用于 credits 降级响应重试场景:避免重试命中同一个降级缓存。
|
||||
func (s *AccountUsageService) InvalidateAntigravityCreditsCache(accountID int64) {
|
||||
s.cache.antigravityCache.Delete(accountID)
|
||||
}
|
||||
|
||||
// recalcAntigravityRemainingSeconds 重新计算 Antigravity UsageInfo 中各窗口的 RemainingSeconds
|
||||
// 用于从缓存取出时更新倒计时,避免返回过时的剩余秒数
|
||||
func recalcAntigravityRemainingSeconds(info *UsageInfo) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -18,140 +17,8 @@ const (
|
||||
// 与普通模型限流完全同构:通过 SetModelRateLimit / isRateLimitActiveForKey 读写。
|
||||
creditsExhaustedKey = "AICredits"
|
||||
creditsExhaustedDuration = 5 * time.Hour
|
||||
|
||||
// credits 降级响应重试参数
|
||||
creditsRetryMaxAttempts = 3
|
||||
creditsRetryBaseInterval = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
// creditsRetryableErrorCodes 是降级响应中可重试的错误码集合。
|
||||
// forbidden 是稳定的封号状态,不属于可恢复的瞬态错误,不重试。
|
||||
var creditsRetryableErrorCodes = map[string]bool{
|
||||
errorCodeUnauthenticated: true,
|
||||
errorCodeRateLimited: true,
|
||||
errorCodeNetworkError: true,
|
||||
}
|
||||
|
||||
// isAntigravityDegradedResponse 检查 UsageInfo 是否为可重试的降级响应。
|
||||
// 仅检测 3 个瞬态错误码(unauthenticated/rate_limited/network_error),
|
||||
// forbidden 是稳定的封号状态,不属于降级。
|
||||
func isAntigravityDegradedResponse(info *UsageInfo) bool {
|
||||
if info == nil || info.ErrorCode == "" {
|
||||
return false
|
||||
}
|
||||
return creditsRetryableErrorCodes[info.ErrorCode]
|
||||
}
|
||||
|
||||
// checkAccountCredits 通过共享的 AccountUsageService 缓存检查账号是否有足够的 AI Credits。
|
||||
// 缓存 TTL 不足时会自动从 Google loadCodeAssist API 刷新。
|
||||
// 检测到降级响应时会清除缓存并重试,最终 fail-open(返回 true)。
|
||||
func (s *AntigravityGatewayService) checkAccountCredits(
|
||||
ctx context.Context, account *Account,
|
||||
) bool {
|
||||
if account == nil || account.ID == 0 {
|
||||
return false
|
||||
}
|
||||
if s.accountUsageService == nil {
|
||||
return true // 无 usage service 时不阻断
|
||||
}
|
||||
|
||||
usageInfo, err := s.accountUsageService.GetAntigravityCredits(ctx, account)
|
||||
if err != nil {
|
||||
slog.Error("check_credits: get_credits_failed",
|
||||
"account_id", account.ID, "error", err)
|
||||
return true // 出错时 fail-open
|
||||
}
|
||||
|
||||
// 非降级响应:直接检查积分余额
|
||||
if !isAntigravityDegradedResponse(usageInfo) {
|
||||
return s.logCreditsResult(account, usageInfo)
|
||||
}
|
||||
|
||||
// 降级响应:清除缓存后重试
|
||||
return s.retryCreditsOnDegraded(ctx, account, usageInfo)
|
||||
}
|
||||
|
||||
// retryCreditsOnDegraded 在检测到降级响应后,清除缓存并重试获取 credits。
|
||||
// 使用指数退避(500ms → 1s → 2s),最多重试 creditsRetryMaxAttempts 次。
|
||||
// 所有重试失败后 fail-open(返回 true),不做熔断。
|
||||
func (s *AntigravityGatewayService) retryCreditsOnDegraded(
|
||||
ctx context.Context, account *Account, lastInfo *UsageInfo,
|
||||
) bool {
|
||||
for attempt := 1; attempt <= creditsRetryMaxAttempts; attempt++ {
|
||||
delay := creditsRetryBaseInterval << (attempt - 1) // 指数退避:500ms, 1s, 2s
|
||||
slog.Warn("check_credits: degraded response, retrying",
|
||||
"account_id", account.ID,
|
||||
"attempt", attempt,
|
||||
"max_attempts", creditsRetryMaxAttempts,
|
||||
"error_code", lastInfo.ErrorCode,
|
||||
"delay", delay,
|
||||
)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
slog.Warn("check_credits: context cancelled during retry, fail-open",
|
||||
"account_id", account.ID, "attempt", attempt)
|
||||
return true
|
||||
case <-time.After(delay):
|
||||
}
|
||||
|
||||
// 清除缓存,强制下次 GetAntigravityCredits 重新拉取
|
||||
s.accountUsageService.InvalidateAntigravityCreditsCache(account.ID)
|
||||
|
||||
info, err := s.accountUsageService.GetAntigravityCredits(ctx, account)
|
||||
if err != nil {
|
||||
slog.Error("check_credits: retry get_credits_failed",
|
||||
"account_id", account.ID, "attempt", attempt, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 重试成功(不再是降级响应):检查积分余额
|
||||
if !isAntigravityDegradedResponse(info) {
|
||||
slog.Info("check_credits: retry succeeded",
|
||||
"account_id", account.ID, "attempt", attempt)
|
||||
return s.logCreditsResult(account, info)
|
||||
}
|
||||
lastInfo = info
|
||||
}
|
||||
|
||||
// 所有重试失败:fail-open,不做熔断
|
||||
slog.Warn("check_credits: all retries exhausted, fail-open",
|
||||
"account_id", account.ID,
|
||||
"last_error_code", lastInfo.ErrorCode,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// logCreditsResult 检查积分并记录不足日志,返回是否有积分。
|
||||
func (s *AntigravityGatewayService) logCreditsResult(account *Account, info *UsageInfo) bool {
|
||||
hasCredits := hasEnoughCredits(info)
|
||||
if !hasCredits {
|
||||
slog.Warn("check_credits: insufficient credits",
|
||||
"account_id", account.ID)
|
||||
}
|
||||
return hasCredits
|
||||
}
|
||||
|
||||
// hasEnoughCredits 检查 UsageInfo 中是否有足够的 GOOGLE_ONE_AI 积分。
|
||||
// 返回 true 表示积分可用,false 表示积分不足或无积分信息。
|
||||
func hasEnoughCredits(info *UsageInfo) bool {
|
||||
if info == nil || len(info.AICredits) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, credit := range info.AICredits {
|
||||
if credit.CreditType == "GOOGLE_ONE_AI" {
|
||||
minimum := credit.MinimumBalance
|
||||
if minimum <= 0 {
|
||||
minimum = 5
|
||||
}
|
||||
return credit.Amount >= minimum
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type antigravity429Category string
|
||||
|
||||
const (
|
||||
@@ -224,10 +91,6 @@ func (s *AntigravityGatewayService) clearCreditsExhausted(ctx context.Context, a
|
||||
}); err != nil {
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "clear credits exhausted failed: account=%d err=%v", account.ID, err)
|
||||
}
|
||||
// 同步更新 Redis 调度快照,避免其他节点/请求延迟感知
|
||||
if s.schedulerSnapshot != nil {
|
||||
_ = s.schedulerSnapshot.UpdateAccountInCache(ctx, account)
|
||||
}
|
||||
}
|
||||
|
||||
// classifyAntigravity429 将 Antigravity 的 429 响应归类为配额耗尽、限流或未知。
|
||||
@@ -278,14 +141,6 @@ func resolveCreditsOveragesModelKey(ctx context.Context, account *Account, upstr
|
||||
}
|
||||
|
||||
// shouldMarkCreditsExhausted 判断一次 credits 请求失败是否应标记为 credits 耗尽。
|
||||
// 此函数在积分注入后失败时调用(预检查注入 + attemptCreditsOveragesRetry 两条路径)。
|
||||
// - 429 + 单模型限流("exhausted your capacity on this model"):该模型免费配额用完,
|
||||
// 积分注入对此无效,但账号积分对其他模型可能仍可用 → 不标记积分耗尽。
|
||||
// - 429 + 积分关键词(如 "insufficient credits"):积分确实不足 → 标记耗尽。
|
||||
// - 429 + 无积分关键词(节点限流、瞬时 rate limit 等):与积分无关 → 不标记。
|
||||
// - 403 等其他 4xx:检查 body 是否包含积分不足的关键词。
|
||||
//
|
||||
// clearCreditsExhausted 会在后续成功时自动清除。
|
||||
func shouldMarkCreditsExhausted(resp *http.Response, respBody []byte, reqErr error) bool {
|
||||
if reqErr != nil || resp == nil {
|
||||
return false
|
||||
@@ -293,23 +148,13 @@ func shouldMarkCreditsExhausted(resp *http.Response, respBody []byte, reqErr err
|
||||
if resp.StatusCode >= 500 || resp.StatusCode == http.StatusRequestTimeout {
|
||||
return false
|
||||
}
|
||||
bodyLower := strings.ToLower(string(respBody))
|
||||
// 积分注入后仍 429
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
// 单模型配额耗尽:积分注入对此无效,不标记整个账号积分耗尽
|
||||
if strings.Contains(bodyLower, "exhausted your capacity on this model") {
|
||||
return false
|
||||
}
|
||||
// 仅当 body 包含积分相关关键词时才标记耗尽;
|
||||
// 节点限流、瞬时 rate limit 等非积分 429 不应触发 5 小时熔断。
|
||||
return containsCreditsExhaustedKeyword(bodyLower)
|
||||
// 注意:不再检查 isURLLevelRateLimit。此函数仅在积分重试失败后调用,
|
||||
// 如果注入 enabledCreditTypes 后仍返回 "Resource has been exhausted",
|
||||
// 说明积分也已耗尽,应该标记。clearCreditsExhausted 会在后续成功时自动清除。
|
||||
if info := parseAntigravitySmartRetryInfo(respBody); info != nil {
|
||||
return false
|
||||
}
|
||||
// 其他 4xx:关键词匹配(如 403 + "Insufficient credits")
|
||||
return containsCreditsExhaustedKeyword(bodyLower)
|
||||
}
|
||||
|
||||
// containsCreditsExhaustedKeyword 检查响应体是否包含积分耗尽的关键词。
|
||||
func containsCreditsExhaustedKeyword(bodyLower string) bool {
|
||||
bodyLower := strings.ToLower(string(respBody))
|
||||
for _, keyword := range creditsExhaustedKeywords {
|
||||
if strings.Contains(bodyLower, keyword) {
|
||||
return true
|
||||
@@ -336,16 +181,6 @@ func (s *AntigravityGatewayService) attemptCreditsOveragesRetry(
|
||||
if creditsBody == nil {
|
||||
return &creditsOveragesRetryResult{handled: false}
|
||||
}
|
||||
|
||||
// Check actual credits balance before attempting retry
|
||||
if !s.checkAccountCredits(p.ctx, p.account) {
|
||||
s.setCreditsExhausted(p.ctx, p.account)
|
||||
modelKey := resolveCreditsOveragesModelKey(p.ctx, p.account, modelName, p.requestedModel)
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "%s credit_overages_no_credits model=%s account=%d (skipping credits retry)",
|
||||
p.prefix, modelKey, p.account.ID)
|
||||
return &creditsOveragesRetryResult{handled: true}
|
||||
}
|
||||
|
||||
modelKey := resolveCreditsOveragesModelKey(p.ctx, p.account, modelName, p.requestedModel)
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "%s status=429 credit_overages_retry model=%s account=%d (injecting enabledCreditTypes)",
|
||||
p.prefix, modelKey, p.account.ID)
|
||||
|
||||
@@ -239,7 +239,7 @@ func TestAntigravityRetryLoop_ModelRateLimited_InjectsCredits(t *testing.T) {
|
||||
Extra: map[string]any{
|
||||
"allow_overages": true,
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limited_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"rate_limit_reset_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339),
|
||||
},
|
||||
@@ -290,7 +290,7 @@ func TestAntigravityRetryLoop_CreditsExhausted_DoesNotInject(t *testing.T) {
|
||||
Extra: map[string]any{
|
||||
"allow_overages": true,
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limited_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"rate_limit_reset_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339),
|
||||
},
|
||||
@@ -355,7 +355,7 @@ func TestAntigravityRetryLoop_CreditErrorMarksExhausted(t *testing.T) {
|
||||
Extra: map[string]any{
|
||||
"allow_overages": true,
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limited_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"rate_limit_reset_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339),
|
||||
},
|
||||
@@ -418,25 +418,13 @@ func TestShouldMarkCreditsExhausted(t *testing.T) {
|
||||
require.True(t, shouldMarkCreditsExhausted(resp, body, nil))
|
||||
})
|
||||
|
||||
t.Run("单模型配额耗尽不标记(积分对此无效)", func(t *testing.T) {
|
||||
resp := &http.Response{StatusCode: http.StatusTooManyRequests}
|
||||
body := []byte(`{"error":{"code":429,"message":"You have exhausted your capacity on this model. Your quota will reset after 146h11m17s.","status":"RESOURCE_EXHAUSTED"}}`)
|
||||
require.False(t, shouldMarkCreditsExhausted(resp, body, nil))
|
||||
})
|
||||
|
||||
t.Run("429 结构化限流不标记(节点限流/瞬时 rate limit 与积分无关)", func(t *testing.T) {
|
||||
t.Run("结构化限流不标记", func(t *testing.T) {
|
||||
resp := &http.Response{StatusCode: http.StatusTooManyRequests}
|
||||
body := []byte(`{"error":{"status":"RESOURCE_EXHAUSTED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"RATE_LIMIT_EXCEEDED"},{"@type":"type.googleapis.com/google.rpc.RetryInfo","retryDelay":"0.5s"}]}}`)
|
||||
require.False(t, shouldMarkCreditsExhausted(resp, body, nil))
|
||||
})
|
||||
|
||||
t.Run("429 含积分关键词时标记", func(t *testing.T) {
|
||||
resp := &http.Response{StatusCode: http.StatusTooManyRequests}
|
||||
body := []byte(`{"error":{"message":"Insufficient credits to complete this request"}}`)
|
||||
require.True(t, shouldMarkCreditsExhausted(resp, body, nil))
|
||||
})
|
||||
|
||||
t.Run("含 credits 关键词时标记(403 场景)", func(t *testing.T) {
|
||||
t.Run("含 credits 关键词时标记", func(t *testing.T) {
|
||||
resp := &http.Response{StatusCode: http.StatusForbidden}
|
||||
for _, keyword := range []string{
|
||||
"Insufficient GOOGLE_ONE_AI credits",
|
||||
@@ -515,7 +503,7 @@ func TestClearCreditsExhausted(t *testing.T) {
|
||||
ID: 1,
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limited_at": "2026-03-15T00:00:00Z",
|
||||
"rate_limit_reset_at": "2099-03-15T00:00:00Z",
|
||||
},
|
||||
@@ -532,7 +520,7 @@ func TestClearCreditsExhausted(t *testing.T) {
|
||||
ID: 1,
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limited_at": "2026-03-15T00:00:00Z",
|
||||
"rate_limit_reset_at": "2099-03-15T00:00:00Z",
|
||||
},
|
||||
@@ -550,109 +538,7 @@ func TestClearCreditsExhausted(t *testing.T) {
|
||||
_, exists := rawLimits[creditsExhaustedKey]
|
||||
require.False(t, exists, "AICredits key 应被删除")
|
||||
// 普通模型限流应保留
|
||||
_, exists = rawLimits["claude-sonnet-4-6"]
|
||||
_, exists = rawLimits["claude-sonnet-4-5"]
|
||||
require.True(t, exists, "普通模型限流应保留")
|
||||
})
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// hasEnoughCredits — standalone credits balance check
|
||||
// ===========================================================================
|
||||
|
||||
func TestHasEnoughCredits(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
info *UsageInfo
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil UsageInfo",
|
||||
info: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty AICredits list",
|
||||
info: &UsageInfo{AICredits: []AICredit{}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "GOOGLE_ONE_AI with enough credits (amount=18778, minimum=50)",
|
||||
info: &UsageInfo{
|
||||
AICredits: []AICredit{
|
||||
{CreditType: "GOOGLE_ONE_AI", Amount: 18778, MinimumBalance: 50},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "GOOGLE_ONE_AI below minimum (amount=3, minimum=5)",
|
||||
info: &UsageInfo{
|
||||
AICredits: []AICredit{
|
||||
{CreditType: "GOOGLE_ONE_AI", Amount: 3, MinimumBalance: 5},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "GOOGLE_ONE_AI with zero MinimumBalance defaults to 5, amount=6",
|
||||
info: &UsageInfo{
|
||||
AICredits: []AICredit{
|
||||
{CreditType: "GOOGLE_ONE_AI", Amount: 6, MinimumBalance: 0},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "GOOGLE_ONE_AI with zero MinimumBalance defaults to 5, amount=4",
|
||||
info: &UsageInfo{
|
||||
AICredits: []AICredit{
|
||||
{CreditType: "GOOGLE_ONE_AI", Amount: 4, MinimumBalance: 0},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "GOOGLE_ONE_AI exactly at minimum (amount=5, minimum=5)",
|
||||
info: &UsageInfo{
|
||||
AICredits: []AICredit{
|
||||
{CreditType: "GOOGLE_ONE_AI", Amount: 5, MinimumBalance: 5},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no GOOGLE_ONE_AI credit type",
|
||||
info: &UsageInfo{
|
||||
AICredits: []AICredit{
|
||||
{CreditType: "OTHER_CREDIT", Amount: 10000, MinimumBalance: 5},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "multiple credits, GOOGLE_ONE_AI present with enough",
|
||||
info: &UsageInfo{
|
||||
AICredits: []AICredit{
|
||||
{CreditType: "OTHER_CREDIT", Amount: 0, MinimumBalance: 5},
|
||||
{CreditType: "GOOGLE_ONE_AI", Amount: 100, MinimumBalance: 10},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "negative MinimumBalance defaults to 5",
|
||||
info: &UsageInfo{
|
||||
AICredits: []AICredit{
|
||||
{CreditType: "GOOGLE_ONE_AI", Amount: 6, MinimumBalance: -1},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, hasEnoughCredits(tt.info))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,13 +557,7 @@ func (s *AntigravityGatewayService) antigravityRetryLoop(p antigravityRetryLoopP
|
||||
if p.requestedModel != "" && p.account.Platform == PlatformAntigravity &&
|
||||
p.account.IsOveragesEnabled() && !p.account.isCreditsExhausted() &&
|
||||
p.account.isModelRateLimitedWithContext(p.ctx, p.requestedModel) {
|
||||
// Check actual credits balance before injection
|
||||
if !s.checkAccountCredits(p.ctx, p.account) {
|
||||
// No credits available - mark as exhausted and skip injection
|
||||
s.setCreditsExhausted(p.ctx, p.account)
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "%s pre_check: no_credits_available account=%d (skipping credits injection)",
|
||||
p.prefix, p.account.ID)
|
||||
} else if creditsBody := injectEnabledCreditTypes(p.body); creditsBody != nil {
|
||||
if creditsBody := injectEnabledCreditTypes(p.body); creditsBody != nil {
|
||||
p.body = creditsBody
|
||||
overagesInjected = true
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "%s pre_check: model_rate_limited_credits_inject model=%s account=%d (injecting enabledCreditTypes)",
|
||||
@@ -876,15 +870,14 @@ func logPrefix(sessionID, accountName string) string {
|
||||
|
||||
// AntigravityGatewayService 处理 Antigravity 平台的 API 转发
|
||||
type AntigravityGatewayService struct {
|
||||
accountRepo AccountRepository
|
||||
tokenProvider *AntigravityTokenProvider
|
||||
rateLimitService *RateLimitService
|
||||
httpUpstream HTTPUpstream
|
||||
settingService *SettingService
|
||||
cache GatewayCache // 用于模型级限流时清除粘性会话绑定
|
||||
schedulerSnapshot *SchedulerSnapshotService
|
||||
internal500Cache Internal500CounterCache // INTERNAL 500 渐进惩罚计数器
|
||||
accountUsageService *AccountUsageService // 共享 usage 缓存,用于积分余额检查
|
||||
accountRepo AccountRepository
|
||||
tokenProvider *AntigravityTokenProvider
|
||||
rateLimitService *RateLimitService
|
||||
httpUpstream HTTPUpstream
|
||||
settingService *SettingService
|
||||
cache GatewayCache // 用于模型级限流时清除粘性会话绑定
|
||||
schedulerSnapshot *SchedulerSnapshotService
|
||||
internal500Cache Internal500CounterCache // INTERNAL 500 渐进惩罚计数器
|
||||
}
|
||||
|
||||
func NewAntigravityGatewayService(
|
||||
@@ -896,18 +889,16 @@ func NewAntigravityGatewayService(
|
||||
httpUpstream HTTPUpstream,
|
||||
settingService *SettingService,
|
||||
internal500Cache Internal500CounterCache,
|
||||
accountUsageService *AccountUsageService,
|
||||
) *AntigravityGatewayService {
|
||||
return &AntigravityGatewayService{
|
||||
accountRepo: accountRepo,
|
||||
tokenProvider: tokenProvider,
|
||||
rateLimitService: rateLimitService,
|
||||
httpUpstream: httpUpstream,
|
||||
settingService: settingService,
|
||||
cache: cache,
|
||||
schedulerSnapshot: schedulerSnapshot,
|
||||
internal500Cache: internal500Cache,
|
||||
accountUsageService: accountUsageService,
|
||||
accountRepo: accountRepo,
|
||||
tokenProvider: tokenProvider,
|
||||
rateLimitService: rateLimitService,
|
||||
httpUpstream: httpUpstream,
|
||||
settingService: settingService,
|
||||
cache: cache,
|
||||
schedulerSnapshot: schedulerSnapshot,
|
||||
internal500Cache: internal500Cache,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,8 +998,14 @@ func (s *AntigravityGatewayService) getMappedModel(account *Account, requestedMo
|
||||
}
|
||||
|
||||
// applyThinkingModelSuffix 根据 thinking 配置调整模型名
|
||||
// Sonnet 4.5 已统一映射到 4.6,此函数保留为扩展点
|
||||
// 当映射结果是 claude-sonnet-4-5 且请求开启了 thinking 时,改为 claude-sonnet-4-5-thinking
|
||||
func applyThinkingModelSuffix(mappedModel string, thinkingEnabled bool) string {
|
||||
if !thinkingEnabled {
|
||||
return mappedModel
|
||||
}
|
||||
if mappedModel == "claude-sonnet-4-5" {
|
||||
return "claude-sonnet-4-5-thinking"
|
||||
}
|
||||
return mappedModel
|
||||
}
|
||||
|
||||
@@ -1744,7 +1741,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
|
||||
var clientDisconnect bool
|
||||
if claudeReq.Stream {
|
||||
// 客户端要求流式,直接透传转换
|
||||
streamRes, err := s.handleClaudeStreamingResponse(c, resp, startTime, originalModel, account.ID)
|
||||
streamRes, err := s.handleClaudeStreamingResponse(c, resp, startTime, originalModel)
|
||||
if err != nil {
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "%s status=stream_error error=%v", prefix, err)
|
||||
return nil, err
|
||||
@@ -1754,7 +1751,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
|
||||
clientDisconnect = streamRes.clientDisconnect
|
||||
} else {
|
||||
// 客户端要求非流式,收集流式响应后转换返回
|
||||
streamRes, err := s.handleClaudeStreamToNonStreaming(c, resp, startTime, originalModel, account.ID)
|
||||
streamRes, err := s.handleClaudeStreamToNonStreaming(c, resp, startTime, originalModel)
|
||||
if err != nil {
|
||||
logger.LegacyPrintf("service.antigravity_gateway", "%s status=stream_collect_error error=%v", prefix, err)
|
||||
return nil, err
|
||||
@@ -3702,7 +3699,7 @@ func (s *AntigravityGatewayService) writeGoogleError(c *gin.Context, status int,
|
||||
|
||||
// handleClaudeStreamToNonStreaming 收集上游流式响应,转换为 Claude 非流式格式返回
|
||||
// 用于处理客户端非流式请求但上游只支持流式的情况
|
||||
func (s *AntigravityGatewayService) handleClaudeStreamToNonStreaming(c *gin.Context, resp *http.Response, startTime time.Time, originalModel string, accountID int64) (*antigravityStreamResult, error) {
|
||||
func (s *AntigravityGatewayService) handleClaudeStreamToNonStreaming(c *gin.Context, resp *http.Response, startTime time.Time, originalModel string) (*antigravityStreamResult, error) {
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
maxLineSize := defaultMaxLineSize
|
||||
if s.settingService.cfg != nil && s.settingService.cfg.Gateway.MaxLineSize > 0 {
|
||||
@@ -3874,7 +3871,7 @@ returnResponse:
|
||||
}
|
||||
|
||||
// handleClaudeStreamingResponse 处理 Claude 流式响应(Gemini SSE → Claude SSE 转换)
|
||||
func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context, resp *http.Response, startTime time.Time, originalModel string, accountID int64) (*antigravityStreamResult, error) {
|
||||
func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context, resp *http.Response, startTime time.Time, originalModel string) (*antigravityStreamResult, error) {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
@@ -3887,7 +3884,6 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context
|
||||
}
|
||||
|
||||
processor := antigravity.NewStreamingProcessor(originalModel)
|
||||
|
||||
var firstTokenMs *int
|
||||
// 使用 Scanner 并限制单行大小,避免 ReadString 无上限导致 OOM
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
|
||||
@@ -926,7 +926,7 @@ func TestHandleClaudeStreamingResponse_NormalComplete(t *testing.T) {
|
||||
fmt.Fprintln(pw, "")
|
||||
}()
|
||||
|
||||
result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5", 0)
|
||||
result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5")
|
||||
_ = pr.Close()
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -1003,7 +1003,7 @@ func TestHandleClaudeStreamingResponse_ThoughtsTokenCount(t *testing.T) {
|
||||
fmt.Fprintln(pw, "")
|
||||
}()
|
||||
|
||||
result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "gemini-2.5-pro", 0)
|
||||
result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "gemini-2.5-pro")
|
||||
_ = pr.Close()
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -1206,7 +1206,7 @@ func TestHandleClaudeStreamingResponse_ClientDisconnect(t *testing.T) {
|
||||
fmt.Fprintln(pw, "")
|
||||
}()
|
||||
|
||||
result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5", 0)
|
||||
result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5")
|
||||
_ = pr.Close()
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -1238,7 +1238,7 @@ func TestHandleClaudeStreamingResponse_EmptyStream(t *testing.T) {
|
||||
fmt.Fprintln(pw, "")
|
||||
}()
|
||||
|
||||
_, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5", 0)
|
||||
_, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5")
|
||||
_ = pr.Close()
|
||||
|
||||
// 应当返回 UpstreamFailoverError 而非 nil,以便上层触发 failover
|
||||
@@ -1270,7 +1270,7 @@ func TestHandleClaudeStreamingResponse_ContextCanceled(t *testing.T) {
|
||||
|
||||
resp := &http.Response{StatusCode: http.StatusOK, Body: cancelReadCloser{}, Header: http.Header{}}
|
||||
|
||||
result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5", 0)
|
||||
result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
|
||||
@@ -69,10 +69,10 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) {
|
||||
expected: "claude-sonnet-4-6",
|
||||
},
|
||||
{
|
||||
name: "默认映射 - claude-sonnet-4-5-20250929 → claude-sonnet-4-6",
|
||||
name: "默认映射 - claude-sonnet-4-5-20250929 → claude-sonnet-4-5",
|
||||
requestedModel: "claude-sonnet-4-5-20250929",
|
||||
accountMapping: nil,
|
||||
expected: "claude-sonnet-4-6",
|
||||
expected: "claude-sonnet-4-5",
|
||||
},
|
||||
|
||||
// 3. 默认映射中的透传(映射到自己)
|
||||
@@ -83,10 +83,10 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) {
|
||||
expected: "claude-sonnet-4-6",
|
||||
},
|
||||
{
|
||||
name: "默认映射 - claude-sonnet-4-5 → claude-sonnet-4-6",
|
||||
name: "默认映射透传 - claude-sonnet-4-5",
|
||||
requestedModel: "claude-sonnet-4-5",
|
||||
accountMapping: nil,
|
||||
expected: "claude-sonnet-4-6",
|
||||
expected: "claude-sonnet-4-5",
|
||||
},
|
||||
{
|
||||
name: "默认映射透传 - claude-opus-4-6-thinking",
|
||||
@@ -95,10 +95,10 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) {
|
||||
expected: "claude-opus-4-6-thinking",
|
||||
},
|
||||
{
|
||||
name: "默认映射 - claude-sonnet-4-5-thinking → claude-sonnet-4-6",
|
||||
name: "默认映射透传 - claude-sonnet-4-5-thinking",
|
||||
requestedModel: "claude-sonnet-4-5-thinking",
|
||||
accountMapping: nil,
|
||||
expected: "claude-sonnet-4-6",
|
||||
expected: "claude-sonnet-4-5-thinking",
|
||||
},
|
||||
{
|
||||
name: "默认映射透传 - gemini-2.5-flash",
|
||||
|
||||
@@ -206,7 +206,7 @@ func TestHandleUpstreamError_429_NonModelRateLimit(t *testing.T) {
|
||||
// 但 429 兜底逻辑会使用 requestedModel 设置模型级限流
|
||||
require.Nil(t, result)
|
||||
require.Len(t, repo.modelRateLimitCalls, 1)
|
||||
require.Equal(t, "claude-sonnet-4-6", repo.modelRateLimitCalls[0].modelKey)
|
||||
require.Equal(t, "claude-sonnet-4-5", repo.modelRateLimitCalls[0].modelKey)
|
||||
}
|
||||
|
||||
// TestHandleUpstreamError_429_NonModelRateLimit_UsesMappedModelKey 测试 429 非模型限流场景
|
||||
@@ -832,7 +832,7 @@ func TestAntigravityRetryLoop_PreCheck_SwitchesWhenRateLimited(t *testing.T) {
|
||||
Concurrency: 1,
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limit_reset_at": time.Now().Add(2 * time.Second).Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
@@ -875,7 +875,7 @@ func TestAntigravityRetryLoop_PreCheck_SwitchesWhenRemainingLong(t *testing.T) {
|
||||
Concurrency: 1,
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limit_reset_at": time.Now().Add(11 * time.Second).Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -689,7 +689,7 @@ func TestAntigravityRetryLoop_PreCheck_SingleAccountRetry_SkipsRateLimit(t *test
|
||||
Concurrency: 1,
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limit_reset_at": time.Now().Add(30 * time.Second).Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
@@ -733,7 +733,7 @@ func TestAntigravityRetryLoop_PreCheck_NoSingleAccountRetry_SwitchesOnRateLimit(
|
||||
Concurrency: 1,
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limit_reset_at": time.Now().Add(30 * time.Second).Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,12 +27,12 @@ func TestApplyThinkingModelSuffix(t *testing.T) {
|
||||
expected: "claude-opus-4-6-thinking",
|
||||
},
|
||||
|
||||
// Thinking 开启 + claude-sonnet-4-5:Sonnet 4.5 已统一映射到 4.6,不再特殊处理
|
||||
// Thinking 开启 + claude-sonnet-4-5:自动添加后缀
|
||||
{
|
||||
name: "thinking enabled - claude-sonnet-4-5 unchanged (mapped to 4.6 elsewhere)",
|
||||
name: "thinking enabled - claude-sonnet-4-5 becomes thinking version",
|
||||
mappedModel: "claude-sonnet-4-5",
|
||||
thinkingEnabled: true,
|
||||
expected: "claude-sonnet-4-5",
|
||||
expected: "claude-sonnet-4-5-thinking",
|
||||
},
|
||||
|
||||
// Thinking 开启 + 其他模型:保持原样
|
||||
|
||||
@@ -103,8 +103,7 @@ func TestGatewayService_isModelSupportedByAccountWithContext_ThinkingMode(t *tes
|
||||
expected: false,
|
||||
},
|
||||
// 场景 3: 配置 claude-sonnet-4-5(非 thinking),请求 claude-sonnet-4-5 + thinking=true
|
||||
// applyThinkingModelSuffix 已简化为直通,不再添加 -thinking 后缀
|
||||
// 最终模型名仍为 claude-sonnet-4-5,在 mapping 中 → 返回 true
|
||||
// 最终模型名 = claude-sonnet-4-5-thinking,不在 mapping 中,应该不匹配
|
||||
{
|
||||
name: "thinking_enabled_no_match_non_thinking_mapping",
|
||||
modelMapping: map[string]any{
|
||||
@@ -112,7 +111,7 @@ func TestGatewayService_isModelSupportedByAccountWithContext_ThinkingMode(t *tes
|
||||
},
|
||||
requestedModel: "claude-sonnet-4-5",
|
||||
thinkingEnabled: true,
|
||||
expected: true,
|
||||
expected: false,
|
||||
},
|
||||
// 场景 4: 配置两种模型,请求 claude-sonnet-4-5 + thinking=true,应该匹配 thinking 版本
|
||||
{
|
||||
|
||||
@@ -181,12 +181,11 @@ func TestIsModelRateLimited_Antigravity_ThinkingAffectsModelKey(t *testing.T) {
|
||||
now := time.Now()
|
||||
future := now.Add(10 * time.Minute).Format(time.RFC3339)
|
||||
|
||||
// Sonnet 4.5 now maps to Sonnet 4.6, so rate limit key is claude-sonnet-4-6
|
||||
account := &Account{
|
||||
Platform: PlatformAntigravity,
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5-thinking": map[string]any{
|
||||
"rate_limit_reset_at": future,
|
||||
},
|
||||
},
|
||||
@@ -344,7 +343,7 @@ func TestGetRateLimitRemainingTime(t *testing.T) {
|
||||
Platform: PlatformAntigravity,
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limit_reset_at": future15m,
|
||||
},
|
||||
},
|
||||
@@ -360,7 +359,7 @@ func TestGetRateLimitRemainingTime(t *testing.T) {
|
||||
Platform: PlatformAntigravity,
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
"claude-sonnet-4-6": map[string]any{
|
||||
"claude-sonnet-4-5": map[string]any{
|
||||
"rate_limit_reset_at": future5m,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
-- Add claude-sonnet-4-6 to model_mapping for all Antigravity accounts
|
||||
--
|
||||
-- Background:
|
||||
-- Antigravity now supports claude-sonnet-4-6
|
||||
--
|
||||
-- Strategy:
|
||||
-- Directly overwrite the entire model_mapping with updated mappings
|
||||
-- This ensures consistency with DefaultAntigravityModelMapping in constants.go
|
||||
|
||||
UPDATE accounts
|
||||
SET credentials = jsonb_set(
|
||||
credentials,
|
||||
'{model_mapping}',
|
||||
'{
|
||||
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
|
||||
"claude-opus-4-6": "claude-opus-4-6-thinking",
|
||||
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking",
|
||||
"claude-opus-4-5-20251101": "claude-opus-4-6-thinking",
|
||||
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
||||
"claude-sonnet-4-5": "claude-sonnet-4-5",
|
||||
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
||||
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
|
||||
"claude-haiku-4-5": "claude-sonnet-4-5",
|
||||
"claude-haiku-4-5-20251001": "claude-sonnet-4-5",
|
||||
"gemini-2.5-flash": "gemini-2.5-flash",
|
||||
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
|
||||
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
|
||||
"gemini-2.5-pro": "gemini-2.5-pro",
|
||||
"gemini-3-flash": "gemini-3-flash",
|
||||
"gemini-3-pro-high": "gemini-3-pro-high",
|
||||
"gemini-3-pro-low": "gemini-3-pro-low",
|
||||
"gemini-3-pro-image": "gemini-3-pro-image",
|
||||
"gemini-3-flash-preview": "gemini-3-flash",
|
||||
"gemini-3-pro-preview": "gemini-3-pro-high",
|
||||
"gemini-3-pro-image-preview": "gemini-3-pro-image",
|
||||
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
|
||||
"tab_flash_lite_preview": "tab_flash_lite_preview"
|
||||
}'::jsonb
|
||||
)
|
||||
WHERE platform = 'antigravity'
|
||||
AND deleted_at IS NULL
|
||||
AND credentials->'model_mapping' IS NOT NULL;
|
||||
@@ -1,45 +0,0 @@
|
||||
-- Add gemini-3.1-pro-high, gemini-3.1-pro-low, gemini-3.1-pro-preview to model_mapping
|
||||
--
|
||||
-- Background:
|
||||
-- Antigravity now supports gemini-3.1-pro-high and gemini-3.1-pro-low
|
||||
--
|
||||
-- Strategy:
|
||||
-- Directly overwrite the entire model_mapping with updated mappings
|
||||
-- This ensures consistency with DefaultAntigravityModelMapping in constants.go
|
||||
|
||||
UPDATE accounts
|
||||
SET credentials = jsonb_set(
|
||||
credentials,
|
||||
'{model_mapping}',
|
||||
'{
|
||||
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
|
||||
"claude-opus-4-6": "claude-opus-4-6-thinking",
|
||||
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking",
|
||||
"claude-opus-4-5-20251101": "claude-opus-4-6-thinking",
|
||||
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
||||
"claude-sonnet-4-5": "claude-sonnet-4-5",
|
||||
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
||||
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
|
||||
"claude-haiku-4-5": "claude-sonnet-4-5",
|
||||
"claude-haiku-4-5-20251001": "claude-sonnet-4-5",
|
||||
"gemini-2.5-flash": "gemini-2.5-flash",
|
||||
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
|
||||
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
|
||||
"gemini-2.5-pro": "gemini-2.5-pro",
|
||||
"gemini-3-flash": "gemini-3-flash",
|
||||
"gemini-3-pro-high": "gemini-3-pro-high",
|
||||
"gemini-3-pro-low": "gemini-3-pro-low",
|
||||
"gemini-3-pro-image": "gemini-3-pro-image",
|
||||
"gemini-3-flash-preview": "gemini-3-flash",
|
||||
"gemini-3-pro-preview": "gemini-3-pro-high",
|
||||
"gemini-3-pro-image-preview": "gemini-3-pro-image",
|
||||
"gemini-3.1-pro-high": "gemini-3.1-pro-high",
|
||||
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
|
||||
"gemini-3.1-pro-preview": "gemini-3.1-pro-high",
|
||||
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
|
||||
"tab_flash_lite_preview": "tab_flash_lite_preview"
|
||||
}'::jsonb
|
||||
)
|
||||
WHERE platform = 'antigravity'
|
||||
AND deleted_at IS NULL
|
||||
AND credentials->'model_mapping' IS NOT NULL;
|
||||
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE groups
|
||||
ADD COLUMN IF NOT EXISTS simulate_claude_max_enabled BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
-- +migrate Up
|
||||
|
||||
-- Sora 异步任务表:记录视频/角色/图片生成任务状态
|
||||
CREATE TABLE IF NOT EXISTS sora_tasks (
|
||||
id VARCHAR(64) PRIMARY KEY, -- 对外 ID (video_xxx / char_xxx / img_xxx)
|
||||
account_id BIGINT NOT NULL, -- 使用的账号 ID
|
||||
api_key_id BIGINT, -- 调用方 API Key ID
|
||||
upstream_task_id VARCHAR(128) NOT NULL DEFAULT '', -- 上游任务 ID
|
||||
object_type VARCHAR(16) NOT NULL DEFAULT 'video', -- video / character / image
|
||||
model VARCHAR(64) NOT NULL, -- 请求模型名
|
||||
prompt TEXT NOT NULL DEFAULT '', -- 生成提示词
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'queued', -- queued / in_progress / completed / failed
|
||||
progress INT NOT NULL DEFAULT 0, -- 进度 0-100
|
||||
video_url TEXT NOT NULL DEFAULT '', -- 原始上游下载 URL
|
||||
stored_key TEXT NOT NULL DEFAULT '', -- 存储后的 key(本地相对路径或 S3 object key)
|
||||
storage_type VARCHAR(16) NOT NULL DEFAULT '', -- 存储类型:local / s3 / gdrive / 空=未存储
|
||||
share_id VARCHAR(128) NOT NULL DEFAULT '', -- 可分享 ID
|
||||
character_info JSONB, -- 角色信息 {username, display_name}
|
||||
error_message TEXT NOT NULL DEFAULT '', -- 错误信息
|
||||
error_type VARCHAR(64) NOT NULL DEFAULT '', -- 错误类型
|
||||
request_body JSONB, -- 原始请求体(用于上游重放)
|
||||
seconds VARCHAR(8) NOT NULL DEFAULT '', -- 视频时长
|
||||
size VARCHAR(16) NOT NULL DEFAULT '', -- 分辨率 (如 1920x1080)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- 仅索引未完成任务,供后台 worker 轮询
|
||||
CREATE INDEX IF NOT EXISTS idx_sora_tasks_pending ON sora_tasks(status) WHERE status IN ('queued', 'in_progress');
|
||||
|
||||
-- 按 API Key 查询
|
||||
CREATE INDEX IF NOT EXISTS idx_sora_tasks_api_key ON sora_tasks(api_key_id) WHERE api_key_id IS NOT NULL;
|
||||
|
||||
-- +migrate Down
|
||||
|
||||
DROP TABLE IF EXISTS sora_tasks;
|
||||
@@ -1,18 +0,0 @@
|
||||
-- 077: Rollback Sonnet/Haiku mappings from Opus back to original values
|
||||
-- Reverts migration 077_migrate_sonnet_to_opus46_thinking.sql
|
||||
-- Restores: Sonnet→Sonnet, Haiku→Sonnet (original behavior)
|
||||
|
||||
UPDATE accounts
|
||||
SET credentials = jsonb_set(
|
||||
credentials, '{model_mapping}',
|
||||
(credentials->'model_mapping')
|
||||
|| '{"claude-sonnet-4-6": "claude-sonnet-4-6"}'::jsonb
|
||||
|| '{"claude-sonnet-4-5": "claude-sonnet-4-5"}'::jsonb
|
||||
|| '{"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking"}'::jsonb
|
||||
|| '{"claude-sonnet-4-5-20250929": "claude-sonnet-4-5"}'::jsonb
|
||||
|| '{"claude-haiku-4-5": "claude-sonnet-4-6"}'::jsonb
|
||||
|| '{"claude-haiku-4-5-20251001": "claude-sonnet-4-6"}'::jsonb
|
||||
)
|
||||
WHERE platform = 'antigravity'
|
||||
AND deleted_at IS NULL
|
||||
AND credentials->'model_mapping' IS NOT NULL;
|
||||
@@ -1,16 +0,0 @@
|
||||
-- 078: Migrate Sonnet 4.5 → Sonnet 4.6 for all antigravity accounts
|
||||
-- Sonnet 4.5 is no longer reliable on upstream, redirect to 4.6
|
||||
|
||||
UPDATE accounts
|
||||
SET credentials = jsonb_set(
|
||||
credentials, '{model_mapping}',
|
||||
(credentials->'model_mapping')
|
||||
|| '{"claude-sonnet-4-5": "claude-sonnet-4-6"}'::jsonb
|
||||
|| '{"claude-sonnet-4-5-thinking": "claude-sonnet-4-6"}'::jsonb
|
||||
|| '{"claude-sonnet-4-5-20250929": "claude-sonnet-4-6"}'::jsonb
|
||||
|| '{"claude-haiku-4-5": "claude-sonnet-4-6"}'::jsonb
|
||||
|| '{"claude-haiku-4-5-20251001": "claude-sonnet-4-6"}'::jsonb
|
||||
)
|
||||
WHERE platform = 'antigravity'
|
||||
AND deleted_at IS NULL
|
||||
AND credentials->'model_mapping' IS NOT NULL;
|
||||
@@ -283,10 +283,10 @@ const geminiPresetMappings = [
|
||||
// Antigravity 预设映射(支持通配符)
|
||||
const antigravityPresetMappings = [
|
||||
// Claude 通配符映射
|
||||
{ label: 'Claude→Sonnet4.6', from: 'claude-*', to: 'claude-sonnet-4-6', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
|
||||
{ label: 'Sonnet→4.6', from: 'claude-sonnet-*', to: 'claude-sonnet-4-6', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
|
||||
{ label: 'Claude→Sonnet', from: 'claude-*', to: 'claude-sonnet-4-5', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-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: 'Haiku→Sonnet4.6', from: 'claude-haiku-*', to: 'claude-sonnet-4-6', 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' },
|
||||
{ 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' },
|
||||
@@ -307,7 +307,7 @@ const antigravityPresetMappings = [
|
||||
{ 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.5→4.6', from: 'claude-sonnet-4-5', 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: 'Opus 4.6', from: 'claude-opus-4-6', to: 'claude-opus-4-6-thinking', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
|
||||
{ label: 'Opus 4.6-thinking', from: 'claude-opus-4-6-thinking', to: 'claude-opus-4-6-thinking', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user