fix(gateway): harden digest logging and align antigravity ops
- avoid panic by using safe UUID prefix truncation in Gemini digest fallback logs\n- remove unconditional Antigravity 429 full-body debug logs and honor log truncation config\n- align Antigravity quick preset mappings to opus 4.6-thinking targets only\n- restore scope rate-limit aggregation/output in ops availability stats
This commit is contained in:
@@ -120,3 +120,24 @@ func TestGeminiCLITmpDirRegex(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSafeShortPrefix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
n int
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "空字符串", input: "", n: 8, want: ""},
|
||||||
|
{name: "长度小于截断值", input: "abc", n: 8, want: "abc"},
|
||||||
|
{name: "长度等于截断值", input: "12345678", n: 8, want: "12345678"},
|
||||||
|
{name: "长度大于截断值", input: "1234567890", n: 8, want: "12345678"},
|
||||||
|
{name: "截断值为0", input: "123456", n: 0, want: "123456"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
require.Equal(t, tt.want, safeShortPrefix(tt.input, tt.n))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
|
|||||||
sessionBoundAccountID = foundAccountID
|
sessionBoundAccountID = foundAccountID
|
||||||
geminiSessionUUID = foundUUID
|
geminiSessionUUID = foundUUID
|
||||||
log.Printf("[Gemini] Digest fallback matched: uuid=%s, accountID=%d, chain=%s",
|
log.Printf("[Gemini] Digest fallback matched: uuid=%s, accountID=%d, chain=%s",
|
||||||
foundUUID[:8], foundAccountID, truncateDigestChain(geminiDigestChain))
|
safeShortPrefix(foundUUID, 8), foundAccountID, truncateDigestChain(geminiDigestChain))
|
||||||
|
|
||||||
// 关键:如果原 sessionKey 为空,使用 prefixHash + uuid 作为 sessionKey
|
// 关键:如果原 sessionKey 为空,使用 prefixHash + uuid 作为 sessionKey
|
||||||
// 这样 SelectAccountWithLoadAwareness 的粘性会话逻辑会优先使用匹配到的账号
|
// 这样 SelectAccountWithLoadAwareness 的粘性会话逻辑会优先使用匹配到的账号
|
||||||
@@ -650,6 +650,15 @@ func truncateDigestChain(chain string) string {
|
|||||||
return chain[:50] + "..."
|
return chain[:50] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// safeShortPrefix 返回字符串前 n 个字符;长度不足时返回原字符串。
|
||||||
|
// 用于日志展示,避免切片越界。
|
||||||
|
func safeShortPrefix(value string, n int) string {
|
||||||
|
if n <= 0 || len(value) <= n {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:n]
|
||||||
|
}
|
||||||
|
|
||||||
// derefGroupID 安全解引用 *int64,nil 返回 0
|
// derefGroupID 安全解引用 *int64,nil 返回 0
|
||||||
func derefGroupID(groupID *int64) int64 {
|
func derefGroupID(groupID *int64) int64 {
|
||||||
if groupID == nil {
|
if groupID == nil {
|
||||||
|
|||||||
@@ -1950,11 +1950,6 @@ func parseAntigravitySmartRetryInfo(body []byte) *antigravitySmartRetryInfo {
|
|||||||
isResourceExhausted := status == googleRPCStatusResourceExhausted
|
isResourceExhausted := status == googleRPCStatusResourceExhausted
|
||||||
isUnavailable := status == googleRPCStatusUnavailable
|
isUnavailable := status == googleRPCStatusUnavailable
|
||||||
|
|
||||||
// 调试日志:打印 RESOURCE_EXHAUSTED 的完整响应
|
|
||||||
if isResourceExhausted {
|
|
||||||
log.Printf("[Antigravity-Debug] 429 RESOURCE_EXHAUSTED full body: %s", string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isResourceExhausted && !isUnavailable {
|
if !isResourceExhausted && !isUnavailable {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -2208,8 +2203,10 @@ func (s *AntigravityGatewayService) handleUpstreamError(
|
|||||||
// ========== 原有逻辑,保持不变 ==========
|
// ========== 原有逻辑,保持不变 ==========
|
||||||
// 429 使用 Gemini 格式解析(从 body 解析重置时间)
|
// 429 使用 Gemini 格式解析(从 body 解析重置时间)
|
||||||
if statusCode == 429 {
|
if statusCode == 429 {
|
||||||
// 调试日志:打印 429 响应的完整 body
|
// 调试日志遵循统一日志开关与长度限制,避免无条件记录完整上游响应体。
|
||||||
log.Printf("[Antigravity-Debug] 429 response full body: %s", string(body))
|
if logBody, maxBytes := s.getLogConfig(); logBody {
|
||||||
|
log.Printf("[Antigravity-Debug] 429 response body: %s", truncateString(string(body), maxBytes))
|
||||||
|
}
|
||||||
|
|
||||||
useScopeLimit := quotaScope != ""
|
useScopeLimit := quotaScope != ""
|
||||||
resetAt := ParseGeminiRateLimitResetTime(body)
|
resetAt := ParseGeminiRateLimitResetTime(body)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi
|
|||||||
}
|
}
|
||||||
|
|
||||||
isAvailable := acc.Status == StatusActive && acc.Schedulable && !isRateLimited && !isOverloaded && !isTempUnsched
|
isAvailable := acc.Status == StatusActive && acc.Schedulable && !isRateLimited && !isOverloaded && !isTempUnsched
|
||||||
|
scopeRateLimits := acc.GetAntigravityScopeRateLimits()
|
||||||
|
|
||||||
if acc.Platform != "" {
|
if acc.Platform != "" {
|
||||||
if _, ok := platform[acc.Platform]; !ok {
|
if _, ok := platform[acc.Platform]; !ok {
|
||||||
@@ -84,6 +85,14 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi
|
|||||||
if hasError {
|
if hasError {
|
||||||
p.ErrorCount++
|
p.ErrorCount++
|
||||||
}
|
}
|
||||||
|
if len(scopeRateLimits) > 0 {
|
||||||
|
if p.ScopeRateLimitCount == nil {
|
||||||
|
p.ScopeRateLimitCount = make(map[string]int64)
|
||||||
|
}
|
||||||
|
for scope := range scopeRateLimits {
|
||||||
|
p.ScopeRateLimitCount[scope]++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, grp := range acc.Groups {
|
for _, grp := range acc.Groups {
|
||||||
@@ -108,6 +117,14 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi
|
|||||||
if hasError {
|
if hasError {
|
||||||
g.ErrorCount++
|
g.ErrorCount++
|
||||||
}
|
}
|
||||||
|
if len(scopeRateLimits) > 0 {
|
||||||
|
if g.ScopeRateLimitCount == nil {
|
||||||
|
g.ScopeRateLimitCount = make(map[string]int64)
|
||||||
|
}
|
||||||
|
for scope := range scopeRateLimits {
|
||||||
|
g.ScopeRateLimitCount[scope]++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
displayGroupID := int64(0)
|
displayGroupID := int64(0)
|
||||||
@@ -140,6 +157,9 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi
|
|||||||
item.RateLimitRemainingSec = &remainingSec
|
item.RateLimitRemainingSec = &remainingSec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(scopeRateLimits) > 0 {
|
||||||
|
item.ScopeRateLimits = scopeRateLimits
|
||||||
|
}
|
||||||
if isOverloaded && acc.OverloadUntil != nil {
|
if isOverloaded && acc.OverloadUntil != nil {
|
||||||
item.OverloadUntil = acc.OverloadUntil
|
item.OverloadUntil = acc.OverloadUntil
|
||||||
remainingSec := int64(time.Until(*acc.OverloadUntil).Seconds())
|
remainingSec := int64(time.Until(*acc.OverloadUntil).Seconds())
|
||||||
|
|||||||
@@ -263,14 +263,14 @@ const antigravityPresetMappings = [
|
|||||||
// Claude 通配符映射
|
// Claude 通配符映射
|
||||||
{ 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: '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: '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-5-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 通配符映射
|
// Gemini 通配符映射
|
||||||
{ label: 'Gemini 3→Flash', from: 'gemini-3*', to: 'gemini-3-flash', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' },
|
{ label: 'Gemini 3→Flash', from: 'gemini-3*', to: 'gemini-3-flash', 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', 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: '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' },
|
||||||
{ label: 'Opus 4.5', from: 'claude-opus-4-5-thinking', to: 'claude-opus-4-5-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' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// Antigravity 默认映射(从后端 API 获取,与 constants.go 保持一致)
|
// Antigravity 默认映射(从后端 API 获取,与 constants.go 保持一致)
|
||||||
|
|||||||
Reference in New Issue
Block a user