Commit 4644af2c authored by SsageParuders's avatar SsageParuders
Browse files

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
parent 2e3e8687
...@@ -31,8 +31,7 @@ const ( ...@@ -31,8 +31,7 @@ const (
AccountTypeSetupToken = "setup-token" // Setup Token类型账号(inference only scope) AccountTypeSetupToken = "setup-token" // Setup Token类型账号(inference only scope)
AccountTypeAPIKey = "apikey" // API Key类型账号 AccountTypeAPIKey = "apikey" // API Key类型账号
AccountTypeUpstream = "upstream" // 上游透传类型账号(通过 Base URL + API Key 连接上游) AccountTypeUpstream = "upstream" // 上游透传类型账号(通过 Base URL + API Key 连接上游)
AccountTypeBedrock = "bedrock" // AWS Bedrock 类型账号(通过 SigV4 签名连接 Bedrock) AccountTypeBedrock = "bedrock" // AWS Bedrock 类型账号(通过 SigV4 签名或 API Key 连接 Bedrock,由 credentials.auth_mode 区分)
AccountTypeBedrockAPIKey = "bedrock-apikey" // AWS Bedrock API Key 类型账号(通过 Bearer Token 连接 Bedrock)
) )
// Redeem type constants // Redeem type constants
......
...@@ -97,7 +97,7 @@ type CreateAccountRequest struct { ...@@ -97,7 +97,7 @@ type CreateAccountRequest struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
Notes *string `json:"notes"` Notes *string `json:"notes"`
Platform string `json:"platform" binding:"required"` 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"` Credentials map[string]any `json:"credentials" binding:"required"`
Extra map[string]any `json:"extra"` Extra map[string]any `json:"extra"`
ProxyID *int64 `json:"proxy_id"` ProxyID *int64 `json:"proxy_id"`
...@@ -116,7 +116,7 @@ type CreateAccountRequest struct { ...@@ -116,7 +116,7 @@ type CreateAccountRequest struct {
type UpdateAccountRequest struct { type UpdateAccountRequest struct {
Name string `json:"name"` Name string `json:"name"`
Notes *string `json:"notes"` 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"` Credentials map[string]any `json:"credentials"`
Extra map[string]any `json:"extra"` Extra map[string]any `json:"extra"`
ProxyID *int64 `json:"proxy_id"` ProxyID *int64 `json:"proxy_id"`
......
...@@ -264,8 +264,8 @@ func AccountFromServiceShallow(a *service.Account) *Account { ...@@ -264,8 +264,8 @@ func AccountFromServiceShallow(a *service.Account) *Account {
} }
} }
// 提取 API Key 账号配额限制(apikey 类型有效) // 提取账号配额限制(apikey / bedrock 类型有效)
if a.Type == service.AccountTypeAPIKey { if a.IsAPIKeyOrBedrock() {
if limit := a.GetQuotaLimit(); limit > 0 { if limit := a.GetQuotaLimit(); limit > 0 {
out.QuotaLimit = &limit out.QuotaLimit = &limit
used := a.GetQuotaUsed() used := a.GetQuotaUsed()
......
...@@ -656,7 +656,7 @@ func (a *Account) IsCustomErrorCodesEnabled() bool { ...@@ -656,7 +656,7 @@ func (a *Account) IsCustomErrorCodesEnabled() bool {
// IsPoolMode 检查 API Key 账号是否启用池模式。 // IsPoolMode 检查 API Key 账号是否启用池模式。
// 池模式下,上游错误不标记本地账号状态,而是在同一账号上重试。 // 池模式下,上游错误不标记本地账号状态,而是在同一账号上重试。
func (a *Account) IsPoolMode() bool { func (a *Account) IsPoolMode() bool {
if a.Type != AccountTypeAPIKey || a.Credentials == nil { if !a.IsAPIKeyOrBedrock() || a.Credentials == nil {
return false return false
} }
if v, ok := a.Credentials["pool_mode"]; ok { if v, ok := a.Credentials["pool_mode"]; ok {
...@@ -771,11 +771,16 @@ func (a *Account) IsInterceptWarmupEnabled() bool { ...@@ -771,11 +771,16 @@ func (a *Account) IsInterceptWarmupEnabled() bool {
} }
func (a *Account) IsBedrock() 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 { 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 { func (a *Account) IsOpenAI() bool {
......
...@@ -33,8 +33,7 @@ const ( ...@@ -33,8 +33,7 @@ const (
AccountTypeSetupToken = domain.AccountTypeSetupToken // Setup Token类型账号(inference only scope) AccountTypeSetupToken = domain.AccountTypeSetupToken // Setup Token类型账号(inference only scope)
AccountTypeAPIKey = domain.AccountTypeAPIKey // API Key类型账号 AccountTypeAPIKey = domain.AccountTypeAPIKey // API Key类型账号
AccountTypeUpstream = domain.AccountTypeUpstream // 上游透传类型账号(通过 Base URL + API Key 连接上游) AccountTypeUpstream = domain.AccountTypeUpstream // 上游透传类型账号(通过 Base URL + API Key 连接上游)
AccountTypeBedrock = domain.AccountTypeBedrock // AWS Bedrock 类型账号(通过 SigV4 签名连接 Bedrock) AccountTypeBedrock = domain.AccountTypeBedrock // AWS Bedrock 类型账号(通过 SigV4 签名或 API Key 连接 Bedrock,由 credentials.auth_mode 区分)
AccountTypeBedrockAPIKey = domain.AccountTypeBedrockAPIKey // AWS Bedrock API Key 类型账号(通过 Bearer Token 连接 Bedrock)
) )
// Redeem type constants // Redeem type constants
......
...@@ -2173,10 +2173,10 @@ func (s *GatewayService) withWindowCostPrefetch(ctx context.Context, accounts [] ...@@ -2173,10 +2173,10 @@ func (s *GatewayService) withWindowCostPrefetch(ctx context.Context, accounts []
return context.WithValue(ctx, windowCostPrefetchContextKey, costs) return context.WithValue(ctx, windowCostPrefetchContextKey, costs)
} }
// isAccountSchedulableForQuota 检查 API Key 账号是否在配额限制内 // isAccountSchedulableForQuota 检查账号是否在配额限制内
// 适用于配置了 quota_limit 的 apikey 类型账号 // 适用于配置了 quota_limit 的 apikey 和 bedrock 类型账号
func (s *GatewayService) isAccountSchedulableForQuota(account *Account) bool { func (s *GatewayService) isAccountSchedulableForQuota(account *Account) bool {
if account.Type != AccountTypeAPIKey { if !account.IsAPIKeyOrBedrock() {
return true return true
} }
return !account.IsQuotaExceeded() return !account.IsQuotaExceeded()
...@@ -3532,9 +3532,7 @@ func (s *GatewayService) GetAccessToken(ctx context.Context, account *Account) ( ...@@ -3532,9 +3532,7 @@ func (s *GatewayService) GetAccessToken(ctx context.Context, account *Account) (
} }
return apiKey, "apikey", nil return apiKey, "apikey", nil
case AccountTypeBedrock: case AccountTypeBedrock:
return "", "bedrock", nil // Bedrock 使用 SigV4 签名,不需要 token return "", "bedrock", nil // Bedrock 使用 SigV4 签名或 API Key,由 forwardBedrock 处理
case AccountTypeBedrockAPIKey:
return "", "bedrock-apikey", nil // Bedrock API Key 使用 Bearer Token,由 forwardBedrock 处理
default: default:
return "", "", fmt.Errorf("unsupported account type: %s", account.Type) return "", "", fmt.Errorf("unsupported account type: %s", account.Type)
} }
...@@ -5186,7 +5184,7 @@ func (s *GatewayService) forwardBedrock( ...@@ -5186,7 +5184,7 @@ func (s *GatewayService) forwardBedrock(
if account.IsBedrockAPIKey() { if account.IsBedrockAPIKey() {
bedrockAPIKey = account.GetCredential("api_key") bedrockAPIKey = account.GetCredential("api_key")
if bedrockAPIKey == "" { 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 { } else {
signer, err = NewBedrockSignerFromAccount(account) signer, err = NewBedrockSignerFromAccount(account)
...@@ -5375,8 +5373,9 @@ func (s *GatewayService) handleBedrockUpstreamErrors( ...@@ -5375,8 +5373,9 @@ func (s *GatewayService) handleBedrockUpstreamErrors(
Message: extractUpstreamErrorMessage(respBody), Message: extractUpstreamErrorMessage(respBody),
}) })
return nil, &UpstreamFailoverError{ return nil, &UpstreamFailoverError{
StatusCode: resp.StatusCode, StatusCode: resp.StatusCode,
ResponseBody: respBody, ResponseBody: respBody,
RetryableOnSameAccount: account.IsPoolMode() && isPoolModeRetryableStatus(resp.StatusCode),
} }
} }
return s.handleRetryExhaustedError(ctx, resp, c, account) return s.handleRetryExhaustedError(ctx, resp, c, account)
...@@ -5398,8 +5397,9 @@ func (s *GatewayService) handleBedrockUpstreamErrors( ...@@ -5398,8 +5397,9 @@ func (s *GatewayService) handleBedrockUpstreamErrors(
Message: extractUpstreamErrorMessage(respBody), Message: extractUpstreamErrorMessage(respBody),
}) })
return nil, &UpstreamFailoverError{ return nil, &UpstreamFailoverError{
StatusCode: resp.StatusCode, StatusCode: resp.StatusCode,
ResponseBody: respBody, ResponseBody: respBody,
RetryableOnSameAccount: account.IsPoolMode() && isPoolModeRetryableStatus(resp.StatusCode),
} }
} }
...@@ -5808,9 +5808,10 @@ func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader stri ...@@ -5808,9 +5808,10 @@ func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader stri
return betaPolicyResult{} return betaPolicyResult{}
} }
isOAuth := account.IsOAuth() isOAuth := account.IsOAuth()
isBedrock := account.IsBedrock()
var result betaPolicyResult var result betaPolicyResult
for _, rule := range settings.Rules { for _, rule := range settings.Rules {
if !betaPolicyScopeMatches(rule.Scope, isOAuth) { if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) {
continue continue
} }
switch rule.Action { switch rule.Action {
...@@ -5870,14 +5871,16 @@ func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Cont ...@@ -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. // 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 { switch scope {
case BetaPolicyScopeAll: case BetaPolicyScopeAll:
return true return true
case BetaPolicyScopeOAuth: case BetaPolicyScopeOAuth:
return isOAuth return isOAuth
case BetaPolicyScopeAPIKey: case BetaPolicyScopeAPIKey:
return !isOAuth return !isOAuth && !isBedrock
case BetaPolicyScopeBedrock:
return isBedrock
default: default:
return true // unknown scope → match all (fail-open) return true // unknown scope → match all (fail-open)
} }
...@@ -5959,12 +5962,13 @@ func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, toke ...@@ -5959,12 +5962,13 @@ func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, toke
return nil return nil
} }
isOAuth := account.IsOAuth() isOAuth := account.IsOAuth()
isBedrock := account.IsBedrock()
tokenSet := buildBetaTokenSet(tokens) tokenSet := buildBetaTokenSet(tokens)
for _, rule := range settings.Rules { for _, rule := range settings.Rules {
if rule.Action != BetaPolicyActionBlock { if rule.Action != BetaPolicyActionBlock {
continue continue
} }
if !betaPolicyScopeMatches(rule.Scope, isOAuth) { if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) {
continue continue
} }
if _, present := tokenSet[rule.BetaToken]; present { if _, present := tokenSet[rule.BetaToken]; present {
...@@ -7176,7 +7180,7 @@ func postUsageBilling(ctx context.Context, p *postUsageBillingParams, deps *bill ...@@ -7176,7 +7180,7 @@ func postUsageBilling(ctx context.Context, p *postUsageBillingParams, deps *bill
} }
// 4. 账号配额用量(账号口径:TotalCost × 账号计费倍率) // 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 accountCost := cost.TotalCost * p.AccountRateMultiplier
if err := deps.accountRepo.IncrementQuotaUsed(billingCtx, p.Account.ID, accountCost); err != nil { 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) 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 ...@@ -7264,7 +7268,7 @@ func buildUsageBillingCommand(requestID string, usageLog *UsageLog, p *postUsage
if p.Cost.ActualCost > 0 && p.APIKey.HasRateLimits() && p.APIKeyService != nil { if p.Cost.ActualCost > 0 && p.APIKey.HasRateLimits() && p.APIKeyService != nil {
cmd.APIKeyRateLimitCost = p.Cost.ActualCost 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 cmd.AccountQuotaCost = p.Cost.TotalCost * p.AccountRateMultiplier
} }
......
...@@ -1278,7 +1278,7 @@ func (s *SettingService) SetBetaPolicySettings(ctx context.Context, settings *Be ...@@ -1278,7 +1278,7 @@ func (s *SettingService) SetBetaPolicySettings(ctx context.Context, settings *Be
BetaPolicyActionPass: true, BetaPolicyActionFilter: true, BetaPolicyActionBlock: true, BetaPolicyActionPass: true, BetaPolicyActionFilter: true, BetaPolicyActionBlock: true,
} }
validScopes := map[string]bool{ validScopes := map[string]bool{
BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true, BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true, BetaPolicyScopeBedrock: true,
} }
for i, rule := range settings.Rules { for i, rule := range settings.Rules {
......
...@@ -198,16 +198,17 @@ const ( ...@@ -198,16 +198,17 @@ const (
BetaPolicyActionFilter = "filter" // 过滤,从 beta header 中移除该 token BetaPolicyActionFilter = "filter" // 过滤,从 beta header 中移除该 token
BetaPolicyActionBlock = "block" // 拦截,直接返回错误 BetaPolicyActionBlock = "block" // 拦截,直接返回错误
BetaPolicyScopeAll = "all" // 所有账号类型 BetaPolicyScopeAll = "all" // 所有账号类型
BetaPolicyScopeOAuth = "oauth" // 仅 OAuth 账号 BetaPolicyScopeOAuth = "oauth" // 仅 OAuth 账号
BetaPolicyScopeAPIKey = "apikey" // 仅 API Key 账号 BetaPolicyScopeAPIKey = "apikey" // 仅 API Key 账号
BetaPolicyScopeBedrock = "bedrock" // 仅 AWS Bedrock 账号
) )
// BetaPolicyRule 单条 Beta 策略规则 // BetaPolicyRule 单条 Beta 策略规则
type BetaPolicyRule struct { type BetaPolicyRule struct {
BetaToken string `json:"beta_token"` // beta token 值 BetaToken string `json:"beta_token"` // beta token 值
Action string `json:"action"` // "pass" | "filter" | "block" 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 时生效) ErrorMessage string `json:"error_message,omitempty"` // 自定义错误消息 (action=block 时生效)
} }
......
...@@ -316,7 +316,7 @@ export async function updateRectifierSettings( ...@@ -316,7 +316,7 @@ export async function updateRectifierSettings(
export interface BetaPolicyRule { export interface BetaPolicyRule {
beta_token: string beta_token: string
action: 'pass' | 'filter' | 'block' action: 'pass' | 'filter' | 'block'
scope: 'all' | 'oauth' | 'apikey' scope: 'all' | 'oauth' | 'apikey' | 'bedrock'
error_message?: string error_message?: string
} }
......
...@@ -292,17 +292,19 @@ const rpmTooltip = computed(() => { ...@@ -292,17 +292,19 @@ const rpmTooltip = computed(() => {
} }
}) })
// 是否显示各维度配额(仅 apikey 类型) // 是否显示各维度配额(apikey / bedrock 类型)
const isQuotaEligible = computed(() => props.account.type === 'apikey' || props.account.type === 'bedrock')
const showDailyQuota = computed(() => { 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(() => { 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(() => { const showTotalQuota = computed(() => {
return props.account.type === 'apikey' && (props.account.quota_limit ?? 0) > 0 return isQuotaEligible.value && (props.account.quota_limit ?? 0) > 0
}) })
// 格式化费用显示 // 格式化费用显示
......
...@@ -859,7 +859,7 @@ const makeQuotaBar = ( ...@@ -859,7 +859,7 @@ const makeQuotaBar = (
} }
const hasApiKeyQuota = computed(() => { const hasApiKeyQuota = computed(() => {
if (props.account.type !== 'apikey') return false if (props.account.type !== 'apikey' && props.account.type !== 'bedrock') return false
return ( return (
(props.account.quota_daily_limit ?? 0) > 0 || (props.account.quota_daily_limit ?? 0) > 0 ||
(props.account.quota_weekly_limit ?? 0) > 0 || (props.account.quota_weekly_limit ?? 0) > 0 ||
......
...@@ -563,37 +563,54 @@ ...@@ -563,37 +563,54 @@
</div> </div>
</div> </div>
<!-- Bedrock fields (only for bedrock type) --> <!-- Bedrock fields (for bedrock type, both SigV4 and API Key modes) -->
<div v-if="account.type === 'bedrock'" class="space-y-4"> <div v-if="account.type === 'bedrock'" class="space-y-4">
<div> <!-- SigV4 fields -->
<label class="input-label">{{ t('admin.accounts.bedrockAccessKeyId') }}</label> <template v-if="!isBedrockAPIKeyMode">
<input <div>
v-model="editBedrockAccessKeyId" <label class="input-label">{{ t('admin.accounts.bedrockAccessKeyId') }}</label>
type="text" <input
class="input font-mono" v-model="editBedrockAccessKeyId"
placeholder="AKIA..." type="text"
/> class="input font-mono"
</div> placeholder="AKIA..."
<div> />
<label class="input-label">{{ t('admin.accounts.bedrockSecretAccessKey') }}</label> </div>
<input <div>
v-model="editBedrockSecretAccessKey" <label class="input-label">{{ t('admin.accounts.bedrockSecretAccessKey') }}</label>
type="password" <input
class="input font-mono" v-model="editBedrockSecretAccessKey"
:placeholder="t('admin.accounts.bedrockSecretKeyLeaveEmpty')" type="password"
/> class="input font-mono"
<p class="input-hint">{{ t('admin.accounts.bedrockSecretKeyLeaveEmpty') }}</p> :placeholder="t('admin.accounts.bedrockSecretKeyLeaveEmpty')"
</div> />
<div> <p class="input-hint">{{ t('admin.accounts.bedrockSecretKeyLeaveEmpty') }}</p>
<label class="input-label">{{ t('admin.accounts.bedrockSessionToken') }}</label> </div>
<div>
<label class="input-label">{{ t('admin.accounts.bedrockSessionToken') }}</label>
<input
v-model="editBedrockSessionToken"
type="password"
class="input font-mono"
:placeholder="t('admin.accounts.bedrockSecretKeyLeaveEmpty')"
/>
<p class="input-hint">{{ t('admin.accounts.bedrockSessionTokenHint') }}</p>
</div>
</template>
<!-- API Key field -->
<div v-if="isBedrockAPIKeyMode">
<label class="input-label">{{ t('admin.accounts.bedrockApiKeyInput') }}</label>
<input <input
v-model="editBedrockSessionToken" v-model="editBedrockApiKeyValue"
type="password" type="password"
class="input font-mono" class="input font-mono"
:placeholder="t('admin.accounts.bedrockSecretKeyLeaveEmpty')" :placeholder="t('admin.accounts.bedrockApiKeyLeaveEmpty')"
/> />
<p class="input-hint">{{ t('admin.accounts.bedrockSessionTokenHint') }}</p> <p class="input-hint">{{ t('admin.accounts.bedrockApiKeyLeaveEmpty') }}</p>
</div> </div>
<!-- Shared: Region -->
<div> <div>
<label class="input-label">{{ t('admin.accounts.bedrockRegion') }}</label> <label class="input-label">{{ t('admin.accounts.bedrockRegion') }}</label>
<input <input
...@@ -604,6 +621,8 @@ ...@@ -604,6 +621,8 @@
/> />
<p class="input-hint">{{ t('admin.accounts.bedrockRegionHint') }}</p> <p class="input-hint">{{ t('admin.accounts.bedrockRegionHint') }}</p>
</div> </div>
<!-- Shared: Force Global -->
<div> <div>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input <input
...@@ -684,108 +703,56 @@ ...@@ -684,108 +703,56 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Bedrock API Key fields (only for bedrock-apikey type) -->
<div v-if="account.type === 'bedrock-apikey'" class="space-y-4">
<div>
<label class="input-label">{{ t('admin.accounts.bedrockApiKeyInput') }}</label>
<input
v-model="editBedrockApiKeyValue"
type="password"
class="input font-mono"
:placeholder="t('admin.accounts.bedrockApiKeyLeaveEmpty')"
/>
<p class="input-hint">{{ t('admin.accounts.bedrockApiKeyLeaveEmpty') }}</p>
</div>
<div>
<label class="input-label">{{ t('admin.accounts.bedrockRegion') }}</label>
<input
v-model="editBedrockApiKeyRegion"
type="text"
class="input"
placeholder="us-east-1"
/>
<p class="input-hint">{{ t('admin.accounts.bedrockRegionHint') }}</p>
</div>
<div>
<label class="flex items-center gap-2 cursor-pointer">
<input
v-model="editBedrockApiKeyForceGlobal"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-500"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.accounts.bedrockForceGlobal') }}</span>
</label>
<p class="input-hint mt-1">{{ t('admin.accounts.bedrockForceGlobalHint') }}</p>
</div>
<!-- Model Restriction for Bedrock API Key --> <!-- Pool Mode Section for Bedrock -->
<div class="border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label> <div class="mb-3 flex items-center justify-between">
<div>
<!-- Mode Toggle --> <label class="input-label mb-0">{{ t('admin.accounts.poolMode') }}</label>
<div class="mb-4 flex gap-2"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
<button {{ t('admin.accounts.poolModeHint') }}
type="button" </p>
@click="modelRestrictionMode = 'whitelist'" </div>
:class="[
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
modelRestrictionMode === 'whitelist'
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
]"
>
{{ t('admin.accounts.modelWhitelist') }}
</button>
<button <button
type="button" type="button"
@click="modelRestrictionMode = 'mapping'" @click="poolModeEnabled = !poolModeEnabled"
:class="[ :class="[
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all', 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
modelRestrictionMode === 'mapping' poolModeEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
]" ]"
> >
{{ t('admin.accounts.modelMapping') }} <span
:class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
poolModeEnabled ? 'translate-x-5' : 'translate-x-0'
]"
/>
</button> </button>
</div> </div>
<div v-if="poolModeEnabled" class="rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
<!-- Whitelist Mode --> <p class="text-xs text-blue-700 dark:text-blue-400">
<div v-if="modelRestrictionMode === 'whitelist'"> <Icon name="exclamationCircle" size="sm" class="mr-1 inline" :stroke-width="2" />
<ModelWhitelistSelector v-model="allowedModels" platform="anthropic" /> {{ t('admin.accounts.poolModeInfo') }}
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{ t('admin.accounts.supportsAllModels') }}</span>
</p> </p>
</div> </div>
<div v-if="poolModeEnabled" class="mt-3">
<!-- Mapping Mode --> <label class="input-label">{{ t('admin.accounts.poolModeRetryCount') }}</label>
<div v-else class="space-y-3"> <input
<div v-for="(mapping, index) in modelMappings" :key="getModelMappingKey(mapping)" class="flex items-center gap-2"> v-model.number="poolModeRetryCount"
<input v-model="mapping.from" type="text" class="input flex-1" :placeholder="t('admin.accounts.fromModel')" /> type="number"
<span class="text-gray-400"></span> min="0"
<input v-model="mapping.to" type="text" class="input flex-1" :placeholder="t('admin.accounts.toModel')" /> :max="MAX_POOL_MODE_RETRY_COUNT"
<button type="button" @click="modelMappings.splice(index, 1)" class="text-red-500 hover:text-red-700"> step="1"
<Icon name="trash" size="sm" /> class="input"
</button> />
</div> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
<button type="button" @click="modelMappings.push({ from: '', to: '' })" class="btn btn-secondary text-sm"> {{
+ {{ t('admin.accounts.addMapping') }} t('admin.accounts.poolModeRetryCountHint', {
</button> default: DEFAULT_POOL_MODE_RETRY_COUNT,
<!-- Bedrock Preset Mappings --> max: MAX_POOL_MODE_RETRY_COUNT
<div class="flex flex-wrap gap-2"> })
<button }}
v-for="preset in bedrockPresets" </p>
:key="preset.from"
type="button"
@click="modelMappings.push({ from: preset.from, to: preset.to })"
:class="['rounded-lg px-3 py-1 text-xs transition-colors', preset.color]"
>
+ {{ preset.label }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -1182,8 +1149,8 @@ ...@@ -1182,8 +1149,8 @@
</div> </div>
</div> </div>
<!-- API Key 账号配额限制 --> <!-- API Key / Bedrock 账号配额限制 -->
<div v-if="account?.type === 'apikey'" class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"> <div v-if="account?.type === 'apikey' || account?.type === 'bedrock'" class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
<div class="mb-3"> <div class="mb-3">
<h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaLimit') }}</h3> <h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaLimit') }}</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
...@@ -1781,11 +1748,11 @@ const editBedrockSecretAccessKey = ref('') ...@@ -1781,11 +1748,11 @@ const editBedrockSecretAccessKey = ref('')
const editBedrockSessionToken = ref('') const editBedrockSessionToken = ref('')
const editBedrockRegion = ref('') const editBedrockRegion = ref('')
const editBedrockForceGlobal = ref(false) const editBedrockForceGlobal = ref(false)
// Bedrock API Key credentials
const editBedrockApiKeyValue = ref('') const editBedrockApiKeyValue = ref('')
const editBedrockApiKeyRegion = ref('') const isBedrockAPIKeyMode = computed(() =>
const editBedrockApiKeyForceGlobal = ref(false) props.account?.type === 'bedrock' &&
(props.account?.credentials as Record<string, unknown>)?.auth_mode === 'apikey'
)
const modelMappings = ref<ModelMapping[]>([]) const modelMappings = ref<ModelMapping[]>([])
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist') const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
const allowedModels = ref<string[]>([]) const allowedModels = ref<string[]>([])
...@@ -2026,8 +1993,8 @@ watch( ...@@ -2026,8 +1993,8 @@ watch(
anthropicPassthroughEnabled.value = extra?.anthropic_passthrough === true anthropicPassthroughEnabled.value = extra?.anthropic_passthrough === true
} }
// Load quota limit for apikey accounts // Load quota limit for apikey/bedrock accounts (bedrock quota is also loaded in its own branch above)
if (newAccount.type === 'apikey') { if (newAccount.type === 'apikey' || newAccount.type === 'bedrock') {
const quotaVal = extra?.quota_limit as number | undefined const quotaVal = extra?.quota_limit as number | undefined
editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null
const dailyVal = extra?.quota_daily_limit as number | undefined const dailyVal = extra?.quota_daily_limit as number | undefined
...@@ -2130,39 +2097,31 @@ watch( ...@@ -2130,39 +2097,31 @@ watch(
} }
} else if (newAccount.type === 'bedrock' && newAccount.credentials) { } else if (newAccount.type === 'bedrock' && newAccount.credentials) {
const bedrockCreds = newAccount.credentials as Record<string, unknown> const bedrockCreds = newAccount.credentials as Record<string, unknown>
editBedrockAccessKeyId.value = (bedrockCreds.aws_access_key_id as string) || '' const authMode = (bedrockCreds.auth_mode as string) || 'sigv4'
editBedrockRegion.value = (bedrockCreds.aws_region as string) || '' editBedrockRegion.value = (bedrockCreds.aws_region as string) || ''
editBedrockForceGlobal.value = (bedrockCreds.aws_force_global as string) === 'true' editBedrockForceGlobal.value = (bedrockCreds.aws_force_global as string) === 'true'
editBedrockSecretAccessKey.value = ''
editBedrockSessionToken.value = ''
// Load model mappings for bedrock if (authMode === 'apikey') {
const existingMappings = bedrockCreds.model_mapping as Record<string, string> | undefined editBedrockApiKeyValue.value = ''
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 { } else {
modelRestrictionMode.value = 'whitelist' editBedrockAccessKeyId.value = (bedrockCreds.aws_access_key_id as string) || ''
modelMappings.value = [] editBedrockSecretAccessKey.value = ''
allowedModels.value = [] editBedrockSessionToken.value = ''
} }
} else if (newAccount.type === 'bedrock-apikey' && newAccount.credentials) {
const bedrockApiKeyCreds = newAccount.credentials as Record<string, unknown> // Load pool mode for bedrock
editBedrockApiKeyRegion.value = (bedrockApiKeyCreds.aws_region as string) || 'us-east-1' poolModeEnabled.value = bedrockCreds.pool_mode === true
editBedrockApiKeyForceGlobal.value = (bedrockApiKeyCreds.aws_force_global as string) === 'true' const retryCount = bedrockCreds.pool_mode_retry_count
editBedrockApiKeyValue.value = '' poolModeRetryCount.value = (typeof retryCount === 'number' && retryCount >= 0) ? retryCount : DEFAULT_POOL_MODE_RETRY_COUNT
// Load model mappings for bedrock-apikey // Load quota limits for bedrock
const existingMappings = bedrockApiKeyCreds.model_mapping as Record<string, string> | undefined const bedrockExtra = (newAccount.extra as Record<string, unknown>) || {}
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<string, string> | undefined
if (existingMappings && typeof existingMappings === 'object') { if (existingMappings && typeof existingMappings === 'object') {
const entries = Object.entries(existingMappings) const entries = Object.entries(existingMappings)
const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to) const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to)
...@@ -2727,7 +2686,6 @@ const handleSubmit = async () => { ...@@ -2727,7 +2686,6 @@ const handleSubmit = async () => {
const currentCredentials = (props.account.credentials as Record<string, unknown>) || {} const currentCredentials = (props.account.credentials as Record<string, unknown>) || {}
const newCredentials: Record<string, unknown> = { ...currentCredentials } const newCredentials: Record<string, unknown> = { ...currentCredentials }
newCredentials.aws_access_key_id = editBedrockAccessKeyId.value.trim()
newCredentials.aws_region = editBedrockRegion.value.trim() newCredentials.aws_region = editBedrockRegion.value.trim()
if (editBedrockForceGlobal.value) { if (editBedrockForceGlobal.value) {
newCredentials.aws_force_global = 'true' newCredentials.aws_force_global = 'true'
...@@ -2735,42 +2693,29 @@ const handleSubmit = async () => { ...@@ -2735,42 +2693,29 @@ const handleSubmit = async () => {
delete newCredentials.aws_force_global delete newCredentials.aws_force_global
} }
// Only update secrets if user provided new values if (isBedrockAPIKeyMode.value) {
if (editBedrockSecretAccessKey.value.trim()) { // API Key mode: only update api_key if user provided new value
newCredentials.aws_secret_access_key = editBedrockSecretAccessKey.value.trim() if (editBedrockApiKeyValue.value.trim()) {
} newCredentials.api_key = editBedrockApiKeyValue.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
} else { } else {
delete newCredentials.model_mapping // SigV4 mode
} newCredentials.aws_access_key_id = editBedrockAccessKeyId.value.trim()
if (editBedrockSecretAccessKey.value.trim()) {
applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit') newCredentials.aws_secret_access_key = editBedrockSecretAccessKey.value.trim()
if (!applyTempUnschedConfig(newCredentials)) { }
return if (editBedrockSessionToken.value.trim()) {
newCredentials.aws_session_token = editBedrockSessionToken.value.trim()
}
} }
updatePayload.credentials = newCredentials // Pool mode
} else if (props.account.type === 'bedrock-apikey') { if (poolModeEnabled.value) {
const currentCredentials = (props.account.credentials as Record<string, unknown>) || {} newCredentials.pool_mode = true
const newCredentials: Record<string, unknown> = { ...currentCredentials } newCredentials.pool_mode_retry_count = normalizePoolModeRetryCount(poolModeRetryCount.value)
newCredentials.aws_region = editBedrockApiKeyRegion.value.trim() || 'us-east-1'
if (editBedrockApiKeyForceGlobal.value) {
newCredentials.aws_force_global = 'true'
} else { } else {
delete newCredentials.aws_force_global delete newCredentials.pool_mode
} delete newCredentials.pool_mode_retry_count
// Only update API key if user provided new value
if (editBedrockApiKeyValue.value.trim()) {
newCredentials.api_key = editBedrockApiKeyValue.value.trim()
} }
// Model mapping // Model mapping
...@@ -2980,8 +2925,8 @@ const handleSubmit = async () => { ...@@ -2980,8 +2925,8 @@ const handleSubmit = async () => {
updatePayload.extra = newExtra updatePayload.extra = newExtra
} }
// For apikey accounts, handle quota_limit in extra // For apikey/bedrock accounts, handle quota_limit in extra
if (props.account.type === 'apikey') { if (props.account.type === 'apikey' || props.account.type === 'bedrock') {
const currentExtra = (updatePayload.extra as Record<string, unknown>) || const currentExtra = (updatePayload.extra as Record<string, unknown>) ||
(props.account.extra as Record<string, unknown>) || {} (props.account.extra as Record<string, unknown>) || {}
const newExtra: Record<string, unknown> = { ...currentExtra } const newExtra: Record<string, unknown> = { ...currentExtra }
......
...@@ -76,7 +76,7 @@ const hasRecoverableState = computed(() => { ...@@ -76,7 +76,7 @@ const hasRecoverableState = computed(() => {
return props.account?.status === 'error' || Boolean(isRateLimited.value) || Boolean(isOverloaded.value) || Boolean(isTempUnschedulable.value) return props.account?.status === 'error' || Boolean(isRateLimited.value) || Boolean(isOverloaded.value) || Boolean(isTempUnschedulable.value)
}) })
const hasQuotaLimit = computed(() => { 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_limit ?? 0) > 0 ||
(props.account?.quota_daily_limit ?? 0) > 0 || (props.account?.quota_daily_limit ?? 0) > 0 ||
(props.account?.quota_weekly_limit ?? 0) > 0 (props.account?.quota_weekly_limit ?? 0) > 0
......
...@@ -83,7 +83,7 @@ const typeLabel = computed(() => { ...@@ -83,7 +83,7 @@ const typeLabel = computed(() => {
case 'apikey': case 'apikey':
return 'Key' return 'Key'
case 'bedrock': case 'bedrock':
return 'Bedrock' return 'AWS'
default: default:
return props.type return props.type
} }
......
...@@ -412,7 +412,7 @@ export function getPresetMappingsByPlatform(platform: string) { ...@@ -412,7 +412,7 @@ export function getPresetMappingsByPlatform(platform: string) {
if (platform === 'gemini') return geminiPresetMappings if (platform === 'gemini') return geminiPresetMappings
if (platform === 'sora') return soraPresetMappings if (platform === 'sora') return soraPresetMappings
if (platform === 'antigravity') return antigravityPresetMappings if (platform === 'antigravity') return antigravityPresetMappings
if (platform === 'bedrock' || platform === 'bedrock-apikey') return bedrockPresetMappings if (platform === 'bedrock') return bedrockPresetMappings
return anthropicPresetMappings return anthropicPresetMappings
} }
......
...@@ -1934,7 +1934,7 @@ export default { ...@@ -1934,7 +1934,7 @@ export default {
claudeCode: 'Claude Code', claudeCode: 'Claude Code',
claudeConsole: 'Claude Console', claudeConsole: 'Claude Console',
bedrockLabel: 'AWS Bedrock', bedrockLabel: 'AWS Bedrock',
bedrockDesc: 'SigV4 Signing', bedrockDesc: 'SigV4 / API Key',
oauthSetupToken: 'OAuth / Setup Token', oauthSetupToken: 'OAuth / Setup Token',
addMethod: 'Add Method', addMethod: 'Add Method',
setupTokenLongLived: 'Setup Token (Long-lived)', setupTokenLongLived: 'Setup Token (Long-lived)',
...@@ -2136,6 +2136,9 @@ export default { ...@@ -2136,6 +2136,9 @@ export default {
bedrockRegionRequired: 'Please select AWS Region', bedrockRegionRequired: 'Please select AWS Region',
bedrockSessionTokenHint: 'Optional, for temporary credentials', bedrockSessionTokenHint: 'Optional, for temporary credentials',
bedrockSecretKeyLeaveEmpty: 'Leave empty to keep current key', bedrockSecretKeyLeaveEmpty: 'Leave empty to keep current key',
bedrockAuthMode: 'Authentication Mode',
bedrockAuthModeSigv4: 'SigV4 Signing',
bedrockAuthModeApikey: 'Bedrock API Key',
bedrockApiKeyLabel: 'Bedrock API Key', bedrockApiKeyLabel: 'Bedrock API Key',
bedrockApiKeyDesc: 'Bearer Token', bedrockApiKeyDesc: 'Bearer Token',
bedrockApiKeyInput: 'API Key', bedrockApiKeyInput: 'API Key',
...@@ -4127,6 +4130,7 @@ export default { ...@@ -4127,6 +4130,7 @@ export default {
scopeAll: 'All accounts', scopeAll: 'All accounts',
scopeOAuth: 'OAuth only', scopeOAuth: 'OAuth only',
scopeAPIKey: 'API Key only', scopeAPIKey: 'API Key only',
scopeBedrock: 'Bedrock only',
errorMessage: 'Error message', errorMessage: 'Error message',
errorMessagePlaceholder: 'Custom error message when blocked', errorMessagePlaceholder: 'Custom error message when blocked',
errorMessageHint: 'Leave empty for default message', errorMessageHint: 'Leave empty for default message',
......
...@@ -2082,7 +2082,7 @@ export default { ...@@ -2082,7 +2082,7 @@ export default {
claudeCode: 'Claude Code', claudeCode: 'Claude Code',
claudeConsole: 'Claude Console', claudeConsole: 'Claude Console',
bedrockLabel: 'AWS Bedrock', bedrockLabel: 'AWS Bedrock',
bedrockDesc: 'SigV4 签名', bedrockDesc: 'SigV4 / API Key',
oauthSetupToken: 'OAuth / Setup Token', oauthSetupToken: 'OAuth / Setup Token',
addMethod: '添加方式', addMethod: '添加方式',
setupTokenLongLived: 'Setup Token(长期有效)', setupTokenLongLived: 'Setup Token(长期有效)',
...@@ -2277,6 +2277,9 @@ export default { ...@@ -2277,6 +2277,9 @@ export default {
bedrockRegionRequired: '请选择 AWS Region', bedrockRegionRequired: '请选择 AWS Region',
bedrockSessionTokenHint: '可选,用于临时凭证', bedrockSessionTokenHint: '可选,用于临时凭证',
bedrockSecretKeyLeaveEmpty: '留空以保持当前密钥', bedrockSecretKeyLeaveEmpty: '留空以保持当前密钥',
bedrockAuthMode: '认证方式',
bedrockAuthModeSigv4: 'SigV4 签名',
bedrockAuthModeApikey: 'Bedrock API Key',
bedrockApiKeyLabel: 'Bedrock API Key', bedrockApiKeyLabel: 'Bedrock API Key',
bedrockApiKeyDesc: 'Bearer Token 认证', bedrockApiKeyDesc: 'Bearer Token 认证',
bedrockApiKeyInput: 'API Key', bedrockApiKeyInput: 'API Key',
...@@ -4300,6 +4303,7 @@ export default { ...@@ -4300,6 +4303,7 @@ export default {
scopeAll: '全部账号', scopeAll: '全部账号',
scopeOAuth: '仅 OAuth 账号', scopeOAuth: '仅 OAuth 账号',
scopeAPIKey: '仅 API Key 账号', scopeAPIKey: '仅 API Key 账号',
scopeBedrock: '仅 Bedrock 账号',
errorMessage: '错误消息', errorMessage: '错误消息',
errorMessagePlaceholder: '拦截时返回的自定义错误消息', errorMessagePlaceholder: '拦截时返回的自定义错误消息',
errorMessageHint: '留空则使用默认错误消息', errorMessageHint: '留空则使用默认错误消息',
......
...@@ -531,7 +531,7 @@ export interface UpdateGroupRequest { ...@@ -531,7 +531,7 @@ export interface UpdateGroupRequest {
// ==================== Account & Proxy Types ==================== // ==================== Account & Proxy Types ====================
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'sora' 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 OAuthAddMethod = 'oauth' | 'setup-token'
export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h' export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h'
......
...@@ -1745,7 +1745,7 @@ const betaPolicyForm = reactive({ ...@@ -1745,7 +1745,7 @@ const betaPolicyForm = reactive({
rules: [] as Array<{ rules: [] as Array<{
beta_token: string beta_token: string
action: 'pass' | 'filter' | 'block' action: 'pass' | 'filter' | 'block'
scope: 'all' | 'oauth' | 'apikey' scope: 'all' | 'oauth' | 'apikey' | 'bedrock'
error_message?: string error_message?: string
}> }>
}) })
...@@ -2297,7 +2297,8 @@ const betaPolicyActionOptions = computed(() => [ ...@@ -2297,7 +2297,8 @@ const betaPolicyActionOptions = computed(() => [
const betaPolicyScopeOptions = computed(() => [ const betaPolicyScopeOptions = computed(() => [
{ value: 'all', label: t('admin.settings.betaPolicy.scopeAll') }, { value: 'all', label: t('admin.settings.betaPolicy.scopeAll') },
{ value: 'oauth', label: t('admin.settings.betaPolicy.scopeOAuth') }, { 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 方法 // Beta Policy 方法
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment