mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat(models): 路由预览显示 Key 白名单的模型映射匹配
- 后端返回 GlobalModel 的 model_mappings 正则模式 - 前端用映射模式匹配 Key 白名单,显示匹配的模型名
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user