refactor: 废弃 rate_multiplier 字段,改进成本倍率回退逻辑

- 在前后端模型中为 rate_multiplier 添加 @deprecated 标记
- 修复 rate_multipliers 回退逻辑:当 rate_multipliers 存在但未配置当前格式时使用 1.0
- ModelMappingsTab: 删除映射后自动保存,简化空状态提示
- ModelManagement: 添加 refreshModel 事件在映射更新后刷新模型数据
- CacheAwareScheduler: 添加映射匹配调试日志,移除 candidate_models 限制
This commit is contained in:
fawney19
2026-01-14 21:07:28 +08:00
parent bec9c3a989
commit 0348b7d915
10 changed files with 67 additions and 22 deletions

View File

@@ -86,7 +86,8 @@ export async function addProviderKey(
api_formats: string[] // 支持的 API 格式列表(必填) api_formats: string[] // 支持的 API 格式列表(必填)
api_key: string api_key: string
name: string name: string
rate_multiplier?: number // 默认成本倍率 /** @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 限制(留空=自适应模式)
@@ -111,7 +112,8 @@ export async function updateProviderKey(
api_formats: string[] // 支持的 API 格式列表 api_formats: string[] // 支持的 API 格式列表
api_key: string api_key: string
name: string name: string
rate_multiplier: number // 默认成本倍率 /** @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: number | null

View File

@@ -102,8 +102,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 // 密钥名称(必填,用于识别)
rate_multiplier: number // 默认成本倍率(真实成本 = 表面成本 × 倍率) /** @deprecated 已废弃,请使用 rate_multipliers */
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率,如 {"CLAUDE": 1.0, "OPENAI": 0.8} rate_multiplier: number // [DEPRECATED] 默认成本倍率,已废弃
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?: number | null // 全局 Key 优先级
rpm_limit?: number | null // RPM 速率限制 (1-10000)null 表示自适应模式 rpm_limit?: number | null // RPM 速率限制 (1-10000)null 表示自适应模式
@@ -179,7 +180,8 @@ export interface EndpointAPIKeyUpdate {
api_formats?: string[] // 支持的 API 格式列表 api_formats?: string[] // 支持的 API 格式列表
name?: string name?: string
api_key?: string // 仅在需要更新时提供 api_key?: string // 仅在需要更新时提供
rate_multiplier?: number // 默认成本倍率 /** @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?: number | null

View File

@@ -499,6 +499,7 @@ const emit = defineEmits<{
'editProvider': [provider: any] 'editProvider': [provider: any]
'deleteProvider': [provider: any] 'deleteProvider': [provider: any]
'toggleProviderStatus': [provider: any] 'toggleProviderStatus': [provider: any]
'refreshModel': []
}>() }>()
const { success: showSuccess, error: showError } = useToast() const { success: showSuccess, error: showError } = useToast()
const { copyToClipboard } = useClipboard() const { copyToClipboard } = useClipboard()
@@ -548,6 +549,8 @@ function handleMappingsUpdate(_mappings: string[]) {
// 映射已在 ModelMappingsTab 内部保存到服务器 // 映射已在 ModelMappingsTab 内部保存到服务器
// 刷新路由数据以反映可能的候选变化 // 刷新路由数据以反映可能的候选变化
refreshRoutingData() refreshRoutingData()
// 通知父组件刷新模型数据
emit('refreshModel')
} }
// 暴露刷新方法给父组件 // 暴露刷新方法给父组件

View File

@@ -155,15 +155,12 @@
<!-- 空状态 --> <!-- 空状态 -->
<div <div
v-else v-else
class="text-center py-8" class="text-center py-32"
> >
<GitMerge class="w-10 h-10 mx-auto text-muted-foreground/30 mb-3" /> <GitMerge class="w-10 h-10 mx-auto text-muted-foreground/30 mb-3" />
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
暂无映射规则 暂无映射规则
</p> </p>
<p class="text-xs text-muted-foreground mt-1">
添加映射可匹配 Provider Key 白名单中的模型
</p>
</div> </div>
</Card> </Card>
</template> </template>
@@ -462,14 +459,15 @@ function addMapping() {
expandedIndex.value = localMappings.value.length - 1 expandedIndex.value = localMappings.value.length - 1
} }
function removeMapping(index: number) { async function removeMapping(index: number) {
localMappings.value.splice(index, 1) localMappings.value.splice(index, 1)
isDirty.value = true
if (expandedIndex.value === index) { if (expandedIndex.value === index) {
expandedIndex.value = null expandedIndex.value = null
} else if (expandedIndex.value !== null && expandedIndex.value > index) { } else if (expandedIndex.value !== null && expandedIndex.value > index) {
expandedIndex.value-- expandedIndex.value--
} }
// 删除后自动保存
await saveMappings()
} }
async function saveMappings() { async function saveMappings() {

View File

@@ -1423,6 +1423,11 @@ function getKeyRateMultiplier(key: EndpointAPIKey, format: string): number {
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
// 只有当 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 key.rate_multiplier || 1.0
} }

View File

@@ -430,6 +430,7 @@
@edit-provider="openEditProviderImplementation" @edit-provider="openEditProviderImplementation"
@delete-provider="confirmDeleteProviderImplementation" @delete-provider="confirmDeleteProviderImplementation"
@toggle-provider-status="toggleProviderStatus" @toggle-provider-status="toggleProviderStatus"
@refresh-model="refreshSelectedModel"
/> />
<!-- 批量添加关联提供商对话框 --> <!-- 批量添加关联提供商对话框 -->
@@ -614,6 +615,7 @@ import {
} from '@/components/ui' } from '@/components/ui'
import { import {
listGlobalModels, listGlobalModels,
getGlobalModel,
updateGlobalModel, updateGlobalModel,
deleteGlobalModel, deleteGlobalModel,
batchAssignToProviders, batchAssignToProviders,
@@ -958,6 +960,17 @@ async function selectModel(model: GlobalModelResponse) {
await loadModelProviders(model.id) await loadModelProviders(model.id)
} }
// 刷新当前选中的模型数据
async function refreshSelectedModel() {
if (!selectedModel.value) return
try {
const updated = await getGlobalModel(selectedModel.value.id)
selectedModel.value = updated
} catch (err) {
log.error('刷新模型数据失败:', err)
}
}
// 加载指定模型的关联提供商 // 加载指定模型的关联提供商
async function loadModelProviders(_globalModelId: string) { async function loadModelProviders(_globalModelId: string) {
loadingModelProviders.value = true loadingModelProviders.value = true

View File

@@ -1004,12 +1004,14 @@ 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( rate_multiplier = Column(
Float, default=1.0, nullable=False Float, default=1.0, nullable=False
) # 默认成本倍率(真实成本 = 表面成本 × 倍率) ) # [DEPRECATED] 默认成本倍率,请使用 rate_multipliers
rate_multipliers = Column( rate_multipliers = Column(
JSON, nullable=True JSON, nullable=True
) # 按 API 格式的成本倍率 {"CLAUDE": 1.0, "OPENAI": 0.8} ) # 按 API 格式的成本倍率 {"CLAUDE_CLI": 1.0, "OPENAI_CLI": 0.8}
# 优先级配置 (数字越小越优先) # 优先级配置 (数字越小越优先)
internal_priority = Column( internal_priority = Column(

View File

@@ -132,11 +132,12 @@ 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( rate_multiplier: float = Field(
default=1.0, ge=0.01, description="默认成本倍率(真实成本 = 表面成本 × 倍率)" 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': 1.0, 'OPENAI': 0.8}" default=None, description="按 API 格式的成本倍率,如 {'CLAUDE_CLI': 1.0, 'OPENAI_CLI': 0.8}"
) )
# 优先级和限制(数字越小越优先) # 优先级和限制(数字越小越优先)
@@ -273,9 +274,10 @@ 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="密钥名称")
rate_multiplier: Optional[float] = Field(default=None, ge=0.01, 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': 1.0, 'OPENAI': 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 内部优先级(提供商优先模式,数字越小越优先)"
@@ -399,9 +401,10 @@ class EndpointAPIKeyResponse(BaseModel):
name: str = Field(..., description="密钥名称") name: str = Field(..., description="密钥名称")
# 成本计算 # 成本计算
rate_multiplier: float = Field(default=1.0, 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': 1.0, 'OPENAI': 0.8}" default=None, description="按 API 格式的成本倍率,如 {'CLAUDE_CLI': 1.0, 'OPENAI_CLI': 0.8}"
) )
# 优先级和限制 # 优先级和限制

View File

@@ -593,6 +593,8 @@ class CacheAwareScheduler:
# 提取模型映射(用于 Provider Key 的 allowed_models 匹配) # 提取模型映射(用于 Provider Key 的 allowed_models 匹配)
model_mappings: List[str] = (global_model.config or {}).get("model_mappings", []) model_mappings: List[str] = (global_model.config or {}).get("model_mappings", [])
if model_mappings:
logger.debug(f"[Scheduler] GlobalModel={global_model.name} 配置了映射规则: {model_mappings}")
# 获取合并后的访问限制ApiKey + User # 获取合并后的访问限制ApiKey + User
restrictions = self._get_effective_restrictions(user_api_key) restrictions = self._get_effective_restrictions(user_api_key)
@@ -929,6 +931,12 @@ class CacheAwareScheduler:
model_mappings=model_mappings, model_mappings=model_mappings,
candidate_models=candidate_models, candidate_models=candidate_models,
) )
if mapping_matched_model:
logger.debug(
f"[Scheduler] Key {key.id[:8]}... 映射匹配成功: "
f"model={model_name} -> {mapping_matched_model}, "
f"allowed_models={key.allowed_models}, model_mappings={model_mappings}"
)
except TimeoutError: except TimeoutError:
# 正则匹配超时(可能是 ReDoS 攻击或复杂模式) # 正则匹配超时(可能是 ReDoS 攻击或复杂模式)
logger.warning(f"映射匹配超时: key_id={key.id}, model={model_name}") logger.warning(f"映射匹配超时: key_id={key.id}, model={model_name}")
@@ -1047,6 +1055,9 @@ class CacheAwareScheduler:
for key in keys: for key in keys:
# Key 级别的能力检查 # Key 级别的能力检查
# 注意:不传入 candidate_models 限制,允许映射匹配到 Key 的 allowed_models 中的任意模型名
# 这支持以下场景Key 只允许使用 gpt-5.2,而 GlobalModel 配置了映射 gpt-5.*2
# 映射匹配后,实际请求会使用 gpt-5.2 作为模型名发送给 Provider
is_available, skip_reason, mapping_matched_model = self._check_key_availability( is_available, skip_reason, mapping_matched_model = self._check_key_availability(
key, key,
target_format_str, target_format_str,
@@ -1054,7 +1065,6 @@ class CacheAwareScheduler:
capability_requirements, capability_requirements,
resolved_model_name=resolved_model_name, resolved_model_name=resolved_model_name,
model_mappings=model_mappings, model_mappings=model_mappings,
candidate_models=provider_model_names,
) )
candidate = ProviderCandidate( candidate = ProviderCandidate(

View File

@@ -64,12 +64,19 @@ class ProviderCacheService:
# 3. 计算倍率并写入缓存 # 3. 计算倍率并写入缓存
if provider_key: if provider_key:
# 优先使用 rate_multipliers[api_format],回退到 rate_multiplier # 优先使用 rate_multipliers[api_format]
rate_multiplier = provider_key.rate_multiplier or 1.0 # 如果 rate_multipliers 存在但未配置该格式,默认为 1.0
# 只有当 rate_multipliers 完全不存在时,才回退到 rate_multiplier
if api_format and provider_key.rate_multipliers: if api_format and provider_key.rate_multipliers:
format_upper = api_format.upper() format_upper = api_format.upper()
if format_upper in provider_key.rate_multipliers: if format_upper in provider_key.rate_multipliers:
rate_multiplier = provider_key.rate_multipliers[format_upper] rate_multiplier = provider_key.rate_multipliers[format_upper]
else:
# rate_multipliers 存在但该格式未配置,使用默认值 1.0
rate_multiplier = 1.0
else:
# rate_multipliers 不存在或未指定 api_format回退到默认倍率
rate_multiplier = provider_key.rate_multiplier or 1.0
await CacheService.set( await CacheService.set(
cache_key, rate_multiplier, ttl_seconds=ProviderCacheService.CACHE_TTL cache_key, rate_multiplier, ttl_seconds=ProviderCacheService.CACHE_TTL