diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go
index 36d043b5..c51046a2 100644
--- a/backend/internal/domain/constants.go
+++ b/backend/internal/domain/constants.go
@@ -27,12 +27,11 @@ const (
// Account type constants
const (
- AccountTypeOAuth = "oauth" // OAuth类型账号(full scope: profile + inference)
- 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)
+ AccountTypeOAuth = "oauth" // OAuth类型账号(full scope: profile + inference)
+ AccountTypeSetupToken = "setup-token" // Setup Token类型账号(inference only scope)
+ AccountTypeAPIKey = "apikey" // API Key类型账号
+ AccountTypeUpstream = "upstream" // 上游透传类型账号(通过 Base URL + API Key 连接上游)
+ 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..61dc7643 100644
--- a/backend/internal/service/domain_constants.go
+++ b/backend/internal/service/domain_constants.go
@@ -29,12 +29,11 @@ const (
// Account type constants
const (
- AccountTypeOAuth = domain.AccountTypeOAuth // OAuth类型账号(full scope: profile + inference)
- 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)
+ AccountTypeOAuth = domain.AccountTypeOAuth // OAuth类型账号(full scope: profile + inference)
+ 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 签名或 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 883af59a..ee13beb7 100644
--- a/frontend/src/components/account/AccountUsageCell.vue
+++ b/frontend/src/components/account/AccountUsageCell.vue
@@ -953,7 +953,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.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.bedrockSecretKeyLeaveEmpty') }}
-
-
-
-
-
{{ t('admin.accounts.bedrockSessionTokenHint') }}
+
{{ t('admin.accounts.bedrockApiKeyLeaveEmpty') }}
+
+
+
+
-
-
-
-
-
-
-
{{ 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 5625bbba..41af9987 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',
@@ -4141,6 +4144,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 39dcc718..4675cffc 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -2091,7 +2091,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(长期有效)',
@@ -2286,6 +2286,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',
@@ -4314,6 +4317,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 500bb715..3064c7c3 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 方法