mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
fix(antigravity): add project fallback for standard tier
This commit is contained in:
@@ -90,6 +90,10 @@ const (
|
||||
antigravityFallbackSecondsEnv = "GATEWAY_ANTIGRAVITY_FALLBACK_COOLDOWN_SECONDS"
|
||||
)
|
||||
|
||||
const antigravityProjectIDFallbackCredentialKey = "antigravity_project_id"
|
||||
|
||||
var errAntigravityProjectIDRequired = errors.New("该 standard-tier Antigravity 账号需配置 project_id")
|
||||
|
||||
// AntigravityAccountSwitchError 账号切换信号
|
||||
// 当账号限流时间超过阈值时,通知上层切换账号
|
||||
type AntigravityAccountSwitchError struct {
|
||||
@@ -1029,6 +1033,22 @@ func (s *AntigravityGatewayService) getMappedModel(account *Account, requestedMo
|
||||
return mapAntigravityModel(account, requestedModel)
|
||||
}
|
||||
|
||||
func resolveAntigravityProjectID(account *Account) (string, error) {
|
||||
if account == nil {
|
||||
return "", errAntigravityProjectIDRequired
|
||||
}
|
||||
if projectID := strings.TrimSpace(account.GetCredential("project_id")); projectID != "" {
|
||||
return projectID, nil
|
||||
}
|
||||
if projectID := strings.TrimSpace(account.GetCredential(antigravityProjectIDFallbackCredentialKey)); projectID != "" {
|
||||
return projectID, nil
|
||||
}
|
||||
if projectID := strings.TrimSpace(account.GetExtraString(antigravityProjectIDFallbackCredentialKey)); projectID != "" {
|
||||
return projectID, nil
|
||||
}
|
||||
return "", errAntigravityProjectIDRequired
|
||||
}
|
||||
|
||||
// applyThinkingModelSuffix 根据 thinking 配置调整模型名
|
||||
// 当映射结果是 claude-sonnet-4-5 且请求开启了 thinking 时,改为 claude-sonnet-4-5-thinking
|
||||
func applyThinkingModelSuffix(mappedModel string, thinkingEnabled bool) string {
|
||||
@@ -1068,8 +1088,10 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account
|
||||
return nil, fmt.Errorf("获取 access_token 失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取 project_id(部分账户类型可能没有)
|
||||
projectID := strings.TrimSpace(account.GetCredential("project_id"))
|
||||
projectID, err := resolveAntigravityProjectID(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 模型映射
|
||||
mappedModel := s.getMappedModel(account, modelID)
|
||||
@@ -1326,6 +1348,10 @@ func (s *AntigravityGatewayService) wrapV1InternalRequest(projectID, model strin
|
||||
if err := json.Unmarshal(originalBody, &request); err != nil {
|
||||
return nil, fmt.Errorf("解析请求体失败: %w", err)
|
||||
}
|
||||
projectID = strings.TrimSpace(projectID)
|
||||
if projectID == "" {
|
||||
return nil, errAntigravityProjectIDRequired
|
||||
}
|
||||
|
||||
wrapped := map[string]any{
|
||||
"project": projectID,
|
||||
@@ -1403,8 +1429,11 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 project_id(部分账户类型可能没有)
|
||||
projectID := strings.TrimSpace(account.GetCredential("project_id"))
|
||||
projectID, err := resolveAntigravityProjectID(account)
|
||||
if err != nil {
|
||||
_ = s.writeClaudeError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 代理 URL
|
||||
proxyURL := ""
|
||||
@@ -2171,8 +2200,11 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 project_id(部分账户类型可能没有)
|
||||
projectID := strings.TrimSpace(account.GetCredential("project_id"))
|
||||
projectID, err := resolveAntigravityProjectID(account)
|
||||
if err != nil {
|
||||
_ = s.writeGoogleError(c, http.StatusBadRequest, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 代理 URL
|
||||
proxyURL := ""
|
||||
|
||||
@@ -185,6 +185,21 @@ func (s *queuedHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, a
|
||||
return s.Do(req, proxyURL, accountID, concurrency)
|
||||
}
|
||||
|
||||
type recordingInternal500CounterCache struct {
|
||||
incrementCalls []int64
|
||||
resetCalls []int64
|
||||
}
|
||||
|
||||
func (c *recordingInternal500CounterCache) IncrementInternal500Count(_ context.Context, accountID int64) (int64, error) {
|
||||
c.incrementCalls = append(c.incrementCalls, accountID)
|
||||
return int64(len(c.incrementCalls)), nil
|
||||
}
|
||||
|
||||
func (c *recordingInternal500CounterCache) ResetInternal500Count(_ context.Context, accountID int64) error {
|
||||
c.resetCalls = append(c.resetCalls, accountID)
|
||||
return nil
|
||||
}
|
||||
|
||||
type antigravitySettingRepoStub struct{}
|
||||
|
||||
func (s *antigravitySettingRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
|
||||
@@ -215,6 +230,157 @@ func (s *antigravitySettingRepoStub) Delete(ctx context.Context, key string) err
|
||||
panic("unexpected Delete call")
|
||||
}
|
||||
|
||||
func TestResolveAntigravityProjectID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
account *Account
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "uses onboard project_id first",
|
||||
account: &Account{Credentials: map[string]any{
|
||||
"project_id": " onboard-project ",
|
||||
antigravityProjectIDFallbackCredentialKey: " configured-project ",
|
||||
}},
|
||||
want: "onboard-project",
|
||||
},
|
||||
{
|
||||
name: "uses configured credentials fallback",
|
||||
account: &Account{Credentials: map[string]any{
|
||||
antigravityProjectIDFallbackCredentialKey: " configured-project ",
|
||||
}},
|
||||
want: "configured-project",
|
||||
},
|
||||
{
|
||||
name: "uses configured extra fallback",
|
||||
account: &Account{Extra: map[string]any{
|
||||
antigravityProjectIDFallbackCredentialKey: " extra-project ",
|
||||
}},
|
||||
want: "extra-project",
|
||||
},
|
||||
{
|
||||
name: "missing project",
|
||||
account: &Account{Credentials: map[string]any{}},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := resolveAntigravityProjectID(tc.account)
|
||||
if tc.wantErr {
|
||||
require.ErrorIs(t, err, errAntigravityProjectIDRequired)
|
||||
require.Empty(t, got)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAntigravityGatewayService_ForwardGemini_UsesConfiguredProjectFallback(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
writer := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(writer)
|
||||
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"contents": []map[string]any{
|
||||
{"role": "user", "parts": []map[string]any{{"text": "hello"}}},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/antigravity/v1beta/models/gemini-2.5-flash:streamGenerateContent", bytes.NewReader(body))
|
||||
|
||||
upstreamBody := []byte("data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"ok\"}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":1,\"candidatesTokenCount\":1}}}\n\n")
|
||||
upstream := &queuedHTTPUpstreamStub{
|
||||
responses: []*http.Response{
|
||||
{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
|
||||
Body: io.NopCloser(bytes.NewReader(upstreamBody)),
|
||||
},
|
||||
},
|
||||
}
|
||||
svc := &AntigravityGatewayService{
|
||||
settingService: NewSettingService(&antigravitySettingRepoStub{}, &config.Config{Gateway: config.GatewayConfig{MaxLineSize: defaultMaxLineSize}}),
|
||||
tokenProvider: &AntigravityTokenProvider{},
|
||||
httpUpstream: upstream,
|
||||
}
|
||||
|
||||
account := &Account{
|
||||
ID: 101,
|
||||
Name: "acc-configured-project",
|
||||
Platform: PlatformAntigravity,
|
||||
Type: AccountTypeOAuth,
|
||||
Status: StatusActive,
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
antigravityProjectIDFallbackCredentialKey: "configured-project",
|
||||
"model_mapping": map[string]any{
|
||||
"gemini-2.5-flash": "gemini-2.5-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := svc.ForwardGemini(context.Background(), c, account, "gemini-2.5-flash", "streamGenerateContent", true, body, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, upstream.requestBodies, 1)
|
||||
|
||||
var wrapped map[string]any
|
||||
require.NoError(t, json.Unmarshal(upstream.requestBodies[0], &wrapped))
|
||||
require.Equal(t, "configured-project", wrapped["project"])
|
||||
}
|
||||
|
||||
func TestAntigravityGatewayService_ForwardGemini_MissingProjectReturnsLocalError(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
writer := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(writer)
|
||||
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"contents": []map[string]any{
|
||||
{"role": "user", "parts": []map[string]any{{"text": "hello"}}},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/antigravity/v1beta/models/gemini-2.5-flash:streamGenerateContent", bytes.NewReader(body))
|
||||
|
||||
upstream := &queuedHTTPUpstreamStub{}
|
||||
internal500Cache := &recordingInternal500CounterCache{}
|
||||
svc := &AntigravityGatewayService{
|
||||
tokenProvider: &AntigravityTokenProvider{},
|
||||
httpUpstream: upstream,
|
||||
internal500Cache: internal500Cache,
|
||||
}
|
||||
|
||||
account := &Account{
|
||||
ID: 102,
|
||||
Name: "acc-missing-project",
|
||||
Platform: PlatformAntigravity,
|
||||
Type: AccountTypeOAuth,
|
||||
Status: StatusActive,
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"model_mapping": map[string]any{
|
||||
"gemini-2.5-flash": "gemini-2.5-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := svc.ForwardGemini(context.Background(), c, account, "gemini-2.5-flash", "streamGenerateContent", true, body, false)
|
||||
require.Nil(t, result)
|
||||
require.ErrorIs(t, err, errAntigravityProjectIDRequired)
|
||||
require.Equal(t, http.StatusBadRequest, writer.Code)
|
||||
require.Empty(t, upstream.requestBodies)
|
||||
require.Empty(t, internal500Cache.incrementCalls)
|
||||
require.Contains(t, writer.Body.String(), "project_id")
|
||||
require.NotContains(t, writer.Body.String(), `"project":""`)
|
||||
}
|
||||
|
||||
func TestAntigravityGatewayService_Forward_PromptTooLong(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
writer := httptest.NewRecorder()
|
||||
@@ -255,6 +421,7 @@ func TestAntigravityGatewayService_Forward_PromptTooLong(t *testing.T) {
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"project_id": "proj",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -313,6 +480,7 @@ func TestAntigravityGatewayService_Forward_ModelRateLimitTriggersFailover(t *tes
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"project_id": "proj",
|
||||
},
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
@@ -369,6 +537,7 @@ func TestAntigravityGatewayService_ForwardGemini_ModelRateLimitTriggersFailover(
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"project_id": "proj",
|
||||
},
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
@@ -423,6 +592,7 @@ func TestAntigravityGatewayService_Forward_StickySessionForceCacheBilling(t *tes
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"project_id": "proj",
|
||||
},
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
@@ -478,6 +648,7 @@ func TestAntigravityGatewayService_ForwardGemini_StickySessionForceCacheBilling(
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"project_id": "proj",
|
||||
},
|
||||
Extra: map[string]any{
|
||||
modelRateLimitsKey: map[string]any{
|
||||
@@ -623,6 +794,7 @@ func TestAntigravityGatewayService_Forward_BillsWithMappedModel(t *testing.T) {
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"project_id": "proj",
|
||||
"model_mapping": map[string]any{
|
||||
"claude-sonnet-4-5": mappedModel,
|
||||
},
|
||||
@@ -676,6 +848,7 @@ func TestAntigravityGatewayService_ForwardGemini_BillsWithMappedModel(t *testing
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"project_id": "proj",
|
||||
"model_mapping": map[string]any{
|
||||
"gemini-2.5-flash": mappedModel,
|
||||
},
|
||||
@@ -747,6 +920,7 @@ func TestAntigravityGatewayService_ForwardGemini_RetriesCorruptedThoughtSignatur
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"project_id": "proj",
|
||||
"model_mapping": map[string]any{
|
||||
originalModel: mappedModel,
|
||||
},
|
||||
@@ -805,6 +979,7 @@ func TestAntigravityGatewayService_ForwardGemini_SignatureRetryPropagatesFailove
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "token",
|
||||
"project_id": "proj",
|
||||
"model_mapping": map[string]any{
|
||||
originalModel: mappedModel,
|
||||
},
|
||||
|
||||
@@ -774,6 +774,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.platform === 'antigravity' && antigravityAccountType === 'oauth'">
|
||||
<label class="input-label">{{ t('admin.accounts.antigravityProjectIdLabel') }}</label>
|
||||
<input
|
||||
v-model="antigravityProjectId"
|
||||
data-testid="antigravity-project-id-input"
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:placeholder="t('admin.accounts.antigravityProjectIdPlaceholder')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.antigravityProjectIdHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Upstream config (only for Antigravity upstream type) -->
|
||||
<div v-if="form.platform === 'antigravity' && antigravityAccountType === 'upstream'" class="space-y-4">
|
||||
<div>
|
||||
@@ -3238,7 +3250,10 @@ import ProxyAdBanner from '@/components/common/ProxyAdBanner.vue'
|
||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
||||
import {
|
||||
applyAntigravityProjectID,
|
||||
applyInterceptWarmup
|
||||
} from '@/components/account/credentialsBuilder'
|
||||
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
||||
import { VERTEX_LOCATION_OPTIONS } from '@/constants/account'
|
||||
@@ -3440,6 +3455,7 @@ loadQuotaNotifyGlobal()
|
||||
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
||||
const allowOverages = ref(false) // For antigravity accounts: enable AI Credits overages
|
||||
const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream
|
||||
const antigravityProjectId = ref('')
|
||||
const upstreamBaseUrl = ref('') // For upstream type: base URL
|
||||
const upstreamApiKey = ref('') // For upstream type: API key
|
||||
const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
||||
@@ -3813,6 +3829,7 @@ watch(
|
||||
antigravityAccountType.value = 'oauth'
|
||||
} else {
|
||||
allowOverages.value = false
|
||||
antigravityProjectId.value = ''
|
||||
antigravityWhitelistModels.value = []
|
||||
antigravityModelMappings.value = []
|
||||
antigravityModelRestrictionMode.value = 'mapping'
|
||||
@@ -4271,6 +4288,7 @@ const resetForm = () => {
|
||||
customBaseUrl.value = ''
|
||||
allowOverages.value = false
|
||||
antigravityAccountType.value = 'oauth'
|
||||
antigravityProjectId.value = ''
|
||||
upstreamBaseUrl.value = ''
|
||||
upstreamApiKey.value = ''
|
||||
vertexServiceAccountJson.value = ''
|
||||
@@ -5133,6 +5151,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
|
||||
}
|
||||
|
||||
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
|
||||
applyAntigravityProjectID(credentials, antigravityProjectId.value, 'create')
|
||||
|
||||
// Generate account name with index for batch
|
||||
const accountName = refreshTokens.length > 1 ? `${form.name} #${i + 1}` : form.name
|
||||
@@ -5249,6 +5268,7 @@ const handleAntigravityExchange = async (authCode: string) => {
|
||||
if (!tokenInfo) return
|
||||
|
||||
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
|
||||
applyAntigravityProjectID(credentials, antigravityProjectId.value, 'create')
|
||||
applyInterceptWarmup(credentials, interceptWarmupRequests.value, 'create')
|
||||
// Antigravity 只使用映射模式
|
||||
const antigravityModelMapping = buildModelMappingObject(
|
||||
|
||||
@@ -1000,6 +1000,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="account.platform === 'antigravity' && account.type === 'oauth'"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
>
|
||||
<label class="input-label">{{ t('admin.accounts.antigravityProjectIdLabel') }}</label>
|
||||
<input
|
||||
v-model="antigravityProjectId"
|
||||
data-testid="antigravity-project-id-input"
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:placeholder="t('admin.accounts.antigravityProjectIdPlaceholder')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.antigravityProjectIdHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Antigravity model restriction (applies to all antigravity types) -->
|
||||
<!-- Antigravity 只支持模型映射模式,不支持白名单模式 -->
|
||||
<div v-if="account.platform === 'antigravity'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
@@ -2398,7 +2413,10 @@ import ProxyAdBanner from '@/components/common/ProxyAdBanner.vue'
|
||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
||||
import {
|
||||
applyAntigravityProjectID,
|
||||
applyInterceptWarmup
|
||||
} from '@/components/account/credentialsBuilder'
|
||||
import { formatDateTime, formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
||||
import { VERTEX_LOCATION_OPTIONS } from '@/constants/account'
|
||||
@@ -2531,6 +2549,7 @@ const autoPause5hDisabled = ref(false)
|
||||
const autoPause7dDisabled = ref(false)
|
||||
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
||||
const allowOverages = ref(false) // For antigravity accounts: enable AI Credits overages
|
||||
const antigravityProjectId = ref('')
|
||||
const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
||||
const antigravityWhitelistModels = ref<string[]>([])
|
||||
const antigravityModelMappings = ref<ModelMapping[]>([])
|
||||
@@ -2940,6 +2959,12 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
editVertexProjectId.value = ''
|
||||
editVertexClientEmail.value = ''
|
||||
editVertexLocation.value = 'us-central1'
|
||||
antigravityProjectId.value =
|
||||
newAccount.platform === 'antigravity' &&
|
||||
newAccount.type === 'oauth' &&
|
||||
typeof credentials?.antigravity_project_id === 'string'
|
||||
? credentials.antigravity_project_id.trim()
|
||||
: ''
|
||||
|
||||
// Load mixed scheduling setting (only for antigravity accounts)
|
||||
mixedScheduling.value = false
|
||||
@@ -3930,6 +3955,9 @@ const handleSubmit = async () => {
|
||||
const currentCredentials = (updatePayload.credentials as Record<string, unknown>) ||
|
||||
((props.account.credentials as Record<string, unknown>) || {})
|
||||
const newCredentials: Record<string, unknown> = { ...currentCredentials }
|
||||
if (props.account.type === 'oauth') {
|
||||
applyAntigravityProjectID(newCredentials, antigravityProjectId.value, 'edit')
|
||||
}
|
||||
|
||||
// 移除旧字段
|
||||
delete newCredentials.model_whitelist
|
||||
|
||||
@@ -167,6 +167,31 @@ function buildVertexAccount() {
|
||||
} as any
|
||||
}
|
||||
|
||||
function buildAntigravityAccount(projectId = 'configured-project') {
|
||||
return {
|
||||
id: 3,
|
||||
name: 'Antigravity OAuth',
|
||||
notes: '',
|
||||
platform: 'antigravity',
|
||||
type: 'oauth',
|
||||
credentials: {
|
||||
antigravity_project_id: projectId,
|
||||
model_mapping: {
|
||||
'gemini-2.5-flash': 'gemini-2.5-flash'
|
||||
}
|
||||
},
|
||||
extra: {},
|
||||
proxy_id: null,
|
||||
concurrency: 1,
|
||||
priority: 1,
|
||||
rate_multiplier: 1,
|
||||
status: 'active',
|
||||
group_ids: [],
|
||||
expires_at: null,
|
||||
auto_pause_on_expired: false
|
||||
} as any
|
||||
}
|
||||
|
||||
function mountModal(account = buildAccount()) {
|
||||
return mount(EditAccountModal, {
|
||||
props: {
|
||||
@@ -579,4 +604,43 @@ describe('EditAccountModal', () => {
|
||||
|
||||
expect(updateAccountMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('loads and submits Antigravity configured project fallback', async () => {
|
||||
const account = buildAntigravityAccount('configured-project')
|
||||
updateAccountMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
||||
updateAccountMock.mockResolvedValue(account)
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
const input = wrapper.get<HTMLInputElement>('[data-testid="antigravity-project-id-input"]')
|
||||
expect(input.element.value).toBe('configured-project')
|
||||
|
||||
await input.setValue(' updated-project ')
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials?.antigravity_project_id).toBe(
|
||||
'updated-project'
|
||||
)
|
||||
})
|
||||
|
||||
it('clears Antigravity configured project fallback when input is empty', async () => {
|
||||
const account = buildAntigravityAccount('configured-project')
|
||||
updateAccountMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
||||
updateAccountMock.mockResolvedValue(account)
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
const input = wrapper.get<HTMLInputElement>('[data-testid="antigravity-project-id-input"]')
|
||||
|
||||
await input.setValue('')
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials).not.toHaveProperty(
|
||||
'antigravity_project_id'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { applyInterceptWarmup } from '../credentialsBuilder'
|
||||
import {
|
||||
ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY,
|
||||
applyAntigravityProjectID,
|
||||
applyInterceptWarmup
|
||||
} from '../credentialsBuilder'
|
||||
|
||||
describe('applyInterceptWarmup', () => {
|
||||
it('create + enabled=true: should set intercept_warmup_requests to true', () => {
|
||||
@@ -44,3 +48,37 @@ describe('applyInterceptWarmup', () => {
|
||||
expect('intercept_warmup_requests' in creds).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyAntigravityProjectID', () => {
|
||||
it('create + project id: trims and stores configured project fallback', () => {
|
||||
const creds: Record<string, unknown> = { access_token: 'tok' }
|
||||
applyAntigravityProjectID(creds, ' configured-project ', 'create')
|
||||
expect(creds[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY]).toBe('configured-project')
|
||||
})
|
||||
|
||||
it('create + empty project id: should not add the field', () => {
|
||||
const creds: Record<string, unknown> = { access_token: 'tok' }
|
||||
applyAntigravityProjectID(creds, ' ', 'create')
|
||||
expect(ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY in creds).toBe(false)
|
||||
})
|
||||
|
||||
it('edit + empty project id: deletes existing fallback', () => {
|
||||
const creds: Record<string, unknown> = {
|
||||
access_token: 'tok',
|
||||
[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY]: 'old-project'
|
||||
}
|
||||
applyAntigravityProjectID(creds, '', 'edit')
|
||||
expect(ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY in creds).toBe(false)
|
||||
})
|
||||
|
||||
it('does not affect onboard project_id or other credentials', () => {
|
||||
const creds: Record<string, unknown> = {
|
||||
project_id: 'onboard-project',
|
||||
model_mapping: { 'gemini-*': 'gemini-2.5-flash' }
|
||||
}
|
||||
applyAntigravityProjectID(creds, 'configured-project', 'edit')
|
||||
expect(creds.project_id).toBe('onboard-project')
|
||||
expect(creds.model_mapping).toEqual({ 'gemini-*': 'gemini-2.5-flash' })
|
||||
expect(creds[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY]).toBe('configured-project')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,3 +9,18 @@ export function applyInterceptWarmup(
|
||||
delete credentials.intercept_warmup_requests
|
||||
}
|
||||
}
|
||||
|
||||
export const ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY = 'antigravity_project_id'
|
||||
|
||||
export function applyAntigravityProjectID(
|
||||
credentials: Record<string, unknown>,
|
||||
projectId: string,
|
||||
mode: 'create' | 'edit'
|
||||
): void {
|
||||
const trimmed = projectId.trim()
|
||||
if (trimmed) {
|
||||
credentials[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY] = trimmed
|
||||
} else if (mode === 'edit') {
|
||||
delete credentials[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3139,6 +3139,10 @@ export default {
|
||||
upstream: 'Upstream',
|
||||
upstreamDesc: 'Connect via Base URL + API Key'
|
||||
},
|
||||
antigravityProjectIdLabel: 'GCP Project ID (optional)',
|
||||
antigravityProjectIdPlaceholder: 'your-gcp-project-id',
|
||||
antigravityProjectIdHint:
|
||||
'Antigravity standard-tier accounts that do not receive an automatic project_id need a user-owned GCP project.',
|
||||
status: {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
|
||||
@@ -3329,6 +3329,10 @@ export default {
|
||||
api_key: 'API Key',
|
||||
cookie: 'Cookie'
|
||||
},
|
||||
antigravityProjectIdLabel: 'GCP Project ID(可选)',
|
||||
antigravityProjectIdPlaceholder: 'your-gcp-project-id',
|
||||
antigravityProjectIdHint:
|
||||
'standard-tier 且未自动返回 project_id 的 Antigravity 账号需要填写用户自带 GCP project。',
|
||||
status: {
|
||||
active: '正常',
|
||||
inactive: '停用',
|
||||
|
||||
Reference in New Issue
Block a user