refactor: 将 global_priority 和 rate_multiplier 改为按 API 格式配置

- 新增 global_priority_by_format 字段,支持按 API 格式设置全局优先级
- 移除已废弃的 rate_multiplier 字段,统一使用 rate_multipliers
- 移除已废弃的 timeout 字段(providers 和 provider_endpoints 表)
- 更新调度器以支持按格式的优先级排序
- 同步更新前后端类型定义和 API 接口
- 添加数据库迁移脚本,自动迁移现有数据
This commit is contained in:
fawney19
2026-01-16 17:53:27 +08:00
parent 6c98816f9f
commit d5d74339dd
18 changed files with 247 additions and 136 deletions

View File

@@ -100,10 +100,9 @@ export interface ProviderKeyExport {
name?: string | null
note?: string | null
api_formats: string[]
rate_multiplier?: number
rate_multipliers?: Record<string, number> | null
internal_priority?: number
global_priority?: number | null
global_priority_by_format?: Record<string, number> | null
rpm_limit?: number | null
allowed_models?: any
capabilities?: any

View File

@@ -60,12 +60,11 @@ export interface UserAffinity {
provider_id: string
provider_name: string | null
endpoint_id: string
endpoint_api_format: string | null
endpoint_url: string | null
key_id: string
key_name: string | null
key_prefix: string | null // Provider Key 脱敏显示前4...后4
rate_multiplier: number
rate_multipliers: Record<string, number> | null
global_model_id: string | null // 原始的 global_model_id用于删除
model_name: string | null // 模型名称(如 claude-haiku-4-5-20250514
model_display_name: string | null // 模型显示名称(如 Claude Haiku 4.5

View File

@@ -86,8 +86,6 @@ export async function addProviderKey(
api_formats: string[] // 支持的 API 格式列表(必填)
api_key: string
name: string
/** @deprecated 已废弃,请使用 rate_multipliers */
rate_multiplier?: number // [DEPRECATED] 默认成本倍率,已废弃
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
internal_priority?: number
rpm_limit?: number | null // RPM 限制(留空=自适应模式)
@@ -112,11 +110,9 @@ export async function updateProviderKey(
api_formats: string[] // 支持的 API 格式列表
api_key: string
name: string
/** @deprecated 已废弃,请使用 rate_multipliers */
rate_multiplier: number // [DEPRECATED] 默认成本倍率,已废弃
rate_multipliers: Record<string, number> | null // 按 API 格式的成本倍率
internal_priority: number
global_priority: number | null
global_priority_by_format: Record<string, number> | null // 按 API 格式的全局优先级
rpm_limit: number | null // RPM 限制(留空=自适应模式)
cache_ttl_minutes: number
max_probe_interval_minutes: number

View File

@@ -127,11 +127,9 @@ export interface EndpointAPIKey {
api_key_masked: string
api_key_plain?: string | null
name: string // 密钥名称(必填,用于识别)
/** @deprecated 已废弃,请使用 rate_multipliers */
rate_multiplier: number // [DEPRECATED] 默认成本倍率,已废弃
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率,如 {"CLAUDE_CLI": 1.0, "OPENAI_CLI": 0.8}
internal_priority: number // Key 内部优先级
global_priority?: number | null // 全局 Key 优先级
global_priority_by_format?: Record<string, number> | null // 按 API 格式的全局优先级
rpm_limit?: number | null // RPM 速率限制 (1-10000)null 表示自适应模式
allowed_models?: AllowedModels // 允许使用的模型列表null=不限制)
capabilities?: Record<string, boolean> | null // 能力标签配置(如 cache_1h, context_1m
@@ -205,11 +203,9 @@ export interface EndpointAPIKeyUpdate {
api_formats?: string[] // 支持的 API 格式列表
name?: string
api_key?: string // 仅在需要更新时提供
/** @deprecated 已废弃,请使用 rate_multipliers */
rate_multiplier?: number // [DEPRECATED] 默认成本倍率,已废弃
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
internal_priority?: number
global_priority?: number | null
global_priority_by_format?: Record<string, number> | null // 按 API 格式的全局优先级
rpm_limit?: number | null // RPM 速率限制 (1-10000)null 表示切换为自适应模式
allowed_models?: AllowedModels
capabilities?: Record<string, boolean> | null

View File

@@ -307,7 +307,7 @@
</div>
<!-- 速率倍数 -->
<div class="text-sm font-medium tabular-nums text-primary min-w-[40px] text-right">
{{ key.rate_multiplier }}x
{{ key.rate_multipliers?.[format] ?? 1 }}x
</div>
</div>
</div>
@@ -422,9 +422,10 @@ interface KeyWithMeta {
name: string
api_key_masked: string
internal_priority: number
global_priority: number | null
global_priority_by_format: Record<string, number> | null
format_priority: number | null // 当前格式的优先级(后端计算)
priority: number // 用于编辑的优先级
rate_multiplier: number
rate_multipliers: Record<string, number> | null
is_active: boolean
circuit_breaker_open: boolean
provider_name: string
@@ -530,13 +531,25 @@ async function loadKeysByFormat() {
const { default: client } = await import('@/api/client')
const response = await client.get('/api/admin/endpoints/keys/grouped-by-format')
// 每个 key 添加 priority 字段,基于 global_priority 计算显示优先级
// 每个格式独立管理优先级,使用后端返回的 format_priority
const data: Record<string, KeyWithMeta[]> = {}
for (const [format, keys] of Object.entries(response.data as Record<string, any[]>)) {
data[format] = keys.map((key, index) => ({
// 计算该格式下的默认优先级
let maxPriority = 0
for (const key of keys) {
if (key.format_priority != null) {
maxPriority = Math.max(maxPriority, key.format_priority)
}
}
let nextPriority = maxPriority + 1
data[format] = keys.map((key) => ({
...key,
priority: key.global_priority ?? index + 1
// 使用格式特定优先级,如果没有则分配默认值
priority: key.format_priority ?? nextPriority++
}))
// 按优先级排序
data[format].sort((a, b) => a.priority - b.priority)
}
keysByFormat.value = data
@@ -565,8 +578,9 @@ function finishEditKeyPriority(format: string, key: KeyWithMeta, event: FocusEve
const newPriority = parseInt(input.value, 10)
if (!isNaN(newPriority) && newPriority >= 1) {
// 每个格式独立管理优先级,只更新当前格式
key.priority = newPriority
// 按 priority 重新排序
// 重新排序当前格式
keysByFormat.value[format] = [...keysByFormat.value[format]].sort((a, b) => a.priority - b.priority)
}
@@ -697,12 +711,13 @@ function handleKeyDrop(format: string, dropIndex: number) {
return
}
// 直接交换优先级
// 每个格式独立管理优先级,只交换当前格式内的优先级
draggedItem.priority = targetPriority
targetItem.priority = draggedPriority
// 重新排序
// 重新排序当前格式
keysByFormat.value[format] = [...keys].sort((a, b) => a.priority - b.priority)
draggedKey.value[format] = null
dragOverKey.value[format] = null
}
@@ -732,16 +747,22 @@ async function save() {
updateProvider(provider.id, { provider_priority: provider.provider_priority })
)
const keyUpdates: Promise<any>[] = []
// 收集每个 Key 的按格式优先级(保留原有其他格式的配置)
const keyPriorityByFormatMap = new Map<string, Record<string, number>>()
for (const format of Object.keys(keysByFormat.value)) {
const keys = keysByFormat.value[format]
keys.forEach((key) => {
// 使用用户设置的 priority 值,相同 priority 会做负载均衡
keyUpdates.push(updateProviderKey(key.id, { global_priority: key.priority }))
// 合并原有配置,避免丢失未显示格式的优先级
const existing = keyPriorityByFormatMap.get(key.id) || { ...key.global_priority_by_format }
existing[format] = key.priority
keyPriorityByFormatMap.set(key.id, existing)
})
}
const keyUpdates = Array.from(keyPriorityByFormatMap.entries()).map(([keyId, priorityByFormat]) =>
updateProviderKey(keyId, { global_priority_by_format: priorityByFormat })
)
await Promise.all([...providerUpdates, ...keyUpdates])
await loadKeysByFormat()

View File

@@ -1126,17 +1126,10 @@ function getKeyApiFormats(key: EndpointAPIKey, endpoint?: ProviderEndpointWithKe
// 获取密钥在指定 API 格式下的成本倍率
function getKeyRateMultiplier(key: EndpointAPIKey, format: string): number {
// 优先使用 rate_multipliers 中指定格式的倍率
if (key.rate_multipliers && key.rate_multipliers[format] !== undefined) {
return key.rate_multipliers[format]
}
// 如果 rate_multipliers 存在但该格式未配置,说明用户期望使用默认值 1.0
// 只有当 rate_multipliers 完全不存在时,才回退到 rate_multiplier
if (key.rate_multipliers && Object.keys(key.rate_multipliers).length > 0) {
return 1.0
}
// 回退到默认倍率
return key.rate_multiplier || 1.0
return 1.0
}
// 健康度颜色

View File

@@ -570,11 +570,11 @@ onBeforeUnmount(() => {
:title="item.user_api_key_name || undefined"
>{{ item.user_api_key_name || '未命名' }}</span>
<Badge
v-if="item.rate_multiplier !== 1.0"
v-if="item.api_format && item.rate_multipliers?.[item.api_format] && item.rate_multipliers[item.api_format] !== 1.0"
variant="outline"
class="text-warning border-warning/30 text-[10px] px-2"
>
{{ item.rate_multiplier }}x
{{ item.rate_multipliers[item.api_format] }}x
</Badge>
</div>
<div class="text-xs text-muted-foreground font-mono">
@@ -605,7 +605,7 @@ onBeforeUnmount(() => {
</TableCell>
<TableCell>
<div class="text-sm">
{{ item.endpoint_api_format || '---' }}
{{ item.api_format || '---' }}
</div>
<div class="text-xs text-muted-foreground font-mono">
{{ item.key_prefix || '---' }}
@@ -685,7 +685,7 @@ onBeforeUnmount(() => {
<span class="truncate max-w-[100px]">{{ item.model_display_name || '---' }}</span>
</div>
<div class="flex items-center justify-between text-xs">
<span class="text-muted-foreground">{{ item.endpoint_api_format || '---' }}</span>
<span class="text-muted-foreground">{{ item.api_format || '---' }}</span>
<span>{{ getRemainingTime(item.expire_at) }} · {{ item.request_count }}</span>
</div>
</div>