mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-20 06:44:44 +08:00
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
This commit is contained in:
@@ -563,37 +563,54 @@
|
||||
</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>
|
||||
<label class="input-label">{{ t('admin.accounts.bedrockAccessKeyId') }}</label>
|
||||
<!-- SigV4 fields -->
|
||||
<template v-if="!isBedrockAPIKeyMode">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.bedrockAccessKeyId') }}</label>
|
||||
<input
|
||||
v-model="editBedrockAccessKeyId"
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
placeholder="AKIA..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.bedrockSecretAccessKey') }}</label>
|
||||
<input
|
||||
v-model="editBedrockSecretAccessKey"
|
||||
type="password"
|
||||
class="input font-mono"
|
||||
:placeholder="t('admin.accounts.bedrockSecretKeyLeaveEmpty')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.bedrockSecretKeyLeaveEmpty') }}</p>
|
||||
</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
|
||||
v-model="editBedrockAccessKeyId"
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
placeholder="AKIA..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.bedrockSecretAccessKey') }}</label>
|
||||
<input
|
||||
v-model="editBedrockSecretAccessKey"
|
||||
v-model="editBedrockApiKeyValue"
|
||||
type="password"
|
||||
class="input font-mono"
|
||||
:placeholder="t('admin.accounts.bedrockSecretKeyLeaveEmpty')"
|
||||
:placeholder="t('admin.accounts.bedrockApiKeyLeaveEmpty')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.bedrockSecretKeyLeaveEmpty') }}</p>
|
||||
</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>
|
||||
<p class="input-hint">{{ t('admin.accounts.bedrockApiKeyLeaveEmpty') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Shared: Region -->
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.bedrockRegion') }}</label>
|
||||
<input
|
||||
@@ -604,6 +621,8 @@
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.bedrockRegionHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Shared: Force Global -->
|
||||
<div>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
@@ -684,108 +703,56 @@
|
||||
</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">
|
||||
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||
|
||||
<!-- Mode Toggle -->
|
||||
<div class="mb-4 flex gap-2">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.poolMode') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.poolModeHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="modelRestrictionMode = 'whitelist'"
|
||||
@click="poolModeEnabled = !poolModeEnabled"
|
||||
: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'
|
||||
'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',
|
||||
poolModeEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
{{ t('admin.accounts.modelWhitelist') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="modelRestrictionMode = 'mapping'"
|
||||
:class="[
|
||||
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
|
||||
modelRestrictionMode === 'mapping'
|
||||
? '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>
|
||||
</div>
|
||||
|
||||
<!-- Whitelist Mode -->
|
||||
<div v-if="modelRestrictionMode === 'whitelist'">
|
||||
<ModelWhitelistSelector v-model="allowedModels" platform="anthropic" />
|
||||
<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>
|
||||
<div v-if="poolModeEnabled" class="rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
|
||||
<p class="text-xs text-blue-700 dark:text-blue-400">
|
||||
<Icon name="exclamationCircle" size="sm" class="mr-1 inline" :stroke-width="2" />
|
||||
{{ t('admin.accounts.poolModeInfo') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mapping Mode -->
|
||||
<div v-else class="space-y-3">
|
||||
<div v-for="(mapping, index) in modelMappings" :key="getModelMappingKey(mapping)" class="flex items-center gap-2">
|
||||
<input v-model="mapping.from" type="text" class="input flex-1" :placeholder="t('admin.accounts.fromModel')" />
|
||||
<span class="text-gray-400">→</span>
|
||||
<input v-model="mapping.to" type="text" class="input flex-1" :placeholder="t('admin.accounts.toModel')" />
|
||||
<button type="button" @click="modelMappings.splice(index, 1)" class="text-red-500 hover:text-red-700">
|
||||
<Icon name="trash" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" @click="modelMappings.push({ from: '', to: '' })" class="btn btn-secondary text-sm">
|
||||
+ {{ t('admin.accounts.addMapping') }}
|
||||
</button>
|
||||
<!-- Bedrock Preset Mappings -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="preset in bedrockPresets"
|
||||
: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 v-if="poolModeEnabled" class="mt-3">
|
||||
<label class="input-label">{{ t('admin.accounts.poolModeRetryCount') }}</label>
|
||||
<input
|
||||
v-model.number="poolModeRetryCount"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="MAX_POOL_MODE_RETRY_COUNT"
|
||||
step="1"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t('admin.accounts.poolModeRetryCountHint', {
|
||||
default: DEFAULT_POOL_MODE_RETRY_COUNT,
|
||||
max: MAX_POOL_MODE_RETRY_COUNT
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1182,8 +1149,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key 账号配额限制 -->
|
||||
<div v-if="account?.type === 'apikey'" class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
||||
<!-- API Key / Bedrock 账号配额限制 -->
|
||||
<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">
|
||||
<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">
|
||||
@@ -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<string, unknown>)?.auth_mode === 'apikey'
|
||||
)
|
||||
const modelMappings = ref<ModelMapping[]>([])
|
||||
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
||||
const allowedModels = ref<string[]>([])
|
||||
@@ -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<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) || ''
|
||||
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<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
|
||||
@@ -2155,31 +2139,6 @@ watch(
|
||||
modelMappings.value = []
|
||||
allowedModels.value = []
|
||||
}
|
||||
} else if (newAccount.type === 'bedrock-apikey' && newAccount.credentials) {
|
||||
const bedrockApiKeyCreds = newAccount.credentials as Record<string, unknown>
|
||||
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<string, string> | 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<string, unknown>
|
||||
editBaseUrl.value = (credentials.base_url as string) || ''
|
||||
@@ -2727,7 +2686,6 @@ const handleSubmit = async () => {
|
||||
const currentCredentials = (props.account.credentials as Record<string, unknown>) || {}
|
||||
const newCredentials: Record<string, unknown> = { ...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<string, unknown>) || {}
|
||||
const newCredentials: Record<string, unknown> = { ...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<string, unknown>) ||
|
||||
(props.account.extra as Record<string, unknown>) || {}
|
||||
const newExtra: Record<string, unknown> = { ...currentExtra }
|
||||
|
||||
Reference in New Issue
Block a user