From 650c50e34bc8754471be629c0689ba69b6e8d682 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Thu, 25 Jun 2026 16:29:33 +0800 Subject: [PATCH] fix(antigravity): add project fallback for standard tier --- .../service/antigravity_gateway_service.go | 44 ++++- .../antigravity_gateway_service_test.go | 175 ++++++++++++++++++ .../components/account/CreateAccountModal.vue | 22 ++- .../components/account/EditAccountModal.vue | 30 ++- .../__tests__/EditAccountModal.spec.ts | 64 +++++++ .../__tests__/credentialsBuilder.spec.ts | 40 +++- .../components/account/credentialsBuilder.ts | 15 ++ frontend/src/i18n/locales/en.ts | 4 + frontend/src/i18n/locales/zh.ts | 4 + 9 files changed, 389 insertions(+), 9 deletions(-) diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 7bdda2e507..aa4cab22d7 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -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 := "" diff --git a/backend/internal/service/antigravity_gateway_service_test.go b/backend/internal/service/antigravity_gateway_service_test.go index 0fac7a1eb6..02c2d85e36 100644 --- a/backend/internal/service/antigravity_gateway_service_test.go +++ b/backend/internal/service/antigravity_gateway_service_test.go @@ -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, }, diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index d1a7729a58..16cbf43af4 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -774,6 +774,18 @@ +
+ + +

{{ t('admin.accounts.antigravityProjectIdHint') }}

+
+
@@ -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( diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 8dc85d0eb8..cabcc03167 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1000,6 +1000,21 @@
+
+ + +

{{ t('admin.accounts.antigravityProjectIdHint') }}

+
+
@@ -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([]) const antigravityModelMappings = ref([]) @@ -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) || ((props.account.credentials as Record) || {}) const newCredentials: Record = { ...currentCredentials } + if (props.account.type === 'oauth') { + applyAntigravityProjectID(newCredentials, antigravityProjectId.value, 'edit') + } // 移除旧字段 delete newCredentials.model_whitelist diff --git a/frontend/src/components/account/__tests__/EditAccountModal.spec.ts b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts index f4865de9e6..77c333fb2d 100644 --- a/frontend/src/components/account/__tests__/EditAccountModal.spec.ts +++ b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts @@ -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('[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('[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' + ) + }) }) diff --git a/frontend/src/components/account/__tests__/credentialsBuilder.spec.ts b/frontend/src/components/account/__tests__/credentialsBuilder.spec.ts index be2a8d521c..665b1732e7 100644 --- a/frontend/src/components/account/__tests__/credentialsBuilder.spec.ts +++ b/frontend/src/components/account/__tests__/credentialsBuilder.spec.ts @@ -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 = { 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 = { 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 = { + 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 = { + 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') + }) +}) diff --git a/frontend/src/components/account/credentialsBuilder.ts b/frontend/src/components/account/credentialsBuilder.ts index b8008e8bfb..f138976519 100644 --- a/frontend/src/components/account/credentialsBuilder.ts +++ b/frontend/src/components/account/credentialsBuilder.ts @@ -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, + 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] + } +} diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index bfbde93293..35545b7a62 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -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', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 47d0054493..5fcb9d6b58 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -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: '停用',