feat(models): 路由预览显示 Key 白名单的模型映射匹配

- 后端返回 GlobalModel 的 model_mappings 正则模式
- 前端用映射模式匹配 Key 白名单,显示匹配的模型名
This commit is contained in:
fawney19
2026-01-15 02:36:00 +08:00
parent 4835ca13f5
commit 5d34426428
3 changed files with 89 additions and 5 deletions

View File

@@ -707,6 +707,7 @@ export interface ModelRoutingPreviewResponse {
global_model_name: string global_model_name: string
display_name: string display_name: string
is_active: boolean is_active: boolean
global_model_mappings: string[] // GlobalModel 的模型映射规则(正则模式)
providers: RoutingProviderInfo[] providers: RoutingProviderInfo[]
total_providers: number total_providers: number
active_providers: number active_providers: number

View File

@@ -174,11 +174,24 @@
> >
{{ keyEntry.key.name }} {{ keyEntry.key.name }}
</div> </div>
<!-- 第二行提供商名 · sk --> <!-- 第二行提供商名 · sk · 模型映射 -->
<div class="flex items-center gap-1 text-[10px] text-muted-foreground"> <div class="flex items-center gap-1 text-[10px] text-muted-foreground">
<span>{{ keyEntry.provider.name }}</span> <span>{{ keyEntry.provider.name }}</span>
<span>·</span> <span>·</span>
<code class="text-muted-foreground/60">{{ keyEntry.key.masked_key }}</code> <code class="text-muted-foreground/60">{{ keyEntry.key.masked_key }}</code>
<!-- Key 白名单映射显示 -->
<template v-if="getKeyMatchedModels(keyEntry.key).length > 0">
<span>·</span>
<span
class="text-primary/70"
:title="getKeyMatchedModels(keyEntry.key).join(', ')"
>{{ formatMatchedModels(getKeyMatchedModels(keyEntry.key)) }}</span>
</template>
<!-- Provider 模型映射显示 -->
<template v-else-if="hasModelMapping(keyEntry.provider)">
<span>·</span>
<span class="text-primary/70">{{ keyEntry.provider.provider_model_name }}</span>
</template>
</div> </div>
</div> </div>
@@ -374,6 +387,14 @@
v-if="isProviderInFormatExpanded(formatGroup.api_format, providerEntry.provider.id, providerEntry.endpoint?.id)" v-if="isProviderInFormatExpanded(formatGroup.api_format, providerEntry.provider.id, providerEntry.endpoint?.id)"
class="border-t border-border/30 p-2.5" class="border-t border-border/30 p-2.5"
> >
<!-- 模型映射显示 -->
<div
v-if="hasModelMapping(providerEntry.provider)"
class="flex items-center gap-1.5 text-[10px] text-muted-foreground mb-2 px-1"
>
<span class="text-muted-foreground/60">映射:</span>
<span class="text-primary/70 font-medium">{{ providerEntry.provider.provider_model_name }}</span>
</div>
<!-- Keys 列表 --> <!-- Keys 列表 -->
<div <div
v-if="providerEntry.keys.length > 0" v-if="providerEntry.keys.length > 0"
@@ -430,7 +451,7 @@
]" ]"
:title="getKeyTooltip(key)" :title="getKeyTooltip(key)"
> >
<!-- 名称 + sk垂直堆叠 --> <!-- 名称 + sk + 映射垂直堆叠 -->
<div class="min-w-0 flex flex-col"> <div class="min-w-0 flex flex-col">
<span <span
class="font-medium truncate" class="font-medium truncate"
@@ -438,9 +459,18 @@
> >
{{ key.name }} {{ key.name }}
</span> </span>
<code class="font-mono text-[10px] text-muted-foreground/60"> <div class="flex items-center gap-1">
{{ key.masked_key }} <code class="font-mono text-[10px] text-muted-foreground/60">
</code> {{ key.masked_key }}
</code>
<template v-if="getKeyMatchedModels(key).length > 0">
<span class="text-[10px] text-muted-foreground/40">·</span>
<span
class="text-[10px] text-primary/70"
:title="getKeyMatchedModels(key).join(', ')"
>{{ formatMatchedModels(getKeyMatchedModels(key)) }}</span>
</template>
</div>
</div> </div>
<span class="flex-1" /> <span class="flex-1" />
<!-- 熔断徽章带倒计时- 靠右 --> <!-- 熔断徽章带倒计时- 靠右 -->
@@ -816,6 +846,47 @@ function hasModelMapping(provider: RoutingProviderInfo): boolean {
return provider.provider_model_name !== routingData.value?.global_model_name return provider.provider_model_name !== routingData.value?.global_model_name
} }
// 获取 Key 的 allowed_models 中匹配当前 GlobalModel 的所有模型名
// 逻辑:用 GlobalModel 的 model_mappings正则模式去匹配 Key 的 allowed_models 中的值
function getKeyMatchedModels(key: RoutingKeyInfo): string[] {
if (!key.allowed_models || key.allowed_models.length === 0) {
return []
}
const globalModelName = routingData.value?.global_model_name
const globalModelMappings = routingData.value?.global_model_mappings || []
if (!globalModelName) {
return []
}
const matched: string[] = []
// 遍历 Key 的白名单
for (const allowedModel of key.allowed_models) {
// 如果完全匹配 GlobalModel 名称,跳过(不需要显示)
if (allowedModel === globalModelName) {
continue
}
// 用 GlobalModel 的映射模式匹配白名单中的模型名
for (const pattern of globalModelMappings) {
try {
if (new RegExp(`^${pattern}$`, 'i').test(allowedModel)) {
matched.push(allowedModel)
break // 该 allowedModel 已匹配,不需要继续检查其他 pattern
}
} catch {
// 正则无效,跳过
}
}
}
return matched
}
// 格式化匹配的模型显示文本
function formatMatchedModels(models: string[]): string {
if (models.length === 0) return ''
if (models.length === 1) return models[0]
return `${models[0]} +${models.length - 1}`
}
// 按优先级分组 Keys提供商优先模式下使用 // 按优先级分组 Keys提供商优先模式下使用
interface KeyPriorityGroup { interface KeyPriorityGroup {
priority: number | null priority: number | null

View File

@@ -132,6 +132,10 @@ class ModelRoutingPreviewResponse(BaseModel):
global_model_name: str global_model_name: str
display_name: str display_name: str
is_active: bool is_active: bool
# GlobalModel 的模型映射(用于前端匹配 Key 白名单)
global_model_mappings: List[str] = Field(
default_factory=list, description="GlobalModel 的模型映射规则(正则模式)"
)
# 链路信息 # 链路信息
providers: List[RoutingProviderInfo] = Field( providers: List[RoutingProviderInfo] = Field(
default_factory=list, description="按优先级排序的提供商列表" default_factory=list, description="按优先级排序的提供商列表"
@@ -465,11 +469,19 @@ class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
) )
) )
# 从 GlobalModel.config 中提取 model_mappings
global_model_mappings: List[str] = []
if global_model.config and isinstance(global_model.config, dict):
mappings = global_model.config.get("model_mappings")
if isinstance(mappings, list):
global_model_mappings = [m for m in mappings if isinstance(m, str)]
return ModelRoutingPreviewResponse( return ModelRoutingPreviewResponse(
global_model_id=global_model.id, global_model_id=global_model.id,
global_model_name=global_model.name, global_model_name=global_model.name,
display_name=global_model.display_name, display_name=global_model.display_name,
is_active=bool(global_model.is_active), is_active=bool(global_model.is_active),
global_model_mappings=global_model_mappings,
providers=provider_infos, providers=provider_infos,
total_providers=len(provider_infos), total_providers=len(provider_infos),
active_providers=active_providers, active_providers=active_providers,