From 4644af2ccc7e56fd30c9371ec2ad4f0328bee448 Mon Sep 17 00:00:00 2001 From: SsageParuders Date: Sat, 14 Mar 2026 17:13:30 +0800 Subject: [PATCH] refactor: merge bedrock-apikey into bedrock with auth_mode credential Consolidate two separate channel types (bedrock + bedrock-apikey) into a single "AWS Bedrock" channel. Authentication mode is now distinguished by credentials.auth_mode ("sigv4" | "apikey") instead of separate types. Backend: - Remove AccountTypeBedrockAPIKey constant - IsBedrock() simplified; IsBedrockAPIKey() checks auth_mode - Add IsAPIKeyOrBedrock() helper to eliminate repeated type checks - Extend pool mode, quota scheduling, and billing to bedrock - Add RetryableOnSameAccount to handleBedrockUpstreamErrors - Add "bedrock" scope to Beta Policy for independent control Frontend: - Merge two buttons into one "AWS Bedrock" with auth mode radio - Badge displays "Anthropic | AWS" - Pool mode and quota limit UI available for bedrock - Quota display in account list (usage bars, capacity badges, reset) - Remove all bedrock-apikey type references --- backend/internal/domain/constants.go | 3 +- .../internal/handler/admin/account_handler.go | 4 +- backend/internal/handler/dto/mappers.go | 4 +- backend/internal/service/account.go | 11 +- backend/internal/service/domain_constants.go | 3 +- backend/internal/service/gateway_service.go | 38 +- backend/internal/service/setting_service.go | 2 +- backend/internal/service/settings_view.go | 9 +- frontend/src/api/admin/settings.ts | 2 +- .../account/AccountCapacityCell.vue | 10 +- .../components/account/AccountUsageCell.vue | 2 +- .../components/account/CreateAccountModal.vue | 370 +++++++----------- .../components/account/EditAccountModal.vue | 319 +++++++-------- .../admin/account/AccountActionMenu.vue | 2 +- .../components/common/PlatformTypeBadge.vue | 2 +- frontend/src/composables/useModelWhitelist.ts | 2 +- frontend/src/i18n/locales/en.ts | 6 +- frontend/src/i18n/locales/zh.ts | 6 +- frontend/src/types/index.ts | 2 +- frontend/src/views/admin/SettingsView.vue | 5 +- 20 files changed, 339 insertions(+), 463 deletions(-) diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go index 36d043b5..baab8379 100644 --- a/backend/internal/domain/constants.go +++ b/backend/internal/domain/constants.go @@ -31,8 +31,7 @@ const ( AccountTypeSetupToken = "setup-token" // Setup Token类型账号(inference only scope) AccountTypeAPIKey = "apikey" // API Key类型账号 AccountTypeUpstream = "upstream" // 上游透传类型账号(通过 Base URL + API Key 连接上游) - AccountTypeBedrock = "bedrock" // AWS Bedrock 类型账号(通过 SigV4 签名连接 Bedrock) - AccountTypeBedrockAPIKey = "bedrock-apikey" // AWS Bedrock API Key 类型账号(通过 Bearer Token 连接 Bedrock) + AccountTypeBedrock = "bedrock" // AWS Bedrock 类型账号(通过 SigV4 签名或 API Key 连接 Bedrock,由 credentials.auth_mode 区分) ) // Redeem type constants diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 7fdd5ad4..3ef213e1 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -97,7 +97,7 @@ type CreateAccountRequest struct { Name string `json:"name" binding:"required"` Notes *string `json:"notes"` Platform string `json:"platform" binding:"required"` - Type string `json:"type" binding:"required,oneof=oauth setup-token apikey upstream bedrock bedrock-apikey"` + Type string `json:"type" binding:"required,oneof=oauth setup-token apikey upstream bedrock"` Credentials map[string]any `json:"credentials" binding:"required"` Extra map[string]any `json:"extra"` ProxyID *int64 `json:"proxy_id"` @@ -116,7 +116,7 @@ type CreateAccountRequest struct { type UpdateAccountRequest struct { Name string `json:"name"` Notes *string `json:"notes"` - Type string `json:"type" binding:"omitempty,oneof=oauth setup-token apikey upstream bedrock bedrock-apikey"` + Type string `json:"type" binding:"omitempty,oneof=oauth setup-token apikey upstream bedrock"` Credentials map[string]any `json:"credentials"` Extra map[string]any `json:"extra"` ProxyID *int64 `json:"proxy_id"` diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index 205ccd65..972c3e5e 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -264,8 +264,8 @@ func AccountFromServiceShallow(a *service.Account) *Account { } } - // 提取 API Key 账号配额限制(仅 apikey 类型有效) - if a.Type == service.AccountTypeAPIKey { + // 提取账号配额限制(apikey / bedrock 类型有效) + if a.IsAPIKeyOrBedrock() { if limit := a.GetQuotaLimit(); limit > 0 { out.QuotaLimit = &limit used := a.GetQuotaUsed() diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index 6c88ed68..cd8b9378 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -656,7 +656,7 @@ func (a *Account) IsCustomErrorCodesEnabled() bool { // IsPoolMode 检查 API Key 账号是否启用池模式。 // 池模式下,上游错误不标记本地账号状态,而是在同一账号上重试。 func (a *Account) IsPoolMode() bool { - if a.Type != AccountTypeAPIKey || a.Credentials == nil { + if !a.IsAPIKeyOrBedrock() || a.Credentials == nil { return false } if v, ok := a.Credentials["pool_mode"]; ok { @@ -771,11 +771,16 @@ func (a *Account) IsInterceptWarmupEnabled() bool { } func (a *Account) IsBedrock() bool { - return a.Platform == PlatformAnthropic && (a.Type == AccountTypeBedrock || a.Type == AccountTypeBedrockAPIKey) + return a.Platform == PlatformAnthropic && a.Type == AccountTypeBedrock } func (a *Account) IsBedrockAPIKey() bool { - return a.Platform == PlatformAnthropic && a.Type == AccountTypeBedrockAPIKey + return a.IsBedrock() && a.GetCredential("auth_mode") == "apikey" +} + +// IsAPIKeyOrBedrock 返回账号类型是否支持配额和池模式等特性 +func (a *Account) IsAPIKeyOrBedrock() bool { + return a.Type == AccountTypeAPIKey || a.Type == AccountTypeBedrock } func (a *Account) IsOpenAI() bool { diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index ad64b467..69166401 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -33,8 +33,7 @@ const ( AccountTypeSetupToken = domain.AccountTypeSetupToken // Setup Token类型账号(inference only scope) AccountTypeAPIKey = domain.AccountTypeAPIKey // API Key类型账号 AccountTypeUpstream = domain.AccountTypeUpstream // 上游透传类型账号(通过 Base URL + API Key 连接上游) - AccountTypeBedrock = domain.AccountTypeBedrock // AWS Bedrock 类型账号(通过 SigV4 签名连接 Bedrock) - AccountTypeBedrockAPIKey = domain.AccountTypeBedrockAPIKey // AWS Bedrock API Key 类型账号(通过 Bearer Token 连接 Bedrock) + AccountTypeBedrock = domain.AccountTypeBedrock // AWS Bedrock 类型账号(通过 SigV4 签名或 API Key 连接 Bedrock,由 credentials.auth_mode 区分) ) // Redeem type constants diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index c86b6964..0240519e 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -2173,10 +2173,10 @@ func (s *GatewayService) withWindowCostPrefetch(ctx context.Context, accounts [] return context.WithValue(ctx, windowCostPrefetchContextKey, costs) } -// isAccountSchedulableForQuota 检查 API Key 账号是否在配额限制内 -// 仅适用于配置了 quota_limit 的 apikey 类型账号 +// isAccountSchedulableForQuota 检查账号是否在配额限制内 +// 适用于配置了 quota_limit 的 apikey 和 bedrock 类型账号 func (s *GatewayService) isAccountSchedulableForQuota(account *Account) bool { - if account.Type != AccountTypeAPIKey { + if !account.IsAPIKeyOrBedrock() { return true } return !account.IsQuotaExceeded() @@ -3532,9 +3532,7 @@ func (s *GatewayService) GetAccessToken(ctx context.Context, account *Account) ( } return apiKey, "apikey", nil case AccountTypeBedrock: - return "", "bedrock", nil // Bedrock 使用 SigV4 签名,不需要 token - case AccountTypeBedrockAPIKey: - return "", "bedrock-apikey", nil // Bedrock API Key 使用 Bearer Token,由 forwardBedrock 处理 + return "", "bedrock", nil // Bedrock 使用 SigV4 签名或 API Key,由 forwardBedrock 处理 default: return "", "", fmt.Errorf("unsupported account type: %s", account.Type) } @@ -5186,7 +5184,7 @@ func (s *GatewayService) forwardBedrock( if account.IsBedrockAPIKey() { bedrockAPIKey = account.GetCredential("api_key") if bedrockAPIKey == "" { - return nil, fmt.Errorf("api_key not found in bedrock-apikey credentials") + return nil, fmt.Errorf("api_key not found in bedrock credentials") } } else { signer, err = NewBedrockSignerFromAccount(account) @@ -5375,8 +5373,9 @@ func (s *GatewayService) handleBedrockUpstreamErrors( Message: extractUpstreamErrorMessage(respBody), }) return nil, &UpstreamFailoverError{ - StatusCode: resp.StatusCode, - ResponseBody: respBody, + StatusCode: resp.StatusCode, + ResponseBody: respBody, + RetryableOnSameAccount: account.IsPoolMode() && isPoolModeRetryableStatus(resp.StatusCode), } } return s.handleRetryExhaustedError(ctx, resp, c, account) @@ -5398,8 +5397,9 @@ func (s *GatewayService) handleBedrockUpstreamErrors( Message: extractUpstreamErrorMessage(respBody), }) return nil, &UpstreamFailoverError{ - StatusCode: resp.StatusCode, - ResponseBody: respBody, + StatusCode: resp.StatusCode, + ResponseBody: respBody, + RetryableOnSameAccount: account.IsPoolMode() && isPoolModeRetryableStatus(resp.StatusCode), } } @@ -5808,9 +5808,10 @@ func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader stri return betaPolicyResult{} } isOAuth := account.IsOAuth() + isBedrock := account.IsBedrock() var result betaPolicyResult for _, rule := range settings.Rules { - if !betaPolicyScopeMatches(rule.Scope, isOAuth) { + if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) { continue } switch rule.Action { @@ -5870,14 +5871,16 @@ func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Cont } // betaPolicyScopeMatches checks whether a rule's scope matches the current account type. -func betaPolicyScopeMatches(scope string, isOAuth bool) bool { +func betaPolicyScopeMatches(scope string, isOAuth bool, isBedrock bool) bool { switch scope { case BetaPolicyScopeAll: return true case BetaPolicyScopeOAuth: return isOAuth case BetaPolicyScopeAPIKey: - return !isOAuth + return !isOAuth && !isBedrock + case BetaPolicyScopeBedrock: + return isBedrock default: return true // unknown scope → match all (fail-open) } @@ -5959,12 +5962,13 @@ func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, toke return nil } isOAuth := account.IsOAuth() + isBedrock := account.IsBedrock() tokenSet := buildBetaTokenSet(tokens) for _, rule := range settings.Rules { if rule.Action != BetaPolicyActionBlock { continue } - if !betaPolicyScopeMatches(rule.Scope, isOAuth) { + if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) { continue } if _, present := tokenSet[rule.BetaToken]; present { @@ -7176,7 +7180,7 @@ func postUsageBilling(ctx context.Context, p *postUsageBillingParams, deps *bill } // 4. 账号配额用量(账号口径:TotalCost × 账号计费倍率) - if cost.TotalCost > 0 && p.Account.Type == AccountTypeAPIKey && p.Account.HasAnyQuotaLimit() { + if cost.TotalCost > 0 && p.Account.IsAPIKeyOrBedrock() && p.Account.HasAnyQuotaLimit() { accountCost := cost.TotalCost * p.AccountRateMultiplier if err := deps.accountRepo.IncrementQuotaUsed(billingCtx, p.Account.ID, accountCost); err != nil { slog.Error("increment account quota used failed", "account_id", p.Account.ID, "cost", accountCost, "error", err) @@ -7264,7 +7268,7 @@ func buildUsageBillingCommand(requestID string, usageLog *UsageLog, p *postUsage if p.Cost.ActualCost > 0 && p.APIKey.HasRateLimits() && p.APIKeyService != nil { cmd.APIKeyRateLimitCost = p.Cost.ActualCost } - if p.Cost.TotalCost > 0 && p.Account.Type == AccountTypeAPIKey && p.Account.HasAnyQuotaLimit() { + if p.Cost.TotalCost > 0 && p.Account.IsAPIKeyOrBedrock() && p.Account.HasAnyQuotaLimit() { cmd.AccountQuotaCost = p.Cost.TotalCost * p.AccountRateMultiplier } diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index b77867de..73ca5101 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -1278,7 +1278,7 @@ func (s *SettingService) SetBetaPolicySettings(ctx context.Context, settings *Be BetaPolicyActionPass: true, BetaPolicyActionFilter: true, BetaPolicyActionBlock: true, } validScopes := map[string]bool{ - BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true, + BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true, BetaPolicyScopeBedrock: true, } for i, rule := range settings.Rules { diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 8734e28a..e5b463d2 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -198,16 +198,17 @@ const ( BetaPolicyActionFilter = "filter" // 过滤,从 beta header 中移除该 token BetaPolicyActionBlock = "block" // 拦截,直接返回错误 - BetaPolicyScopeAll = "all" // 所有账号类型 - BetaPolicyScopeOAuth = "oauth" // 仅 OAuth 账号 - BetaPolicyScopeAPIKey = "apikey" // 仅 API Key 账号 + BetaPolicyScopeAll = "all" // 所有账号类型 + BetaPolicyScopeOAuth = "oauth" // 仅 OAuth 账号 + BetaPolicyScopeAPIKey = "apikey" // 仅 API Key 账号 + BetaPolicyScopeBedrock = "bedrock" // 仅 AWS Bedrock 账号 ) // BetaPolicyRule 单条 Beta 策略规则 type BetaPolicyRule struct { BetaToken string `json:"beta_token"` // beta token 值 Action string `json:"action"` // "pass" | "filter" | "block" - Scope string `json:"scope"` // "all" | "oauth" | "apikey" + Scope string `json:"scope"` // "all" | "oauth" | "apikey" | "bedrock" ErrorMessage string `json:"error_message,omitempty"` // 自定义错误消息 (action=block 时生效) } diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 2b156ea1..063d62ad 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -316,7 +316,7 @@ export async function updateRectifierSettings( export interface BetaPolicyRule { beta_token: string action: 'pass' | 'filter' | 'block' - scope: 'all' | 'oauth' | 'apikey' + scope: 'all' | 'oauth' | 'apikey' | 'bedrock' error_message?: string } diff --git a/frontend/src/components/account/AccountCapacityCell.vue b/frontend/src/components/account/AccountCapacityCell.vue index b077264d..f8fe4b47 100644 --- a/frontend/src/components/account/AccountCapacityCell.vue +++ b/frontend/src/components/account/AccountCapacityCell.vue @@ -292,17 +292,19 @@ const rpmTooltip = computed(() => { } }) -// 是否显示各维度配额(仅 apikey 类型) +// 是否显示各维度配额(apikey / bedrock 类型) +const isQuotaEligible = computed(() => props.account.type === 'apikey' || props.account.type === 'bedrock') + const showDailyQuota = computed(() => { - return props.account.type === 'apikey' && (props.account.quota_daily_limit ?? 0) > 0 + return isQuotaEligible.value && (props.account.quota_daily_limit ?? 0) > 0 }) const showWeeklyQuota = computed(() => { - return props.account.type === 'apikey' && (props.account.quota_weekly_limit ?? 0) > 0 + return isQuotaEligible.value && (props.account.quota_weekly_limit ?? 0) > 0 }) const showTotalQuota = computed(() => { - return props.account.type === 'apikey' && (props.account.quota_limit ?? 0) > 0 + return isQuotaEligible.value && (props.account.quota_limit ?? 0) > 0 }) // 格式化费用显示 diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index e83eaead..58473093 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -859,7 +859,7 @@ const makeQuotaBar = ( } const hasApiKeyQuota = computed(() => { - if (props.account.type !== 'apikey') return false + if (props.account.type !== 'apikey' && props.account.type !== 'bedrock') return false return ( (props.account.quota_daily_limit ?? 0) > 0 || (props.account.quota_weekly_limit ?? 0) > 0 || diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 1ac96ed6..9002f0aa 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -323,35 +323,6 @@ - @@ -956,7 +927,7 @@ -
+
+
- - + +
+ + +
-
- + + + + + +
+
-
- - -

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

-
+ +

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

+ +
-
- -
-
- - -
-
- - -

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

-
-
- -

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

-
- - +
- - - -
+
+
+ +

+ {{ t('admin.accounts.poolModeHint') }} +

+
-
- - -
- -

- {{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} - {{ t('admin.accounts.supportsAllModels') }} +

+

+ + {{ t('admin.accounts.poolModeInfo') }}

- - -
-
- - - - -
- - -
- -
+
+ + +

+ {{ + t('admin.accounts.poolModeRetryCountHint', { + default: DEFAULT_POOL_MODE_RETRY_COUNT, + max: MAX_POOL_MODE_RETRY_COUNT + }) + }} +

- -
+ +

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

@@ -3014,7 +2948,7 @@ interface TempUnschedRuleForm { // State const step = ref(1) const submitting = ref(false) -const accountCategory = ref<'oauth-based' | 'apikey' | 'bedrock' | 'bedrock-apikey'>('oauth-based') // UI selection for account category +const accountCategory = ref<'oauth-based' | 'apikey' | 'bedrock'>('oauth-based') // UI selection for account category const addMethod = ref('oauth') // For oauth-based: 'oauth' or 'setup-token' const apiKeyBaseUrl = ref('https://api.anthropic.com') const apiKeyValue = ref('') @@ -3050,16 +2984,13 @@ const antigravityPresetMappings = computed(() => getPresetMappingsByPlatform('an const bedrockPresets = computed(() => getPresetMappingsByPlatform('bedrock')) // Bedrock credentials +const bedrockAuthMode = ref<'sigv4' | 'apikey'>('sigv4') const bedrockAccessKeyId = ref('') const bedrockSecretAccessKey = ref('') const bedrockSessionToken = ref('') const bedrockRegion = ref('us-east-1') const bedrockForceGlobal = ref(false) - -// Bedrock API Key credentials const bedrockApiKeyValue = ref('') -const bedrockApiKeyRegion = ref('us-east-1') -const bedrockApiKeyForceGlobal = ref(false) const tempUnschedEnabled = ref(false) const tempUnschedRules = ref([]) const getModelMappingKey = createStableObjectKeyResolver('create-model-mapping') @@ -3343,7 +3274,8 @@ watch( bedrockSessionToken.value = '' bedrockRegion.value = 'us-east-1' bedrockForceGlobal.value = false - bedrockApiKeyForceGlobal.value = false + bedrockAuthMode.value = 'sigv4' + bedrockApiKeyValue.value = '' // Reset Anthropic/Antigravity-specific settings when switching to other platforms if (newPlatform !== 'anthropic' && newPlatform !== 'antigravity') { interceptWarmupRequests.value = false @@ -3919,27 +3851,34 @@ const handleSubmit = async () => { appStore.showError(t('admin.accounts.pleaseEnterAccountName')) return } - if (!bedrockAccessKeyId.value.trim()) { - appStore.showError(t('admin.accounts.bedrockAccessKeyIdRequired')) - return - } - if (!bedrockSecretAccessKey.value.trim()) { - appStore.showError(t('admin.accounts.bedrockSecretAccessKeyRequired')) - return - } - if (!bedrockRegion.value.trim()) { - appStore.showError(t('admin.accounts.bedrockRegionRequired')) - return - } const credentials: Record = { - aws_access_key_id: bedrockAccessKeyId.value.trim(), - aws_secret_access_key: bedrockSecretAccessKey.value.trim(), - aws_region: bedrockRegion.value.trim(), + auth_mode: bedrockAuthMode.value, + aws_region: bedrockRegion.value.trim() || 'us-east-1', } - if (bedrockSessionToken.value.trim()) { - credentials.aws_session_token = bedrockSessionToken.value.trim() + + if (bedrockAuthMode.value === 'sigv4') { + if (!bedrockAccessKeyId.value.trim()) { + appStore.showError(t('admin.accounts.bedrockAccessKeyIdRequired')) + return + } + if (!bedrockSecretAccessKey.value.trim()) { + appStore.showError(t('admin.accounts.bedrockSecretAccessKeyRequired')) + return + } + credentials.aws_access_key_id = bedrockAccessKeyId.value.trim() + credentials.aws_secret_access_key = bedrockSecretAccessKey.value.trim() + if (bedrockSessionToken.value.trim()) { + credentials.aws_session_token = bedrockSessionToken.value.trim() + } + } else { + if (!bedrockApiKeyValue.value.trim()) { + appStore.showError(t('admin.accounts.bedrockApiKeyRequired')) + return + } + credentials.api_key = bedrockApiKeyValue.value.trim() } + if (bedrockForceGlobal.value) { credentials.aws_force_global = 'true' } @@ -3952,45 +3891,18 @@ const handleSubmit = async () => { credentials.model_mapping = modelMapping } + // Pool mode + if (poolModeEnabled.value) { + credentials.pool_mode = true + credentials.pool_mode_retry_count = normalizePoolModeRetryCount(poolModeRetryCount.value) + } + applyInterceptWarmup(credentials, interceptWarmupRequests.value, 'create') await createAccountAndFinish('anthropic', 'bedrock' as AccountType, credentials) return } - // For Bedrock API Key type, create directly - if (form.platform === 'anthropic' && accountCategory.value === 'bedrock-apikey') { - if (!form.name.trim()) { - appStore.showError(t('admin.accounts.pleaseEnterAccountName')) - return - } - if (!bedrockApiKeyValue.value.trim()) { - appStore.showError(t('admin.accounts.bedrockApiKeyRequired')) - return - } - - const credentials: Record = { - api_key: bedrockApiKeyValue.value.trim(), - aws_region: bedrockApiKeyRegion.value.trim() || 'us-east-1', - } - if (bedrockApiKeyForceGlobal.value) { - credentials.aws_force_global = 'true' - } - - // Model mapping - const modelMapping = buildModelMappingObject( - modelRestrictionMode.value, allowedModels.value, modelMappings.value - ) - if (modelMapping) { - credentials.model_mapping = modelMapping - } - - applyInterceptWarmup(credentials, interceptWarmupRequests.value, 'create') - - await createAccountAndFinish('anthropic', 'bedrock-apikey' as AccountType, credentials) - return - } - // For Antigravity upstream type, create directly if (form.platform === 'antigravity' && antigravityAccountType.value === 'upstream') { if (!form.name.trim()) { @@ -4233,9 +4145,9 @@ const createAccountAndFinish = async ( if (!applyTempUnschedConfig(credentials)) { return } - // Inject quota limits for apikey accounts + // Inject quota limits for apikey/bedrock accounts let finalExtra = extra - if (type === 'apikey') { + if (type === 'apikey' || type === 'bedrock') { const quotaExtra: Record = { ...(extra || {}) } if (editQuotaLimit.value != null && editQuotaLimit.value > 0) { quotaExtra.quota_limit = editQuotaLimit.value diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index b18e9db6..dd496223 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -563,37 +563,54 @@

- +
-
- + + + + +
+ -
-
- - -

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

-
-
- - -

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

+

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

+ +

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

+ +
-
- -
-
- - -

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

-
-
- - -

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

-
-
- -

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

-
- - +
- - - -
+
+
+ +

+ {{ t('admin.accounts.poolModeHint') }} +

+
-
- - -
- -

- {{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} - {{ t('admin.accounts.supportsAllModels') }} +

+

+ + {{ t('admin.accounts.poolModeInfo') }}

- - -
-
- - - - -
- - -
- -
+
+ + +

+ {{ + t('admin.accounts.poolModeRetryCountHint', { + default: DEFAULT_POOL_MODE_RETRY_COUNT, + max: MAX_POOL_MODE_RETRY_COUNT + }) + }} +

@@ -1182,8 +1149,8 @@
- -
+ +

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

@@ -1781,11 +1748,11 @@ const editBedrockSecretAccessKey = ref('') const editBedrockSessionToken = ref('') const editBedrockRegion = ref('') const editBedrockForceGlobal = ref(false) - -// Bedrock API Key credentials const editBedrockApiKeyValue = ref('') -const editBedrockApiKeyRegion = ref('') -const editBedrockApiKeyForceGlobal = ref(false) +const isBedrockAPIKeyMode = computed(() => + props.account?.type === 'bedrock' && + (props.account?.credentials as Record)?.auth_mode === 'apikey' +) const modelMappings = ref([]) const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist') const allowedModels = ref([]) @@ -2026,8 +1993,8 @@ watch( anthropicPassthroughEnabled.value = extra?.anthropic_passthrough === true } - // Load quota limit for apikey accounts - if (newAccount.type === 'apikey') { + // Load quota limit for apikey/bedrock accounts (bedrock quota is also loaded in its own branch above) + if (newAccount.type === 'apikey' || newAccount.type === 'bedrock') { const quotaVal = extra?.quota_limit as number | undefined editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null const dailyVal = extra?.quota_daily_limit as number | undefined @@ -2130,11 +2097,28 @@ watch( } } else if (newAccount.type === 'bedrock' && newAccount.credentials) { const bedrockCreds = newAccount.credentials as Record - editBedrockAccessKeyId.value = (bedrockCreds.aws_access_key_id as string) || '' + const authMode = (bedrockCreds.auth_mode as string) || 'sigv4' editBedrockRegion.value = (bedrockCreds.aws_region as string) || '' editBedrockForceGlobal.value = (bedrockCreds.aws_force_global as string) === 'true' - editBedrockSecretAccessKey.value = '' - editBedrockSessionToken.value = '' + + if (authMode === 'apikey') { + editBedrockApiKeyValue.value = '' + } else { + editBedrockAccessKeyId.value = (bedrockCreds.aws_access_key_id as string) || '' + editBedrockSecretAccessKey.value = '' + editBedrockSessionToken.value = '' + } + + // Load pool mode for bedrock + poolModeEnabled.value = bedrockCreds.pool_mode === true + const retryCount = bedrockCreds.pool_mode_retry_count + poolModeRetryCount.value = (typeof retryCount === 'number' && retryCount >= 0) ? retryCount : DEFAULT_POOL_MODE_RETRY_COUNT + + // Load quota limits for bedrock + const bedrockExtra = (newAccount.extra as Record) || {} + editQuotaLimit.value = typeof bedrockExtra.quota_limit === 'number' ? bedrockExtra.quota_limit : null + editQuotaDailyLimit.value = typeof bedrockExtra.quota_daily_limit === 'number' ? bedrockExtra.quota_daily_limit : null + editQuotaWeeklyLimit.value = typeof bedrockExtra.quota_weekly_limit === 'number' ? bedrockExtra.quota_weekly_limit : null // Load model mappings for bedrock const existingMappings = bedrockCreds.model_mapping as Record | undefined @@ -2155,31 +2139,6 @@ watch( modelMappings.value = [] allowedModels.value = [] } - } else if (newAccount.type === 'bedrock-apikey' && newAccount.credentials) { - const bedrockApiKeyCreds = newAccount.credentials as Record - editBedrockApiKeyRegion.value = (bedrockApiKeyCreds.aws_region as string) || 'us-east-1' - editBedrockApiKeyForceGlobal.value = (bedrockApiKeyCreds.aws_force_global as string) === 'true' - editBedrockApiKeyValue.value = '' - - // Load model mappings for bedrock-apikey - const existingMappings = bedrockApiKeyCreds.model_mapping as Record | undefined - if (existingMappings && typeof existingMappings === 'object') { - const entries = Object.entries(existingMappings) - const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to) - if (isWhitelistMode) { - modelRestrictionMode.value = 'whitelist' - allowedModels.value = entries.map(([from]) => from) - modelMappings.value = [] - } else { - modelRestrictionMode.value = 'mapping' - modelMappings.value = entries.map(([from, to]) => ({ from, to })) - allowedModels.value = [] - } - } else { - modelRestrictionMode.value = 'whitelist' - modelMappings.value = [] - allowedModels.value = [] - } } else if (newAccount.type === 'upstream' && newAccount.credentials) { const credentials = newAccount.credentials as Record editBaseUrl.value = (credentials.base_url as string) || '' @@ -2727,7 +2686,6 @@ const handleSubmit = async () => { const currentCredentials = (props.account.credentials as Record) || {} const newCredentials: Record = { ...currentCredentials } - newCredentials.aws_access_key_id = editBedrockAccessKeyId.value.trim() newCredentials.aws_region = editBedrockRegion.value.trim() if (editBedrockForceGlobal.value) { newCredentials.aws_force_global = 'true' @@ -2735,42 +2693,29 @@ const handleSubmit = async () => { delete newCredentials.aws_force_global } - // Only update secrets if user provided new values - if (editBedrockSecretAccessKey.value.trim()) { - newCredentials.aws_secret_access_key = editBedrockSecretAccessKey.value.trim() - } - if (editBedrockSessionToken.value.trim()) { - newCredentials.aws_session_token = editBedrockSessionToken.value.trim() - } - - // Model mapping - const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value) - if (modelMapping) { - newCredentials.model_mapping = modelMapping + if (isBedrockAPIKeyMode.value) { + // API Key mode: only update api_key if user provided new value + if (editBedrockApiKeyValue.value.trim()) { + newCredentials.api_key = editBedrockApiKeyValue.value.trim() + } } else { - delete newCredentials.model_mapping + // SigV4 mode + newCredentials.aws_access_key_id = editBedrockAccessKeyId.value.trim() + if (editBedrockSecretAccessKey.value.trim()) { + newCredentials.aws_secret_access_key = editBedrockSecretAccessKey.value.trim() + } + if (editBedrockSessionToken.value.trim()) { + newCredentials.aws_session_token = editBedrockSessionToken.value.trim() + } } - applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit') - if (!applyTempUnschedConfig(newCredentials)) { - return - } - - updatePayload.credentials = newCredentials - } else if (props.account.type === 'bedrock-apikey') { - const currentCredentials = (props.account.credentials as Record) || {} - const newCredentials: Record = { ...currentCredentials } - - newCredentials.aws_region = editBedrockApiKeyRegion.value.trim() || 'us-east-1' - if (editBedrockApiKeyForceGlobal.value) { - newCredentials.aws_force_global = 'true' + // Pool mode + if (poolModeEnabled.value) { + newCredentials.pool_mode = true + newCredentials.pool_mode_retry_count = normalizePoolModeRetryCount(poolModeRetryCount.value) } else { - delete newCredentials.aws_force_global - } - - // Only update API key if user provided new value - if (editBedrockApiKeyValue.value.trim()) { - newCredentials.api_key = editBedrockApiKeyValue.value.trim() + delete newCredentials.pool_mode + delete newCredentials.pool_mode_retry_count } // Model mapping @@ -2980,8 +2925,8 @@ const handleSubmit = async () => { updatePayload.extra = newExtra } - // For apikey accounts, handle quota_limit in extra - if (props.account.type === 'apikey') { + // For apikey/bedrock accounts, handle quota_limit in extra + if (props.account.type === 'apikey' || props.account.type === 'bedrock') { const currentExtra = (updatePayload.extra as Record) || (props.account.extra as Record) || {} const newExtra: Record = { ...currentExtra } diff --git a/frontend/src/components/admin/account/AccountActionMenu.vue b/frontend/src/components/admin/account/AccountActionMenu.vue index 29dfb935..f5bc5aa0 100644 --- a/frontend/src/components/admin/account/AccountActionMenu.vue +++ b/frontend/src/components/admin/account/AccountActionMenu.vue @@ -76,7 +76,7 @@ const hasRecoverableState = computed(() => { return props.account?.status === 'error' || Boolean(isRateLimited.value) || Boolean(isOverloaded.value) || Boolean(isTempUnschedulable.value) }) const hasQuotaLimit = computed(() => { - return props.account?.type === 'apikey' && ( + return (props.account?.type === 'apikey' || props.account?.type === 'bedrock') && ( (props.account?.quota_limit ?? 0) > 0 || (props.account?.quota_daily_limit ?? 0) > 0 || (props.account?.quota_weekly_limit ?? 0) > 0 diff --git a/frontend/src/components/common/PlatformTypeBadge.vue b/frontend/src/components/common/PlatformTypeBadge.vue index a6ff490e..5f0bb395 100644 --- a/frontend/src/components/common/PlatformTypeBadge.vue +++ b/frontend/src/components/common/PlatformTypeBadge.vue @@ -83,7 +83,7 @@ const typeLabel = computed(() => { case 'apikey': return 'Key' case 'bedrock': - return 'Bedrock' + return 'AWS' default: return props.type } diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index b47b895c..0ff288bb 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -412,7 +412,7 @@ export function getPresetMappingsByPlatform(platform: string) { if (platform === 'gemini') return geminiPresetMappings if (platform === 'sora') return soraPresetMappings if (platform === 'antigravity') return antigravityPresetMappings - if (platform === 'bedrock' || platform === 'bedrock-apikey') return bedrockPresetMappings + if (platform === 'bedrock') return bedrockPresetMappings return anthropicPresetMappings } diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 1827f96e..b3546b69 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1934,7 +1934,7 @@ export default { claudeCode: 'Claude Code', claudeConsole: 'Claude Console', bedrockLabel: 'AWS Bedrock', - bedrockDesc: 'SigV4 Signing', + bedrockDesc: 'SigV4 / API Key', oauthSetupToken: 'OAuth / Setup Token', addMethod: 'Add Method', setupTokenLongLived: 'Setup Token (Long-lived)', @@ -2136,6 +2136,9 @@ export default { bedrockRegionRequired: 'Please select AWS Region', bedrockSessionTokenHint: 'Optional, for temporary credentials', bedrockSecretKeyLeaveEmpty: 'Leave empty to keep current key', + bedrockAuthMode: 'Authentication Mode', + bedrockAuthModeSigv4: 'SigV4 Signing', + bedrockAuthModeApikey: 'Bedrock API Key', bedrockApiKeyLabel: 'Bedrock API Key', bedrockApiKeyDesc: 'Bearer Token', bedrockApiKeyInput: 'API Key', @@ -4127,6 +4130,7 @@ export default { scopeAll: 'All accounts', scopeOAuth: 'OAuth only', scopeAPIKey: 'API Key only', + scopeBedrock: 'Bedrock only', errorMessage: 'Error message', errorMessagePlaceholder: 'Custom error message when blocked', errorMessageHint: 'Leave empty for default message', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index aef4f1f4..0dcdf727 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2082,7 +2082,7 @@ export default { claudeCode: 'Claude Code', claudeConsole: 'Claude Console', bedrockLabel: 'AWS Bedrock', - bedrockDesc: 'SigV4 签名', + bedrockDesc: 'SigV4 / API Key', oauthSetupToken: 'OAuth / Setup Token', addMethod: '添加方式', setupTokenLongLived: 'Setup Token(长期有效)', @@ -2277,6 +2277,9 @@ export default { bedrockRegionRequired: '请选择 AWS Region', bedrockSessionTokenHint: '可选,用于临时凭证', bedrockSecretKeyLeaveEmpty: '留空以保持当前密钥', + bedrockAuthMode: '认证方式', + bedrockAuthModeSigv4: 'SigV4 签名', + bedrockAuthModeApikey: 'Bedrock API Key', bedrockApiKeyLabel: 'Bedrock API Key', bedrockApiKeyDesc: 'Bearer Token 认证', bedrockApiKeyInput: 'API Key', @@ -4300,6 +4303,7 @@ export default { scopeAll: '全部账号', scopeOAuth: '仅 OAuth 账号', scopeAPIKey: '仅 API Key 账号', + scopeBedrock: '仅 Bedrock 账号', errorMessage: '错误消息', errorMessagePlaceholder: '拦截时返回的自定义错误消息', errorMessageHint: '留空则使用默认错误消息', diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 8b202a90..9f8c75ab 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -531,7 +531,7 @@ export interface UpdateGroupRequest { // ==================== Account & Proxy Types ==================== export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'sora' -export type AccountType = 'oauth' | 'setup-token' | 'apikey' | 'upstream' | 'bedrock' | 'bedrock-apikey' +export type AccountType = 'oauth' | 'setup-token' | 'apikey' | 'upstream' | 'bedrock' export type OAuthAddMethod = 'oauth' | 'setup-token' export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h' diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index c2056ccb..4ac5d5c3 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -1745,7 +1745,7 @@ const betaPolicyForm = reactive({ rules: [] as Array<{ beta_token: string action: 'pass' | 'filter' | 'block' - scope: 'all' | 'oauth' | 'apikey' + scope: 'all' | 'oauth' | 'apikey' | 'bedrock' error_message?: string }> }) @@ -2297,7 +2297,8 @@ const betaPolicyActionOptions = computed(() => [ const betaPolicyScopeOptions = computed(() => [ { value: 'all', label: t('admin.settings.betaPolicy.scopeAll') }, { value: 'oauth', label: t('admin.settings.betaPolicy.scopeOAuth') }, - { value: 'apikey', label: t('admin.settings.betaPolicy.scopeAPIKey') } + { value: 'apikey', label: t('admin.settings.betaPolicy.scopeAPIKey') }, + { value: 'bedrock', label: t('admin.settings.betaPolicy.scopeBedrock') } ]) // Beta Policy 方法