mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
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:
127
alembic/versions/20260116_1200_add_global_priority_by_format.py
Normal file
127
alembic/versions/20260116_1200_add_global_priority_by_format.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"""add global_priority_by_format and remove deprecated fields
|
||||||
|
|
||||||
|
Revision ID: ddd59cdf0349
|
||||||
|
Revises: 6d579000e511
|
||||||
|
Create Date: 2026-01-16 12:00:00.000000+00:00
|
||||||
|
|
||||||
|
变更:
|
||||||
|
1. provider_api_keys 表: 添加 global_priority_by_format 字段(按 API 格式的全局优先级)
|
||||||
|
2. 迁移现有 global_priority 数据到新字段
|
||||||
|
3. 删除已废弃的 global_priority 字段
|
||||||
|
4. 删除已废弃的 rate_multiplier 字段(已被 rate_multipliers 替代)
|
||||||
|
5. 删除已废弃的 providers.timeout 字段(由环境变量控制)
|
||||||
|
6. 删除已废弃的 provider_endpoints.timeout 字段(由环境变量控制)
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import JSON
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ddd59cdf0349'
|
||||||
|
down_revision = '6d579000e511'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _column_exists(connection, table: str, column: str) -> bool:
|
||||||
|
"""检查列是否存在"""
|
||||||
|
result = connection.execute(
|
||||||
|
sa.text("""
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = :table AND column_name = :column
|
||||||
|
"""),
|
||||||
|
{"table": table, "column": column}
|
||||||
|
)
|
||||||
|
return result.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
connection = op.get_bind()
|
||||||
|
|
||||||
|
# 1. 添加 global_priority_by_format 字段
|
||||||
|
if not _column_exists(connection, 'provider_api_keys', 'global_priority_by_format'):
|
||||||
|
op.add_column(
|
||||||
|
'provider_api_keys',
|
||||||
|
sa.Column('global_priority_by_format', JSON, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 迁移现有 global_priority 数据到新字段
|
||||||
|
# 对于有 global_priority 的 Key,将其值应用到所有支持的 api_formats
|
||||||
|
if _column_exists(connection, 'provider_api_keys', 'global_priority'):
|
||||||
|
# 将 JSON 数组转换为 text[] 后使用 unnest
|
||||||
|
connection.execute(sa.text("""
|
||||||
|
UPDATE provider_api_keys
|
||||||
|
SET global_priority_by_format = (
|
||||||
|
SELECT jsonb_object_agg(format, global_priority)
|
||||||
|
FROM jsonb_array_elements_text(api_formats::jsonb) AS format
|
||||||
|
)
|
||||||
|
WHERE global_priority IS NOT NULL
|
||||||
|
AND api_formats IS NOT NULL
|
||||||
|
AND jsonb_array_length(api_formats::jsonb) > 0
|
||||||
|
AND global_priority_by_format IS NULL
|
||||||
|
"""))
|
||||||
|
|
||||||
|
# 3. 删除 global_priority 字段
|
||||||
|
op.drop_column('provider_api_keys', 'global_priority')
|
||||||
|
|
||||||
|
# 4. 删除 rate_multiplier 字段(已被 rate_multipliers 替代)
|
||||||
|
if _column_exists(connection, 'provider_api_keys', 'rate_multiplier'):
|
||||||
|
op.drop_column('provider_api_keys', 'rate_multiplier')
|
||||||
|
|
||||||
|
# 5. 删除 providers.timeout 字段(由环境变量控制)
|
||||||
|
if _column_exists(connection, 'providers', 'timeout'):
|
||||||
|
op.drop_column('providers', 'timeout')
|
||||||
|
|
||||||
|
# 6. 删除 provider_endpoints.timeout 字段(由环境变量控制)
|
||||||
|
if _column_exists(connection, 'provider_endpoints', 'timeout'):
|
||||||
|
op.drop_column('provider_endpoints', 'timeout')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
connection = op.get_bind()
|
||||||
|
|
||||||
|
# 1. 恢复 rate_multiplier 字段
|
||||||
|
if not _column_exists(connection, 'provider_api_keys', 'rate_multiplier'):
|
||||||
|
op.add_column(
|
||||||
|
'provider_api_keys',
|
||||||
|
sa.Column('rate_multiplier', sa.Float, nullable=False, server_default='1.0')
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 恢复 global_priority 字段并迁移数据
|
||||||
|
if not _column_exists(connection, 'provider_api_keys', 'global_priority'):
|
||||||
|
op.add_column(
|
||||||
|
'provider_api_keys',
|
||||||
|
sa.Column('global_priority', sa.Integer, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 从 global_priority_by_format 迁移数据(取第一个格式的优先级值)
|
||||||
|
if _column_exists(connection, 'provider_api_keys', 'global_priority_by_format'):
|
||||||
|
connection.execute(sa.text("""
|
||||||
|
UPDATE provider_api_keys
|
||||||
|
SET global_priority = (
|
||||||
|
SELECT (value::text)::integer
|
||||||
|
FROM jsonb_each(global_priority_by_format::jsonb)
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE global_priority_by_format IS NOT NULL
|
||||||
|
AND jsonb_typeof(global_priority_by_format::jsonb) = 'object'
|
||||||
|
AND global_priority IS NULL
|
||||||
|
"""))
|
||||||
|
|
||||||
|
# 3. 删除 global_priority_by_format 字段
|
||||||
|
if _column_exists(connection, 'provider_api_keys', 'global_priority_by_format'):
|
||||||
|
op.drop_column('provider_api_keys', 'global_priority_by_format')
|
||||||
|
|
||||||
|
# 4. 恢复 providers.timeout 字段
|
||||||
|
if not _column_exists(connection, 'providers', 'timeout'):
|
||||||
|
op.add_column(
|
||||||
|
'providers',
|
||||||
|
sa.Column('timeout', sa.Integer, nullable=True, server_default='300')
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 恢复 provider_endpoints.timeout 字段
|
||||||
|
if not _column_exists(connection, 'provider_endpoints', 'timeout'):
|
||||||
|
op.add_column(
|
||||||
|
'provider_endpoints',
|
||||||
|
sa.Column('timeout', sa.Integer, nullable=True, server_default='300')
|
||||||
|
)
|
||||||
@@ -100,10 +100,9 @@ export interface ProviderKeyExport {
|
|||||||
name?: string | null
|
name?: string | null
|
||||||
note?: string | null
|
note?: string | null
|
||||||
api_formats: string[]
|
api_formats: string[]
|
||||||
rate_multiplier?: number
|
|
||||||
rate_multipliers?: Record<string, number> | null
|
rate_multipliers?: Record<string, number> | null
|
||||||
internal_priority?: number
|
internal_priority?: number
|
||||||
global_priority?: number | null
|
global_priority_by_format?: Record<string, number> | null
|
||||||
rpm_limit?: number | null
|
rpm_limit?: number | null
|
||||||
allowed_models?: any
|
allowed_models?: any
|
||||||
capabilities?: any
|
capabilities?: any
|
||||||
|
|||||||
@@ -60,12 +60,11 @@ export interface UserAffinity {
|
|||||||
provider_id: string
|
provider_id: string
|
||||||
provider_name: string | null
|
provider_name: string | null
|
||||||
endpoint_id: string
|
endpoint_id: string
|
||||||
endpoint_api_format: string | null
|
|
||||||
endpoint_url: string | null
|
endpoint_url: string | null
|
||||||
key_id: string
|
key_id: string
|
||||||
key_name: string | null
|
key_name: string | null
|
||||||
key_prefix: string | null // Provider Key 脱敏显示(前4...后4)
|
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(用于删除)
|
global_model_id: string | null // 原始的 global_model_id(用于删除)
|
||||||
model_name: string | null // 模型名称(如 claude-haiku-4-5-20250514)
|
model_name: string | null // 模型名称(如 claude-haiku-4-5-20250514)
|
||||||
model_display_name: string | null // 模型显示名称(如 Claude Haiku 4.5)
|
model_display_name: string | null // 模型显示名称(如 Claude Haiku 4.5)
|
||||||
|
|||||||
@@ -86,8 +86,6 @@ export async function addProviderKey(
|
|||||||
api_formats: string[] // 支持的 API 格式列表(必填)
|
api_formats: string[] // 支持的 API 格式列表(必填)
|
||||||
api_key: string
|
api_key: string
|
||||||
name: string
|
name: string
|
||||||
/** @deprecated 已废弃,请使用 rate_multipliers */
|
|
||||||
rate_multiplier?: number // [DEPRECATED] 默认成本倍率,已废弃
|
|
||||||
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
||||||
internal_priority?: number
|
internal_priority?: number
|
||||||
rpm_limit?: number | null // RPM 限制(留空=自适应模式)
|
rpm_limit?: number | null // RPM 限制(留空=自适应模式)
|
||||||
@@ -112,11 +110,9 @@ export async function updateProviderKey(
|
|||||||
api_formats: string[] // 支持的 API 格式列表
|
api_formats: string[] // 支持的 API 格式列表
|
||||||
api_key: string
|
api_key: string
|
||||||
name: string
|
name: string
|
||||||
/** @deprecated 已废弃,请使用 rate_multipliers */
|
|
||||||
rate_multiplier: number // [DEPRECATED] 默认成本倍率,已废弃
|
|
||||||
rate_multipliers: Record<string, number> | null // 按 API 格式的成本倍率
|
rate_multipliers: Record<string, number> | null // 按 API 格式的成本倍率
|
||||||
internal_priority: number
|
internal_priority: number
|
||||||
global_priority: number | null
|
global_priority_by_format: Record<string, number> | null // 按 API 格式的全局优先级
|
||||||
rpm_limit: number | null // RPM 限制(留空=自适应模式)
|
rpm_limit: number | null // RPM 限制(留空=自适应模式)
|
||||||
cache_ttl_minutes: number
|
cache_ttl_minutes: number
|
||||||
max_probe_interval_minutes: number
|
max_probe_interval_minutes: number
|
||||||
|
|||||||
@@ -127,11 +127,9 @@ export interface EndpointAPIKey {
|
|||||||
api_key_masked: string
|
api_key_masked: string
|
||||||
api_key_plain?: string | null
|
api_key_plain?: string | null
|
||||||
name: string // 密钥名称(必填,用于识别)
|
name: string // 密钥名称(必填,用于识别)
|
||||||
/** @deprecated 已废弃,请使用 rate_multipliers */
|
|
||||||
rate_multiplier: number // [DEPRECATED] 默认成本倍率,已废弃
|
|
||||||
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率,如 {"CLAUDE_CLI": 1.0, "OPENAI_CLI": 0.8}
|
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率,如 {"CLAUDE_CLI": 1.0, "OPENAI_CLI": 0.8}
|
||||||
internal_priority: number // Key 内部优先级
|
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 表示自适应模式
|
rpm_limit?: number | null // RPM 速率限制 (1-10000),null 表示自适应模式
|
||||||
allowed_models?: AllowedModels // 允许使用的模型列表(null=不限制)
|
allowed_models?: AllowedModels // 允许使用的模型列表(null=不限制)
|
||||||
capabilities?: Record<string, boolean> | null // 能力标签配置(如 cache_1h, context_1m)
|
capabilities?: Record<string, boolean> | null // 能力标签配置(如 cache_1h, context_1m)
|
||||||
@@ -205,11 +203,9 @@ export interface EndpointAPIKeyUpdate {
|
|||||||
api_formats?: string[] // 支持的 API 格式列表
|
api_formats?: string[] // 支持的 API 格式列表
|
||||||
name?: string
|
name?: string
|
||||||
api_key?: string // 仅在需要更新时提供
|
api_key?: string // 仅在需要更新时提供
|
||||||
/** @deprecated 已废弃,请使用 rate_multipliers */
|
|
||||||
rate_multiplier?: number // [DEPRECATED] 默认成本倍率,已废弃
|
|
||||||
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
||||||
internal_priority?: number
|
internal_priority?: number
|
||||||
global_priority?: number | null
|
global_priority_by_format?: Record<string, number> | null // 按 API 格式的全局优先级
|
||||||
rpm_limit?: number | null // RPM 速率限制 (1-10000),null 表示切换为自适应模式
|
rpm_limit?: number | null // RPM 速率限制 (1-10000),null 表示切换为自适应模式
|
||||||
allowed_models?: AllowedModels
|
allowed_models?: AllowedModels
|
||||||
capabilities?: Record<string, boolean> | null
|
capabilities?: Record<string, boolean> | null
|
||||||
|
|||||||
@@ -307,7 +307,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 速率倍数 -->
|
<!-- 速率倍数 -->
|
||||||
<div class="text-sm font-medium tabular-nums text-primary min-w-[40px] text-right">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,9 +422,10 @@ interface KeyWithMeta {
|
|||||||
name: string
|
name: string
|
||||||
api_key_masked: string
|
api_key_masked: string
|
||||||
internal_priority: number
|
internal_priority: number
|
||||||
global_priority: number | null
|
global_priority_by_format: Record<string, number> | null
|
||||||
|
format_priority: number | null // 当前格式的优先级(后端计算)
|
||||||
priority: number // 用于编辑的优先级
|
priority: number // 用于编辑的优先级
|
||||||
rate_multiplier: number
|
rate_multipliers: Record<string, number> | null
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
circuit_breaker_open: boolean
|
circuit_breaker_open: boolean
|
||||||
provider_name: string
|
provider_name: string
|
||||||
@@ -530,13 +531,25 @@ async function loadKeysByFormat() {
|
|||||||
const { default: client } = await import('@/api/client')
|
const { default: client } = await import('@/api/client')
|
||||||
const response = await client.get('/api/admin/endpoints/keys/grouped-by-format')
|
const response = await client.get('/api/admin/endpoints/keys/grouped-by-format')
|
||||||
|
|
||||||
// 为每个 key 添加 priority 字段,基于 global_priority 计算显示优先级
|
// 每个格式独立管理优先级,使用后端返回的 format_priority
|
||||||
const data: Record<string, KeyWithMeta[]> = {}
|
const data: Record<string, KeyWithMeta[]> = {}
|
||||||
for (const [format, keys] of Object.entries(response.data as Record<string, any[]>)) {
|
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,
|
...key,
|
||||||
priority: key.global_priority ?? index + 1
|
// 使用格式特定优先级,如果没有则分配默认值
|
||||||
|
priority: key.format_priority ?? nextPriority++
|
||||||
}))
|
}))
|
||||||
|
// 按优先级排序
|
||||||
|
data[format].sort((a, b) => a.priority - b.priority)
|
||||||
}
|
}
|
||||||
keysByFormat.value = data
|
keysByFormat.value = data
|
||||||
|
|
||||||
@@ -565,8 +578,9 @@ function finishEditKeyPriority(format: string, key: KeyWithMeta, event: FocusEve
|
|||||||
const newPriority = parseInt(input.value, 10)
|
const newPriority = parseInt(input.value, 10)
|
||||||
|
|
||||||
if (!isNaN(newPriority) && newPriority >= 1) {
|
if (!isNaN(newPriority) && newPriority >= 1) {
|
||||||
|
// 每个格式独立管理优先级,只更新当前格式
|
||||||
key.priority = newPriority
|
key.priority = newPriority
|
||||||
// 按 priority 重新排序
|
// 重新排序当前格式
|
||||||
keysByFormat.value[format] = [...keysByFormat.value[format]].sort((a, b) => a.priority - b.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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接交换优先级
|
// 每个格式独立管理优先级,只交换当前格式内的优先级
|
||||||
draggedItem.priority = targetPriority
|
draggedItem.priority = targetPriority
|
||||||
targetItem.priority = draggedPriority
|
targetItem.priority = draggedPriority
|
||||||
|
|
||||||
// 重新排序
|
// 重新排序当前格式
|
||||||
keysByFormat.value[format] = [...keys].sort((a, b) => a.priority - b.priority)
|
keysByFormat.value[format] = [...keys].sort((a, b) => a.priority - b.priority)
|
||||||
|
|
||||||
draggedKey.value[format] = null
|
draggedKey.value[format] = null
|
||||||
dragOverKey.value[format] = null
|
dragOverKey.value[format] = null
|
||||||
}
|
}
|
||||||
@@ -732,16 +747,22 @@ async function save() {
|
|||||||
updateProvider(provider.id, { provider_priority: provider.provider_priority })
|
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)) {
|
for (const format of Object.keys(keysByFormat.value)) {
|
||||||
const keys = keysByFormat.value[format]
|
const keys = keysByFormat.value[format]
|
||||||
keys.forEach((key) => {
|
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 Promise.all([...providerUpdates, ...keyUpdates])
|
||||||
|
|
||||||
await loadKeysByFormat()
|
await loadKeysByFormat()
|
||||||
|
|||||||
@@ -1126,17 +1126,10 @@ function getKeyApiFormats(key: EndpointAPIKey, endpoint?: ProviderEndpointWithKe
|
|||||||
|
|
||||||
// 获取密钥在指定 API 格式下的成本倍率
|
// 获取密钥在指定 API 格式下的成本倍率
|
||||||
function getKeyRateMultiplier(key: EndpointAPIKey, format: string): number {
|
function getKeyRateMultiplier(key: EndpointAPIKey, format: string): number {
|
||||||
// 优先使用 rate_multipliers 中指定格式的倍率
|
|
||||||
if (key.rate_multipliers && key.rate_multipliers[format] !== undefined) {
|
if (key.rate_multipliers && key.rate_multipliers[format] !== undefined) {
|
||||||
return key.rate_multipliers[format]
|
return key.rate_multipliers[format]
|
||||||
}
|
}
|
||||||
// 如果 rate_multipliers 存在但该格式未配置,说明用户期望使用默认值 1.0
|
return 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 健康度颜色
|
// 健康度颜色
|
||||||
|
|||||||
@@ -570,11 +570,11 @@ onBeforeUnmount(() => {
|
|||||||
:title="item.user_api_key_name || undefined"
|
:title="item.user_api_key_name || undefined"
|
||||||
>{{ item.user_api_key_name || '未命名' }}</span>
|
>{{ item.user_api_key_name || '未命名' }}</span>
|
||||||
<Badge
|
<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"
|
variant="outline"
|
||||||
class="text-warning border-warning/30 text-[10px] px-2"
|
class="text-warning border-warning/30 text-[10px] px-2"
|
||||||
>
|
>
|
||||||
{{ item.rate_multiplier }}x
|
{{ item.rate_multipliers[item.api_format] }}x
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground font-mono">
|
<div class="text-xs text-muted-foreground font-mono">
|
||||||
@@ -605,7 +605,7 @@ onBeforeUnmount(() => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{{ item.endpoint_api_format || '---' }}
|
{{ item.api_format || '---' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground font-mono">
|
<div class="text-xs text-muted-foreground font-mono">
|
||||||
{{ item.key_prefix || '---' }}
|
{{ item.key_prefix || '---' }}
|
||||||
@@ -685,7 +685,7 @@ onBeforeUnmount(() => {
|
|||||||
<span class="truncate max-w-[100px]">{{ item.model_display_name || '---' }}</span>
|
<span class="truncate max-w-[100px]">{{ item.model_display_name || '---' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-xs">
|
<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>
|
<span>{{ getRemainingTime(item.expire_at) }} · {{ item.request_count }}次</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async def update_endpoint_key(
|
|||||||
- `api_key`: 新的 API Key 原文
|
- `api_key`: 新的 API Key 原文
|
||||||
- `name`: Key 名称
|
- `name`: Key 名称
|
||||||
- `note`: 备注
|
- `note`: 备注
|
||||||
- `rate_multiplier`: 速率倍数
|
- `rate_multipliers`: 按 API 格式的成本倍率
|
||||||
- `internal_priority`: 内部优先级
|
- `internal_priority`: 内部优先级
|
||||||
- `rpm_limit`: RPM 限制(设置为 null 可切换到自适应模式)
|
- `rpm_limit`: RPM 限制(设置为 null 可切换到自适应模式)
|
||||||
- `allowed_models`: 允许的模型列表
|
- `allowed_models`: 允许的模型列表
|
||||||
@@ -82,8 +82,9 @@ async def get_keys_grouped_by_format(
|
|||||||
- `name`: Key 名称
|
- `name`: Key 名称
|
||||||
- `api_key_masked`: 脱敏后的 API Key
|
- `api_key_masked`: 脱敏后的 API Key
|
||||||
- `internal_priority`: 内部优先级
|
- `internal_priority`: 内部优先级
|
||||||
- `global_priority`: 全局优先级
|
- `global_priority_by_format`: 按 API 格式的全局优先级
|
||||||
- `rate_multiplier`: 速率倍数
|
- `format_priority`: 当前格式的优先级
|
||||||
|
- `rate_multipliers`: 按 API 格式的成本倍率
|
||||||
- `is_active`: 是否活跃
|
- `is_active`: 是否活跃
|
||||||
- `circuit_breaker_open`: 熔断器状态
|
- `circuit_breaker_open`: 熔断器状态
|
||||||
- `provider_name`: Provider 名称
|
- `provider_name`: Provider 名称
|
||||||
@@ -367,7 +368,6 @@ class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
|
|||||||
Provider.is_active.is_(True),
|
Provider.is_active.is_(True),
|
||||||
)
|
)
|
||||||
.order_by(
|
.order_by(
|
||||||
ProviderAPIKey.global_priority.asc().nullslast(),
|
|
||||||
ProviderAPIKey.internal_priority.asc(),
|
ProviderAPIKey.internal_priority.asc(),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
@@ -427,8 +427,8 @@ class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
|
|||||||
"name": key.name,
|
"name": key.name,
|
||||||
"api_key_masked": masked_key,
|
"api_key_masked": masked_key,
|
||||||
"internal_priority": key.internal_priority,
|
"internal_priority": key.internal_priority,
|
||||||
"global_priority": key.global_priority,
|
"global_priority_by_format": key.global_priority_by_format,
|
||||||
"rate_multiplier": key.rate_multiplier,
|
"rate_multipliers": key.rate_multipliers,
|
||||||
"is_active": key.is_active,
|
"is_active": key.is_active,
|
||||||
"provider_name": provider.name,
|
"provider_name": provider.name,
|
||||||
"api_formats": api_formats,
|
"api_formats": api_formats,
|
||||||
@@ -438,9 +438,10 @@ class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
|
|||||||
"request_count": key.request_count,
|
"request_count": key.request_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 将 Key 添加到每个支持的格式分组中,并附加格式特定的健康度数据
|
# 将 Key 添加到每个支持的格式分组中,并附加格式特定的数据
|
||||||
health_by_format = key.health_by_format or {}
|
health_by_format = key.health_by_format or {}
|
||||||
circuit_by_format = key.circuit_breaker_by_format or {}
|
circuit_by_format = key.circuit_breaker_by_format or {}
|
||||||
|
priority_by_format = key.global_priority_by_format or {}
|
||||||
provider_id = str(provider.id)
|
provider_id = str(provider.id)
|
||||||
for api_format in api_formats:
|
for api_format in api_formats:
|
||||||
if api_format not in grouped:
|
if api_format not in grouped:
|
||||||
@@ -451,6 +452,8 @@ class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
|
|||||||
format_key_info["endpoint_base_url"] = endpoint_base_url_map.get(
|
format_key_info["endpoint_base_url"] = endpoint_base_url_map.get(
|
||||||
(provider_id, api_format)
|
(provider_id, api_format)
|
||||||
)
|
)
|
||||||
|
# 添加格式特定的优先级
|
||||||
|
format_key_info["format_priority"] = priority_by_format.get(api_format)
|
||||||
# 添加格式特定的健康度数据
|
# 添加格式特定的健康度数据
|
||||||
format_health = health_by_format.get(api_format, {})
|
format_health = health_by_format.get(api_format, {})
|
||||||
format_circuit = circuit_by_format.get(api_format, {})
|
format_circuit = circuit_by_format.get(api_format, {})
|
||||||
@@ -597,8 +600,7 @@ class AdminCreateProviderKeyAdapter(AdminApiAdapter):
|
|||||||
api_key=encrypted_key,
|
api_key=encrypted_key,
|
||||||
name=self.key_data.name,
|
name=self.key_data.name,
|
||||||
note=self.key_data.note,
|
note=self.key_data.note,
|
||||||
rate_multiplier=self.key_data.rate_multiplier,
|
rate_multipliers=self.key_data.rate_multipliers,
|
||||||
rate_multipliers=self.key_data.rate_multipliers, # 按 API 格式的成本倍率
|
|
||||||
internal_priority=self.key_data.internal_priority,
|
internal_priority=self.key_data.internal_priority,
|
||||||
rpm_limit=self.key_data.rpm_limit,
|
rpm_limit=self.key_data.rpm_limit,
|
||||||
allowed_models=self.key_data.allowed_models if self.key_data.allowed_models else None,
|
allowed_models=self.key_data.allowed_models if self.key_data.allowed_models else None,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class RoutingKeyInfo(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
masked_key: str = Field("", description="脱敏的 API Key")
|
masked_key: str = Field("", description="脱敏的 API Key")
|
||||||
internal_priority: int = Field(..., description="Key 内部优先级")
|
internal_priority: int = Field(..., description="Key 内部优先级")
|
||||||
global_priority: Optional[int] = Field(None, description="全局 Key 优先级")
|
global_priority_by_format: Optional[Dict[str, int]] = Field(None, description="按 API 格式的全局优先级")
|
||||||
rpm_limit: Optional[int] = Field(None, description="RPM 限制,null 表示自适应")
|
rpm_limit: Optional[int] = Field(None, description="RPM 限制,null 表示自适应")
|
||||||
is_adaptive: bool = Field(False, description="是否为自适应 RPM 模式")
|
is_adaptive: bool = Field(False, description="是否为自适应 RPM 模式")
|
||||||
effective_rpm: Optional[int] = Field(None, description="有效 RPM 限制")
|
effective_rpm: Optional[int] = Field(None, description="有效 RPM 限制")
|
||||||
@@ -293,8 +293,14 @@ class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
|
|||||||
for ep in provider_endpoints:
|
for ep in provider_endpoints:
|
||||||
# 获取该 Endpoint 格式对应的 Keys
|
# 获取该 Endpoint 格式对应的 Keys
|
||||||
ep_keys = keys_by_endpoint.get(ep.api_format or "", [])
|
ep_keys = keys_by_endpoint.get(ep.api_format or "", [])
|
||||||
# 按优先级排序
|
# 按优先级排序(使用当前格式的全局优先级)
|
||||||
ep_keys.sort(key=lambda k: (k.global_priority or 999, k.internal_priority or 0))
|
api_format = ep.api_format or ""
|
||||||
|
def get_key_priority(k: ProviderAPIKey) -> tuple[int, int]:
|
||||||
|
format_priority = 999
|
||||||
|
if k.global_priority_by_format and api_format in k.global_priority_by_format:
|
||||||
|
format_priority = k.global_priority_by_format[api_format]
|
||||||
|
return (format_priority, k.internal_priority or 0)
|
||||||
|
ep_keys.sort(key=get_key_priority)
|
||||||
|
|
||||||
key_infos = []
|
key_infos = []
|
||||||
for key in ep_keys:
|
for key in ep_keys:
|
||||||
@@ -353,7 +359,7 @@ class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
|
|||||||
name=key.name or "",
|
name=key.name or "",
|
||||||
masked_key=masked_key,
|
masked_key=masked_key,
|
||||||
internal_priority=key.internal_priority or 0,
|
internal_priority=key.internal_priority or 0,
|
||||||
global_priority=key.global_priority,
|
global_priority_by_format=key.global_priority_by_format,
|
||||||
rpm_limit=key.rpm_limit,
|
rpm_limit=key.rpm_limit,
|
||||||
is_adaptive=is_adaptive,
|
is_adaptive=is_adaptive,
|
||||||
effective_rpm=effective_rpm,
|
effective_rpm=effective_rpm,
|
||||||
|
|||||||
@@ -213,12 +213,11 @@ async def list_affinities(
|
|||||||
- `provider_id`: Provider ID
|
- `provider_id`: Provider ID
|
||||||
- `provider_name`: Provider 显示名称
|
- `provider_name`: Provider 显示名称
|
||||||
- `endpoint_id`: Endpoint ID
|
- `endpoint_id`: Endpoint ID
|
||||||
- `endpoint_api_format`: Endpoint API 格式
|
|
||||||
- `endpoint_url`: Endpoint 基础 URL
|
- `endpoint_url`: Endpoint 基础 URL
|
||||||
- `key_id`: Key ID
|
- `key_id`: Key ID
|
||||||
- `key_name`: Key 名称
|
- `key_name`: Key 名称
|
||||||
- `key_prefix`: 脱敏后的 Provider Key
|
- `key_prefix`: 脱敏后的 Provider Key
|
||||||
- `rate_multiplier`: 速率倍数
|
- `rate_multipliers`: 按 API 格式的成本倍率
|
||||||
- `global_model_id`: GlobalModel ID
|
- `global_model_id`: GlobalModel ID
|
||||||
- `model_name`: 模型名称
|
- `model_name`: 模型名称
|
||||||
- `model_display_name`: 模型显示名称
|
- `model_display_name`: 模型显示名称
|
||||||
@@ -821,14 +820,11 @@ class AdminListAffinitiesAdapter(AdminApiAdapter):
|
|||||||
"provider_id": provider_id,
|
"provider_id": provider_id,
|
||||||
"provider_name": provider.name if provider else None,
|
"provider_name": provider.name if provider else None,
|
||||||
"endpoint_id": endpoint_id,
|
"endpoint_id": endpoint_id,
|
||||||
"endpoint_api_format": (
|
|
||||||
endpoint.api_format if endpoint and endpoint.api_format else None
|
|
||||||
),
|
|
||||||
"endpoint_url": endpoint.base_url if endpoint else None,
|
"endpoint_url": endpoint.base_url if endpoint else None,
|
||||||
"key_id": key_id,
|
"key_id": key_id,
|
||||||
"key_name": key.name if key else None,
|
"key_name": key.name if key else None,
|
||||||
"key_prefix": provider_key_masked,
|
"key_prefix": provider_key_masked,
|
||||||
"rate_multiplier": key.rate_multiplier if key else 1.0,
|
"rate_multipliers": key.rate_multipliers if key else None,
|
||||||
"global_model_id": affinity.get("model_name"), # 原始的 global_model_id
|
"global_model_id": affinity.get("model_name"), # 原始的 global_model_id
|
||||||
"model_name": (
|
"model_name": (
|
||||||
global_model_map.get(affinity.get("model_name")).name
|
global_model_map.get(affinity.get("model_name")).name
|
||||||
|
|||||||
@@ -803,10 +803,9 @@ class AdminExportConfigAdapter(AdminApiAdapter):
|
|||||||
"name": key.name,
|
"name": key.name,
|
||||||
"note": key.note,
|
"note": key.note,
|
||||||
"api_formats": key.api_formats or [],
|
"api_formats": key.api_formats or [],
|
||||||
"rate_multiplier": key.rate_multiplier,
|
|
||||||
"rate_multipliers": key.rate_multipliers,
|
"rate_multipliers": key.rate_multipliers,
|
||||||
"internal_priority": key.internal_priority,
|
"internal_priority": key.internal_priority,
|
||||||
"global_priority": key.global_priority,
|
"global_priority_by_format": key.global_priority_by_format,
|
||||||
"rpm_limit": key.rpm_limit,
|
"rpm_limit": key.rpm_limit,
|
||||||
"allowed_models": key.allowed_models,
|
"allowed_models": key.allowed_models,
|
||||||
"capabilities": key.capabilities,
|
"capabilities": key.capabilities,
|
||||||
@@ -1159,10 +1158,9 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
api_key=encrypted_key,
|
api_key=encrypted_key,
|
||||||
name=key_data.get("name") or "Imported Key",
|
name=key_data.get("name") or "Imported Key",
|
||||||
note=key_data.get("note"),
|
note=key_data.get("note"),
|
||||||
rate_multiplier=key_data.get("rate_multiplier", 1.0),
|
|
||||||
rate_multipliers=key_data.get("rate_multipliers"),
|
rate_multipliers=key_data.get("rate_multipliers"),
|
||||||
internal_priority=key_data.get("internal_priority", 50),
|
internal_priority=key_data.get("internal_priority", 50),
|
||||||
global_priority=key_data.get("global_priority"),
|
global_priority_by_format=key_data.get("global_priority_by_format"),
|
||||||
rpm_limit=key_data.get("rpm_limit"),
|
rpm_limit=key_data.get("rpm_limit"),
|
||||||
allowed_models=key_data.get("allowed_models"),
|
allowed_models=key_data.get("allowed_models"),
|
||||||
capabilities=key_data.get("capabilities"),
|
capabilities=key_data.get("capabilities"),
|
||||||
|
|||||||
@@ -551,11 +551,7 @@ class Provider(Base):
|
|||||||
# 限制
|
# 限制
|
||||||
concurrent_limit = Column(Integer, nullable=True) # 并发请求限制
|
concurrent_limit = Column(Integer, nullable=True) # 并发请求限制
|
||||||
|
|
||||||
# 请求配置(从 Endpoint 迁移,作为全局默认值)
|
# 请求配置
|
||||||
# [已废弃] timeout 字段不再使用,超时由环境变量控制:
|
|
||||||
# - 非流式请求: HTTP_REQUEST_TIMEOUT(默认 300 秒)
|
|
||||||
# - 流式首字节: STREAM_FIRST_BYTE_TIMEOUT(默认 30 秒)
|
|
||||||
timeout = Column(Integer, default=300, nullable=True) # [已废弃] 请求超时(秒)
|
|
||||||
max_retries = Column(Integer, default=2, nullable=True) # 最大重试次数
|
max_retries = Column(Integer, default=2, nullable=True) # 最大重试次数
|
||||||
proxy = Column(JSONB, nullable=True) # 代理配置: {url, username, password, enabled}
|
proxy = Column(JSONB, nullable=True) # 代理配置: {url, username, password, enabled}
|
||||||
|
|
||||||
@@ -603,7 +599,6 @@ class ProviderEndpoint(Base):
|
|||||||
|
|
||||||
# 请求配置
|
# 请求配置
|
||||||
header_rules = Column(JSON, nullable=True) # 请求头规则 [{action, key, value, from, to}]
|
header_rules = Column(JSON, nullable=True) # 请求头规则 [{action, key, value, from, to}]
|
||||||
timeout = Column(Integer, default=300) # [已废弃] 超时(秒),由环境变量控制
|
|
||||||
max_retries = Column(Integer, default=2) # 最大重试次数
|
max_retries = Column(Integer, default=2) # 最大重试次数
|
||||||
|
|
||||||
# 状态
|
# 状态
|
||||||
@@ -1004,11 +999,6 @@ class ProviderAPIKey(Base):
|
|||||||
note = Column(String(500), nullable=True) # 备注说明(可选)
|
note = Column(String(500), nullable=True) # 备注说明(可选)
|
||||||
|
|
||||||
# 成本计算
|
# 成本计算
|
||||||
# [DEPRECATED] rate_multiplier 已废弃,请使用 rate_multipliers
|
|
||||||
# 将在未来版本中移除,目前仅作为 rate_multipliers 未配置时的回退值
|
|
||||||
rate_multiplier = Column(
|
|
||||||
Float, default=1.0, nullable=False
|
|
||||||
) # [DEPRECATED] 默认成本倍率,请使用 rate_multipliers
|
|
||||||
rate_multipliers = Column(
|
rate_multipliers = Column(
|
||||||
JSON, nullable=True
|
JSON, nullable=True
|
||||||
) # 按 API 格式的成本倍率 {"CLAUDE_CLI": 1.0, "OPENAI_CLI": 0.8}
|
) # 按 API 格式的成本倍率 {"CLAUDE_CLI": 1.0, "OPENAI_CLI": 0.8}
|
||||||
@@ -1017,9 +1007,9 @@ class ProviderAPIKey(Base):
|
|||||||
internal_priority = Column(
|
internal_priority = Column(
|
||||||
Integer, default=50
|
Integer, default=50
|
||||||
) # Endpoint 内部优先级(用于提供商优先模式,同 Endpoint 内 Keys 的排序,同优先级参与负载均衡)
|
) # Endpoint 内部优先级(用于提供商优先模式,同 Endpoint 内 Keys 的排序,同优先级参与负载均衡)
|
||||||
global_priority = Column(
|
global_priority_by_format = Column(
|
||||||
Integer, nullable=True
|
JSON, nullable=True
|
||||||
) # 全局 Key 优先级(用于全局 Key 优先模式,跨 Provider 的 Key 排序,NULL=未配置使用默认排序)
|
) # 按 API 格式的全局优先级 {"CLAUDE": 1, "CLAUDE_CLI": 2}
|
||||||
|
|
||||||
# RPM 限制配置(自适应学习)
|
# RPM 限制配置(自适应学习)
|
||||||
# rpm_limit 决定 RPM 控制模式:
|
# rpm_limit 决定 RPM 控制模式:
|
||||||
|
|||||||
@@ -152,10 +152,6 @@ class EndpointAPIKeyCreate(BaseModel):
|
|||||||
name: str = Field(..., min_length=1, max_length=100, description="密钥名称(必填,用于识别)")
|
name: str = Field(..., min_length=1, max_length=100, description="密钥名称(必填,用于识别)")
|
||||||
|
|
||||||
# 成本计算
|
# 成本计算
|
||||||
# [DEPRECATED] rate_multiplier 已废弃,请使用 rate_multipliers
|
|
||||||
rate_multiplier: float = Field(
|
|
||||||
default=1.0, ge=0.01, description="[DEPRECATED] 默认成本倍率,已废弃,请使用 rate_multipliers"
|
|
||||||
)
|
|
||||||
rate_multipliers: Optional[Dict[str, float]] = Field(
|
rate_multipliers: Optional[Dict[str, float]] = Field(
|
||||||
default=None, description="按 API 格式的成本倍率,如 {'CLAUDE_CLI': 1.0, 'OPENAI_CLI': 0.8}"
|
default=None, description="按 API 格式的成本倍率,如 {'CLAUDE_CLI': 1.0, 'OPENAI_CLI': 0.8}"
|
||||||
)
|
)
|
||||||
@@ -294,16 +290,14 @@ class EndpointAPIKeyUpdate(BaseModel):
|
|||||||
default=None, min_length=3, max_length=500, description="API Key(将自动加密)"
|
default=None, min_length=3, max_length=500, description="API Key(将自动加密)"
|
||||||
)
|
)
|
||||||
name: Optional[str] = Field(default=None, min_length=1, max_length=100, description="密钥名称")
|
name: Optional[str] = Field(default=None, min_length=1, max_length=100, description="密钥名称")
|
||||||
# [DEPRECATED] rate_multiplier 已废弃,请使用 rate_multipliers
|
|
||||||
rate_multiplier: Optional[float] = Field(default=None, ge=0.01, description="[DEPRECATED] 默认成本倍率,已废弃")
|
|
||||||
rate_multipliers: Optional[Dict[str, float]] = Field(
|
rate_multipliers: Optional[Dict[str, float]] = Field(
|
||||||
default=None, description="按 API 格式的成本倍率,如 {'CLAUDE_CLI': 1.0, 'OPENAI_CLI': 0.8}"
|
default=None, description="按 API 格式的成本倍率,如 {'CLAUDE_CLI': 1.0, 'OPENAI_CLI': 0.8}"
|
||||||
)
|
)
|
||||||
internal_priority: Optional[int] = Field(
|
internal_priority: Optional[int] = Field(
|
||||||
default=None, description="Key 内部优先级(提供商优先模式,数字越小越优先)"
|
default=None, description="Key 内部优先级(提供商优先模式,数字越小越优先)"
|
||||||
)
|
)
|
||||||
global_priority: Optional[int] = Field(
|
global_priority_by_format: Optional[Dict[str, int]] = Field(
|
||||||
default=None, description="全局 Key 优先级(全局 Key 优先模式,数字越小越优先)"
|
default=None, description="按 API 格式的全局优先级,如 {'CLAUDE': 1, 'CLAUDE_CLI': 2}"
|
||||||
)
|
)
|
||||||
# rpm_limit: 使用特殊标记区分"未提供"和"设置为 null(自适应模式)"
|
# rpm_limit: 使用特殊标记区分"未提供"和"设置为 null(自适应模式)"
|
||||||
# - 不提供字段:不更新
|
# - 不提供字段:不更新
|
||||||
@@ -421,15 +415,15 @@ class EndpointAPIKeyResponse(BaseModel):
|
|||||||
name: str = Field(..., description="密钥名称")
|
name: str = Field(..., description="密钥名称")
|
||||||
|
|
||||||
# 成本计算
|
# 成本计算
|
||||||
# [DEPRECATED] rate_multiplier 已废弃,请使用 rate_multipliers
|
|
||||||
rate_multiplier: float = Field(default=1.0, description="[DEPRECATED] 默认成本倍率,已废弃")
|
|
||||||
rate_multipliers: Optional[Dict[str, float]] = Field(
|
rate_multipliers: Optional[Dict[str, float]] = Field(
|
||||||
default=None, description="按 API 格式的成本倍率,如 {'CLAUDE_CLI': 1.0, 'OPENAI_CLI': 0.8}"
|
default=None, description="按 API 格式的成本倍率,如 {'CLAUDE_CLI': 1.0, 'OPENAI_CLI': 0.8}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 优先级和限制
|
# 优先级和限制
|
||||||
internal_priority: int = Field(default=50, description="Endpoint 内部优先级")
|
internal_priority: int = Field(default=50, description="Endpoint 内部优先级")
|
||||||
global_priority: Optional[int] = Field(default=None, description="全局 Key 优先级")
|
global_priority_by_format: Optional[Dict[str, int]] = Field(
|
||||||
|
default=None, description="按 API 格式的全局优先级"
|
||||||
|
)
|
||||||
rpm_limit: Optional[int] = None
|
rpm_limit: Optional[int] = None
|
||||||
allowed_models: Optional[List[str]] = None
|
allowed_models: Optional[List[str]] = None
|
||||||
capabilities: Optional[Dict[str, bool]] = Field(default=None, description="Key 能力标签")
|
capabilities: Optional[Dict[str, bool]] = Field(default=None, description="Key 能力标签")
|
||||||
@@ -591,7 +585,6 @@ class ProviderUpdateRequest(BaseModel):
|
|||||||
quota_reset_day: Optional[int] = Field(None, ge=1, le=31, description="配额重置日(1-31)")
|
quota_reset_day: Optional[int] = Field(None, ge=1, le=31, description="配额重置日(1-31)")
|
||||||
quota_expires_at: Optional[datetime] = Field(None, description="配额过期时间")
|
quota_expires_at: Optional[datetime] = Field(None, description="配额过期时间")
|
||||||
# 请求配置(从 Endpoint 迁移)
|
# 请求配置(从 Endpoint 迁移)
|
||||||
timeout: Optional[int] = Field(None, ge=1, le=600, description="请求超时(秒)")
|
|
||||||
max_retries: Optional[int] = Field(None, ge=0, le=10, description="最大重试次数")
|
max_retries: Optional[int] = Field(None, ge=0, le=10, description="最大重试次数")
|
||||||
proxy: Optional[Dict[str, Any]] = Field(None, description="代理配置")
|
proxy: Optional[Dict[str, Any]] = Field(None, description="代理配置")
|
||||||
|
|
||||||
|
|||||||
61
src/services/cache/aware_scheduler.py
vendored
61
src/services/cache/aware_scheduler.py
vendored
@@ -670,7 +670,7 @@ class CacheAwareScheduler:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. 应用优先级模式排序
|
# 3. 应用优先级模式排序
|
||||||
candidates = self._apply_priority_mode_sort(candidates, affinity_key)
|
candidates = self._apply_priority_mode_sort(candidates, affinity_key, target_format.value)
|
||||||
|
|
||||||
# 更新指标
|
# 更新指标
|
||||||
self._metrics["total_candidates"] += len(candidates)
|
self._metrics["total_candidates"] += len(candidates)
|
||||||
@@ -693,7 +693,7 @@ class CacheAwareScheduler:
|
|||||||
)
|
)
|
||||||
elif self.scheduling_mode == self.SCHEDULING_MODE_LOAD_BALANCE:
|
elif self.scheduling_mode == self.SCHEDULING_MODE_LOAD_BALANCE:
|
||||||
# 负载均衡模式:忽略缓存,同优先级内随机轮换
|
# 负载均衡模式:忽略缓存,同优先级内随机轮换
|
||||||
candidates = self._apply_load_balance(candidates)
|
candidates = self._apply_load_balance(candidates, target_format.value)
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
candidate.is_cached = False
|
candidate.is_cached = False
|
||||||
else:
|
else:
|
||||||
@@ -1188,50 +1188,57 @@ class CacheAwareScheduler:
|
|||||||
logger.debug(f"[CacheAwareScheduler] 切换调度模式为: {self.scheduling_mode}")
|
logger.debug(f"[CacheAwareScheduler] 切换调度模式为: {self.scheduling_mode}")
|
||||||
|
|
||||||
def _apply_priority_mode_sort(
|
def _apply_priority_mode_sort(
|
||||||
self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None
|
self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None,
|
||||||
|
api_format: Optional[str] = None
|
||||||
) -> List[ProviderCandidate]:
|
) -> List[ProviderCandidate]:
|
||||||
"""
|
"""
|
||||||
根据优先级模式对候选列表排序(数字越小越优先)
|
根据优先级模式对候选列表排序(数字越小越优先)
|
||||||
|
|
||||||
- provider: 提供商优先模式,保持原有顺序(按 Provider.provider_priority -> Key.internal_priority 排序,已由查询保证)
|
- provider: 提供商优先模式,保持原有顺序(按 Provider.provider_priority -> Key.internal_priority 排序,已由查询保证)
|
||||||
Key.internal_priority 表示 Endpoint 内部优先级,同优先级内通过哈希分散负载均衡
|
Key.internal_priority 表示 Endpoint 内部优先级,同优先级内通过哈希分散负载均衡
|
||||||
- global_key: 全局 Key 优先模式,按 Key.global_priority 升序排序(数字小的优先)
|
- global_key: 全局 Key 优先模式,按 Key.global_priority_by_format 升序排序(数字小的优先)
|
||||||
有 global_priority 的优先,NULL 的排后面
|
有优先级的优先,NULL 的排后面
|
||||||
同 global_priority 内通过哈希分散实现负载均衡
|
同优先级内通过哈希分散实现负载均衡
|
||||||
"""
|
"""
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
if self.priority_mode == self.PRIORITY_MODE_GLOBAL_KEY:
|
if self.priority_mode == self.PRIORITY_MODE_GLOBAL_KEY:
|
||||||
# 全局 Key 优先模式:按 global_priority 分组,同组内哈希分散负载均衡
|
# 全局 Key 优先模式:按 global_priority 分组,同组内哈希分散负载均衡
|
||||||
return self._sort_by_global_priority_with_hash(candidates, affinity_key)
|
return self._sort_by_global_priority_with_hash(candidates, affinity_key, api_format)
|
||||||
|
|
||||||
# 提供商优先模式:保持原有顺序(provider_priority 排序已经由查询保证)
|
# 提供商优先模式:保持原有顺序(provider_priority 排序已经由查询保证)
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
def _sort_by_global_priority_with_hash(
|
def _sort_by_global_priority_with_hash(
|
||||||
self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None
|
self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None,
|
||||||
|
api_format: Optional[str] = None
|
||||||
) -> List[ProviderCandidate]:
|
) -> List[ProviderCandidate]:
|
||||||
"""
|
"""
|
||||||
按 global_priority 分组排序,同优先级内通过哈希分散实现负载均衡
|
按 global_priority_by_format 分组排序,同优先级内通过哈希分散实现负载均衡
|
||||||
|
|
||||||
排序逻辑:
|
排序逻辑:
|
||||||
1. 按 global_priority 分组(数字小的优先,NULL 排后面)
|
1. 按 global_priority_by_format[api_format] 分组(数字小的优先,NULL 排后面)
|
||||||
2. 同 global_priority 组内,使用 affinity_key 哈希分散
|
2. 同优先级组内,使用 affinity_key 哈希分散
|
||||||
3. 确保同一用户请求稳定选择同一个 Key(缓存亲和性)
|
3. 确保同一用户请求稳定选择同一个 Key(缓存亲和性)
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
# 按 global_priority 分组
|
def get_priority(candidate: ProviderCandidate) -> int:
|
||||||
|
"""获取候选的优先级"""
|
||||||
|
if not candidate.key:
|
||||||
|
return 999999
|
||||||
|
priority_by_format = candidate.key.global_priority_by_format or {}
|
||||||
|
if api_format and api_format in priority_by_format:
|
||||||
|
return priority_by_format[api_format]
|
||||||
|
return 999999 # NULL 排在后面
|
||||||
|
|
||||||
|
# 按优先级分组
|
||||||
priority_groups: Dict[int, List[ProviderCandidate]] = defaultdict(list)
|
priority_groups: Dict[int, List[ProviderCandidate]] = defaultdict(list)
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
global_priority = (
|
priority = get_priority(candidate)
|
||||||
candidate.key.global_priority
|
priority_groups[priority].append(candidate)
|
||||||
if candidate.key and candidate.key.global_priority is not None
|
|
||||||
else 999999 # NULL 排在后面
|
|
||||||
)
|
|
||||||
priority_groups[global_priority].append(candidate)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for priority in sorted(priority_groups.keys()): # 数字小的优先级高
|
for priority in sorted(priority_groups.keys()): # 数字小的优先级高
|
||||||
@@ -1263,13 +1270,13 @@ class CacheAwareScheduler:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def _apply_load_balance(
|
def _apply_load_balance(
|
||||||
self, candidates: List[ProviderCandidate]
|
self, candidates: List[ProviderCandidate], api_format: Optional[str] = None
|
||||||
) -> List[ProviderCandidate]:
|
) -> List[ProviderCandidate]:
|
||||||
"""
|
"""
|
||||||
负载均衡模式:同优先级内随机轮换
|
负载均衡模式:同优先级内随机轮换
|
||||||
|
|
||||||
排序逻辑:
|
排序逻辑:
|
||||||
1. 按优先级分组(provider_priority, internal_priority 或 global_priority)
|
1. 按优先级分组(provider_priority, internal_priority 或 global_priority_by_format)
|
||||||
2. 同优先级组内随机打乱
|
2. 同优先级组内随机打乱
|
||||||
3. 不考虑缓存亲和性
|
3. 不考虑缓存亲和性
|
||||||
"""
|
"""
|
||||||
@@ -1282,14 +1289,14 @@ class CacheAwareScheduler:
|
|||||||
|
|
||||||
# 根据优先级模式选择分组方式
|
# 根据优先级模式选择分组方式
|
||||||
if self.priority_mode == self.PRIORITY_MODE_GLOBAL_KEY:
|
if self.priority_mode == self.PRIORITY_MODE_GLOBAL_KEY:
|
||||||
# 全局 Key 优先模式:按 global_priority 分组
|
# 全局 Key 优先模式:按格式特定优先级分组
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
global_priority = (
|
priority = 999999
|
||||||
candidate.key.global_priority
|
if candidate.key:
|
||||||
if candidate.key and candidate.key.global_priority is not None
|
priority_by_format = candidate.key.global_priority_by_format or {}
|
||||||
else 999999
|
if api_format and api_format in priority_by_format:
|
||||||
)
|
priority = priority_by_format[api_format]
|
||||||
priority_groups[(global_priority,)].append(candidate)
|
priority_groups[(priority,)].append(candidate)
|
||||||
else:
|
else:
|
||||||
# 提供商优先模式:按 (provider_priority, internal_priority) 分组
|
# 提供商优先模式:按 (provider_priority, internal_priority) 分组
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
|
|||||||
20
src/services/cache/provider_cache.py
vendored
20
src/services/cache/provider_cache.py
vendored
@@ -27,22 +27,15 @@ class ProviderCacheService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compute_rate_multiplier(
|
def compute_rate_multiplier(
|
||||||
rate_multiplier: Optional[float],
|
|
||||||
rate_multipliers: Optional[dict],
|
rate_multipliers: Optional[dict],
|
||||||
api_format: Optional[str] = None,
|
api_format: Optional[str] = None,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
计算 rate_multiplier 的纯函数(无数据库/缓存依赖)
|
计算 rate_multiplier 的纯函数(无数据库/缓存依赖)
|
||||||
|
|
||||||
优先返回指定 API 格式的倍率,如果没有则返回默认倍率。
|
返回指定 API 格式的倍率,如果没有则返回 1.0。
|
||||||
规则:
|
|
||||||
- 如果指定了 api_format 且 rate_multipliers 存在:
|
|
||||||
- 如果 rate_multipliers[api_format] 存在,返回它
|
|
||||||
- 否则返回 1.0(rate_multipliers 存在但该格式未配置)
|
|
||||||
- 否则返回 rate_multiplier 或 1.0
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rate_multiplier: 默认倍率
|
|
||||||
rate_multipliers: 按 API 格式的倍率配置字典
|
rate_multipliers: 按 API 格式的倍率配置字典
|
||||||
api_format: API 格式(可选),如 "CLAUDE"、"OPENAI"
|
api_format: API 格式(可选),如 "CLAUDE"、"OPENAI"
|
||||||
|
|
||||||
@@ -53,12 +46,7 @@ class ProviderCacheService:
|
|||||||
format_upper = api_format.upper()
|
format_upper = api_format.upper()
|
||||||
if format_upper in rate_multipliers:
|
if format_upper in rate_multipliers:
|
||||||
return float(rate_multipliers[format_upper])
|
return float(rate_multipliers[format_upper])
|
||||||
else:
|
return 1.0
|
||||||
# rate_multipliers 存在但该格式未配置,使用默认值 1.0
|
|
||||||
return 1.0
|
|
||||||
else:
|
|
||||||
# rate_multipliers 不存在或未指定 api_format,回退到默认倍率
|
|
||||||
return rate_multiplier or 1.0
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_provider_api_key_rate_multiplier(
|
async def get_provider_api_key_rate_multiplier(
|
||||||
@@ -92,7 +80,7 @@ class ProviderCacheService:
|
|||||||
|
|
||||||
# 2. 缓存未命中,查询数据库
|
# 2. 缓存未命中,查询数据库
|
||||||
provider_key = (
|
provider_key = (
|
||||||
db.query(ProviderAPIKey.rate_multiplier, ProviderAPIKey.rate_multipliers)
|
db.query(ProviderAPIKey.rate_multipliers)
|
||||||
.filter(ProviderAPIKey.id == provider_api_key_id)
|
.filter(ProviderAPIKey.id == provider_api_key_id)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
@@ -100,7 +88,7 @@ class ProviderCacheService:
|
|||||||
# 3. 计算倍率并写入缓存
|
# 3. 计算倍率并写入缓存
|
||||||
if provider_key:
|
if provider_key:
|
||||||
rate_multiplier = ProviderCacheService.compute_rate_multiplier(
|
rate_multiplier = ProviderCacheService.compute_rate_multiplier(
|
||||||
provider_key.rate_multiplier, provider_key.rate_multipliers, api_format
|
provider_key.rate_multipliers, api_format
|
||||||
)
|
)
|
||||||
|
|
||||||
await CacheService.set(
|
await CacheService.set(
|
||||||
|
|||||||
@@ -1567,7 +1567,7 @@ class UsageService:
|
|||||||
from src.services.cache.provider_cache import ProviderCacheService
|
from src.services.cache.provider_cache import ProviderCacheService
|
||||||
|
|
||||||
provider_key = (
|
provider_key = (
|
||||||
db.query(ProviderAPIKey.rate_multiplier, ProviderAPIKey.rate_multipliers)
|
db.query(ProviderAPIKey.rate_multipliers)
|
||||||
.filter(ProviderAPIKey.id == provider_api_key_id)
|
.filter(ProviderAPIKey.id == provider_api_key_id)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
@@ -1576,7 +1576,7 @@ class UsageService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return ProviderCacheService.compute_rate_multiplier(
|
return ProviderCacheService.compute_rate_multiplier(
|
||||||
provider_key.rate_multiplier, provider_key.rate_multipliers, api_format
|
provider_key.rate_multipliers, api_format
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ class TestHelperMethods:
|
|||||||
async def test_get_rate_multiplier_from_provider_api_key(self) -> None:
|
async def test_get_rate_multiplier_from_provider_api_key(self) -> None:
|
||||||
"""测试从 ProviderAPIKey 获取费率倍数"""
|
"""测试从 ProviderAPIKey 获取费率倍数"""
|
||||||
mock_provider_api_key = MagicMock()
|
mock_provider_api_key = MagicMock()
|
||||||
mock_provider_api_key.rate_multiplier = 0.8
|
mock_provider_api_key.rate_multipliers = {"CLAUDE": 0.8}
|
||||||
|
|
||||||
mock_endpoint = MagicMock()
|
mock_endpoint = MagicMock()
|
||||||
mock_endpoint.provider_id = "provider-123"
|
mock_endpoint.provider_id = "provider-123"
|
||||||
@@ -285,7 +285,7 @@ class TestHelperMethods:
|
|||||||
]
|
]
|
||||||
|
|
||||||
rate_multiplier, is_free_tier = await UsageService._get_rate_multiplier_and_free_tier(
|
rate_multiplier, is_free_tier = await UsageService._get_rate_multiplier_and_free_tier(
|
||||||
mock_db, provider_api_key_id="pak-123", provider_id=None
|
mock_db, provider_api_key_id="pak-123", provider_id=None, api_format="CLAUDE"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert rate_multiplier == 0.8
|
assert rate_multiplier == 0.8
|
||||||
|
|||||||
Reference in New Issue
Block a user