fix(antigravity): support upstream accounts and custom model_mapping in scheduling
- GetAccessToken: add upstream branch to read api_key from credentials - shouldTriggerAntigravitySmartRetry: relax check from IsOAuth to Platform-based - isModelSupportedByAccount/WithContext: replace IsAntigravityModelSupported whitelist with mapAntigravityModel for unified scheduling/forwarding logic - mapAntigravityModel: fix edge case where wildcard target equals request model - Update tests for new behavior and add custom model_mapping test cases
This commit is contained in:
@@ -600,11 +600,12 @@ func mapAntigravityModel(account *Account, requestedModel string) string {
|
||||
return mapped
|
||||
}
|
||||
|
||||
// 如果 mapped == requestedModel,检查是否在映射表 key 中显式配置
|
||||
// 如果 mapped == requestedModel,检查是否在映射表中配置(精确或通配符)
|
||||
// 这区分两种情况:
|
||||
// 1. 映射表中有 "model-a": "model-a"(显式透传)→ 返回 model-a
|
||||
// 2. 映射表中没有 model-a 的配置 → 返回空(不支持)
|
||||
if _, exists := mapping[requestedModel]; exists {
|
||||
// 2. 通配符匹配 "claude-*": "claude-sonnet-4-5" 恰好目标等于请求名 → 返回 model-a
|
||||
// 3. 映射表中没有 model-a 的配置 → 返回空(不支持)
|
||||
if account.IsModelSupported(requestedModel) {
|
||||
return requestedModel
|
||||
}
|
||||
|
||||
@@ -2042,7 +2043,7 @@ func parseAntigravitySmartRetryInfo(body []byte) *antigravitySmartRetryInfo {
|
||||
// - waitDuration: 等待时间(智能重试时使用,shouldRateLimitModel=true 时为 0)
|
||||
// - modelName: 限流的模型名称
|
||||
func shouldTriggerAntigravitySmartRetry(account *Account, respBody []byte) (shouldRetry bool, shouldRateLimitModel bool, waitDuration time.Duration, modelName string) {
|
||||
if !account.IsOAuth() {
|
||||
if account.Platform != PlatformAntigravity {
|
||||
return false, false, 0, ""
|
||||
}
|
||||
|
||||
|
||||
@@ -524,8 +524,8 @@ func TestParseAntigravitySmartRetryInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShouldTriggerAntigravitySmartRetry(t *testing.T) {
|
||||
oauthAccount := &Account{Type: AccountTypeOAuth}
|
||||
setupTokenAccount := &Account{Type: AccountTypeSetupToken}
|
||||
oauthAccount := &Account{Type: AccountTypeOAuth, Platform: PlatformAntigravity}
|
||||
setupTokenAccount := &Account{Type: AccountTypeSetupToken, Platform: PlatformAntigravity}
|
||||
apiKeyAccount := &Account{Type: AccountTypeAPIKey}
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -343,13 +343,13 @@ func TestHandleSmartRetry_503_ModelCapacityExhausted_ReturnsSwitchError(t *testi
|
||||
require.Equal(t, "gemini-3-pro-high", repo.modelRateLimitCalls[0].modelKey)
|
||||
}
|
||||
|
||||
// TestHandleSmartRetry_NonOAuthAccount_ContinuesDefaultLogic 测试非 OAuth 账号走默认逻辑
|
||||
func TestHandleSmartRetry_NonOAuthAccount_ContinuesDefaultLogic(t *testing.T) {
|
||||
// TestHandleSmartRetry_NonAntigravityAccount_ContinuesDefaultLogic 测试非 Antigravity 平台账号走默认逻辑
|
||||
func TestHandleSmartRetry_NonAntigravityAccount_ContinuesDefaultLogic(t *testing.T) {
|
||||
account := &Account{
|
||||
ID: 4,
|
||||
Name: "acc-4",
|
||||
Type: AccountTypeAPIKey, // 非 OAuth 账号
|
||||
Platform: PlatformAntigravity,
|
||||
Type: AccountTypeAPIKey, // 非 Antigravity 平台账号
|
||||
Platform: PlatformAnthropic,
|
||||
}
|
||||
|
||||
// 即使是模型限流响应,非 OAuth 账号也应该走默认逻辑
|
||||
@@ -385,7 +385,7 @@ func TestHandleSmartRetry_NonOAuthAccount_ContinuesDefaultLogic(t *testing.T) {
|
||||
result := handleSmartRetry(params, resp, respBody, "https://ag-1.test", 0, availableURLs)
|
||||
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, smartRetryActionContinue, result.action, "non-OAuth account should continue default logic")
|
||||
require.Equal(t, smartRetryActionContinue, result.action, "non-Antigravity platform account should continue default logic")
|
||||
require.Nil(t, result.resp)
|
||||
require.Nil(t, result.err)
|
||||
require.Nil(t, result.switchError)
|
||||
|
||||
@@ -42,7 +42,18 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
|
||||
if account == nil {
|
||||
return "", errors.New("account is nil")
|
||||
}
|
||||
if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth {
|
||||
if account.Platform != PlatformAntigravity {
|
||||
return "", errors.New("not an antigravity account")
|
||||
}
|
||||
// upstream 类型:直接从 credentials 读取 api_key,不走 OAuth 刷新流程
|
||||
if account.Type == AccountTypeUpstream {
|
||||
apiKey := account.GetCredential("api_key")
|
||||
if apiKey == "" {
|
||||
return "", errors.New("upstream account missing api_key in credentials")
|
||||
}
|
||||
return apiKey, nil
|
||||
}
|
||||
if account.Type != AccountTypeOAuth {
|
||||
return "", errors.New("not an antigravity oauth account")
|
||||
}
|
||||
|
||||
|
||||
@@ -2587,6 +2587,9 @@ func (s *GatewayService) isModelSupportedByAccountWithContext(ctx context.Contex
|
||||
// 应用 thinking 后缀后检查最终模型是否在账号映射中
|
||||
if enabled, ok := ctx.Value(ctxkey.ThinkingEnabled).(bool); ok {
|
||||
finalModel := applyThinkingModelSuffix(mapped, enabled)
|
||||
if finalModel == mapped {
|
||||
return true // thinking 后缀未改变模型名,映射已通过
|
||||
}
|
||||
return account.IsModelSupported(finalModel)
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -80,21 +80,21 @@ func TestGatewayService_isModelSupportedByAccountWithContext_ThinkingMode(t *tes
|
||||
thinkingEnabled bool
|
||||
expected bool
|
||||
}{
|
||||
// 场景 1: 配置 claude-sonnet-4-5-thinking,请求 claude-sonnet-4-5 + thinking=true
|
||||
// 最终模型名 = claude-sonnet-4-5-thinking,应该匹配
|
||||
// 场景 1: 只配置 claude-sonnet-4-5-thinking,请求 claude-sonnet-4-5 + thinking=true
|
||||
// mapAntigravityModel 找不到 claude-sonnet-4-5 的映射 → 返回 false
|
||||
{
|
||||
name: "thinking_enabled_matches_thinking_model",
|
||||
name: "thinking_enabled_no_base_mapping_returns_false",
|
||||
modelMapping: map[string]any{
|
||||
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
||||
},
|
||||
requestedModel: "claude-sonnet-4-5",
|
||||
thinkingEnabled: true,
|
||||
expected: true,
|
||||
expected: false,
|
||||
},
|
||||
// 场景 2: 只配置 claude-sonnet-4-5-thinking,请求 claude-sonnet-4-5 + thinking=false
|
||||
// 最终模型名 = claude-sonnet-4-5,不在 mapping 中,应该不匹配
|
||||
// mapAntigravityModel 找不到 claude-sonnet-4-5 的映射 → 返回 false
|
||||
{
|
||||
name: "thinking_disabled_no_match_thinking_only_mapping",
|
||||
name: "thinking_disabled_no_base_mapping_returns_false",
|
||||
modelMapping: map[string]any{
|
||||
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
||||
},
|
||||
@@ -145,15 +145,16 @@ func TestGatewayService_isModelSupportedByAccountWithContext_ThinkingMode(t *tes
|
||||
thinkingEnabled: true,
|
||||
expected: true, // claude-sonnet-4-5-thinking 匹配 claude-*
|
||||
},
|
||||
// 场景 7: 其他模型(非 sonnet-4-5)的 thinking 不受影响
|
||||
// 场景 7: 只配置 thinking 变体但没有基础模型映射 → 返回 false
|
||||
// mapAntigravityModel 找不到 claude-opus-4-6 的映射
|
||||
{
|
||||
name: "opus_thinking_unchanged",
|
||||
name: "opus_thinking_no_base_mapping_returns_false",
|
||||
modelMapping: map[string]any{
|
||||
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
|
||||
},
|
||||
requestedModel: "claude-opus-4-6",
|
||||
thinkingEnabled: true,
|
||||
expected: true, // claude-opus-4-6 映射到 claude-opus-4-6-thinking,匹配
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -175,3 +176,65 @@ func TestGatewayService_isModelSupportedByAccountWithContext_ThinkingMode(t *tes
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGatewayService_isModelSupportedByAccount_CustomMappingNotInDefault 测试自定义模型映射中
|
||||
// 不在 DefaultAntigravityModelMapping 中的模型能通过调度
|
||||
func TestGatewayService_isModelSupportedByAccount_CustomMappingNotInDefault(t *testing.T) {
|
||||
svc := &GatewayService{}
|
||||
|
||||
// 自定义映射中包含不在默认映射中的模型
|
||||
account := &Account{
|
||||
Platform: PlatformAntigravity,
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{
|
||||
"my-custom-model": "actual-upstream-model",
|
||||
"gpt-4o": "some-upstream-model",
|
||||
"llama-3-70b": "llama-3-70b-upstream",
|
||||
"claude-sonnet-4-5": "claude-sonnet-4-5",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 自定义模型应该通过(不在 DefaultAntigravityModelMapping 中也可以)
|
||||
require.True(t, svc.isModelSupportedByAccount(account, "my-custom-model"))
|
||||
require.True(t, svc.isModelSupportedByAccount(account, "gpt-4o"))
|
||||
require.True(t, svc.isModelSupportedByAccount(account, "llama-3-70b"))
|
||||
require.True(t, svc.isModelSupportedByAccount(account, "claude-sonnet-4-5"))
|
||||
|
||||
// 不在自定义映射中的模型不通过
|
||||
require.False(t, svc.isModelSupportedByAccount(account, "gpt-3.5-turbo"))
|
||||
require.False(t, svc.isModelSupportedByAccount(account, "unknown-model"))
|
||||
|
||||
// 空模型允许
|
||||
require.True(t, svc.isModelSupportedByAccount(account, ""))
|
||||
}
|
||||
|
||||
// TestGatewayService_isModelSupportedByAccountWithContext_CustomMappingThinking
|
||||
// 测试自定义映射 + thinking 模式的交互
|
||||
func TestGatewayService_isModelSupportedByAccountWithContext_CustomMappingThinking(t *testing.T) {
|
||||
svc := &GatewayService{}
|
||||
|
||||
// 自定义映射同时配置基础模型和 thinking 变体
|
||||
account := &Account{
|
||||
Platform: PlatformAntigravity,
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{
|
||||
"claude-sonnet-4-5": "claude-sonnet-4-5",
|
||||
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
||||
"my-custom-model": "upstream-model",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// thinking=true: claude-sonnet-4-5 → mapped=claude-sonnet-4-5 → +thinking → check IsModelSupported(claude-sonnet-4-5-thinking)=true
|
||||
ctx := context.WithValue(context.Background(), ctxkey.ThinkingEnabled, true)
|
||||
require.True(t, svc.isModelSupportedByAccountWithContext(ctx, account, "claude-sonnet-4-5"))
|
||||
|
||||
// thinking=false: claude-sonnet-4-5 → mapped=claude-sonnet-4-5 → check IsModelSupported(claude-sonnet-4-5)=true
|
||||
ctx = context.WithValue(context.Background(), ctxkey.ThinkingEnabled, false)
|
||||
require.True(t, svc.isModelSupportedByAccountWithContext(ctx, account, "claude-sonnet-4-5"))
|
||||
|
||||
// 自定义模型(非 claude)不受 thinking 后缀影响,mapped 成功即通过
|
||||
ctx = context.WithValue(context.Background(), ctxkey.ThinkingEnabled, true)
|
||||
require.True(t, svc.isModelSupportedByAccountWithContext(ctx, account, "my-custom-model"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user