mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(models): 模型映射支持按提供商分组并自动关联
- 映射匹配结果按提供商分组显示,支持单个关联 - 保存映射规则时自动批量关联未关联的提供商 - 优化模型详情加载逻辑,并行获取最新数据 - 移除冗余的调度器日志输出
This commit is contained in:
@@ -437,10 +437,13 @@
|
|||||||
<div v-show="detailTab === 'mappings'">
|
<div v-show="detailTab === 'mappings'">
|
||||||
<ModelMappingsTab
|
<ModelMappingsTab
|
||||||
v-if="model"
|
v-if="model"
|
||||||
|
ref="modelMappingsTabRef"
|
||||||
:global-model-id="model.id"
|
:global-model-id="model.id"
|
||||||
:model-name="model.name"
|
:model-name="model.name"
|
||||||
:mappings="model.config?.model_mappings || []"
|
:mappings="model.config?.model_mappings || []"
|
||||||
@update="handleMappingsUpdate"
|
@update="handleMappingsUpdate"
|
||||||
|
@link-provider="(providerId) => $emit('linkProvider', providerId)"
|
||||||
|
@link-providers="(providerIds) => $emit('linkProviders', providerIds)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -500,6 +503,8 @@ const emit = defineEmits<{
|
|||||||
'deleteProvider': [provider: any]
|
'deleteProvider': [provider: any]
|
||||||
'toggleProviderStatus': [provider: any]
|
'toggleProviderStatus': [provider: any]
|
||||||
'refreshModel': []
|
'refreshModel': []
|
||||||
|
'linkProvider': [providerId: string]
|
||||||
|
'linkProviders': [providerIds: string[]]
|
||||||
}>()
|
}>()
|
||||||
const { success: showSuccess, error: showError } = useToast()
|
const { success: showSuccess, error: showError } = useToast()
|
||||||
const { copyToClipboard } = useClipboard()
|
const { copyToClipboard } = useClipboard()
|
||||||
@@ -513,6 +518,8 @@ interface Props {
|
|||||||
|
|
||||||
// RoutingTab 引用
|
// RoutingTab 引用
|
||||||
const routingTabRef = ref<InstanceType<typeof RoutingTab> | null>(null)
|
const routingTabRef = ref<InstanceType<typeof RoutingTab> | null>(null)
|
||||||
|
// ModelMappingsTab 引用
|
||||||
|
const modelMappingsTabRef = ref<InstanceType<typeof ModelMappingsTab> | null>(null)
|
||||||
|
|
||||||
// 将 RoutingProviderInfo 转换为父组件期望的格式
|
// 将 RoutingProviderInfo 转换为父组件期望的格式
|
||||||
function convertRoutingProviderToLegacyFormat(provider: RoutingProviderInfo) {
|
function convertRoutingProviderToLegacyFormat(provider: RoutingProviderInfo) {
|
||||||
@@ -542,6 +549,7 @@ function handleDeleteProviderFromRouting(provider: RoutingProviderInfo) {
|
|||||||
// 刷新路由数据
|
// 刷新路由数据
|
||||||
function refreshRoutingData() {
|
function refreshRoutingData() {
|
||||||
routingTabRef.value?.loadRoutingData?.()
|
routingTabRef.value?.loadRoutingData?.()
|
||||||
|
modelMappingsTabRef.value?.refresh?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理模型映射更新
|
// 处理模型映射更新
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 展开内容:匹配的 Key 列表 -->
|
<!-- 展开内容:匹配的 Key 列表(按提供商分组) -->
|
||||||
<div
|
<div
|
||||||
v-if="expandedIndex === index"
|
v-if="expandedIndex === index"
|
||||||
class="border-t bg-muted/10 px-4 py-3"
|
class="border-t bg-muted/10 px-4 py-3"
|
||||||
@@ -117,34 +117,65 @@
|
|||||||
<RefreshCw class="w-4 h-4 animate-spin text-muted-foreground" />
|
<RefreshCw class="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="getMatchedKeysForMapping(mapping).length === 0" class="text-center py-4">
|
<div v-else-if="getMatchedKeysGroupedByProvider(mapping).length === 0" class="text-center py-4">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{{ mapping.trim() ? '此规则暂无匹配的 Key 白名单' : '请输入映射规则' }}
|
{{ mapping.trim() ? '此规则暂无匹配的 Key 白名单' : '请输入映射规则' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-2">
|
<div v-else class="space-y-3">
|
||||||
|
<!-- 按提供商分组 -->
|
||||||
<div
|
<div
|
||||||
v-for="item in getMatchedKeysForMapping(mapping)"
|
v-for="group in getMatchedKeysGroupedByProvider(mapping)"
|
||||||
:key="item.keyId"
|
:key="group.providerId"
|
||||||
class="bg-background rounded-md border p-3"
|
class="bg-background rounded-md border overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1.5 text-sm mb-2">
|
<!-- 提供商标题 -->
|
||||||
<span class="text-muted-foreground">{{ item.providerName }}</span>
|
<div class="px-3 py-2 bg-muted/30 border-b flex items-center justify-between">
|
||||||
<span class="text-muted-foreground">/</span>
|
<div>
|
||||||
<span class="font-medium">{{ item.keyName }}</span>
|
<span class="text-sm font-medium">{{ group.providerName }}</span>
|
||||||
<span class="text-muted-foreground">·</span>
|
<span class="text-xs text-muted-foreground ml-2">({{ group.keys.length }} Key)</span>
|
||||||
<code class="text-xs text-muted-foreground/70">{{ item.maskedKey }}</code>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-1">
|
|
||||||
<Badge
|
<Badge
|
||||||
v-for="model in item.matchedModels"
|
v-if="group.isLinked"
|
||||||
:key="model"
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="text-xs font-mono"
|
class="text-xs"
|
||||||
>
|
>
|
||||||
{{ model }}
|
已关联
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
title="关联到当前模型"
|
||||||
|
@click="$emit('linkProvider', group.providerId)"
|
||||||
|
>
|
||||||
|
<Link class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<!-- Key 列表 -->
|
||||||
|
<div class="divide-y divide-border/50">
|
||||||
|
<div
|
||||||
|
v-for="keyItem in group.keys"
|
||||||
|
:key="keyItem.keyId"
|
||||||
|
class="px-3 py-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1.5 text-sm mb-1.5">
|
||||||
|
<span class="font-medium">{{ keyItem.keyName }}</span>
|
||||||
|
<code class="text-xs text-muted-foreground/70">{{ keyItem.maskedKey }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<Badge
|
||||||
|
v-for="model in keyItem.matchedModels"
|
||||||
|
:key="model"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs font-mono"
|
||||||
|
>
|
||||||
|
{{ model }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +199,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
|
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
|
||||||
import { Card, Button, Input, Badge } from '@/components/ui'
|
import { Card, Button, Input, Badge } from '@/components/ui'
|
||||||
import { Plus, Trash2, GitMerge, RefreshCw, ChevronRight, Save, AlertCircle } from 'lucide-vue-next'
|
import { Plus, Trash2, GitMerge, RefreshCw, ChevronRight, Save, AlertCircle, Link } from 'lucide-vue-next'
|
||||||
import { updateGlobalModel, getGlobalModel, getGlobalModelRoutingPreview } from '@/api/global-models'
|
import { updateGlobalModel, getGlobalModel, getGlobalModelRoutingPreview } from '@/api/global-models'
|
||||||
import type { ModelRoutingPreviewResponse } from '@/api/endpoints/types'
|
import type { ModelRoutingPreviewResponse } from '@/api/endpoints/types'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
@@ -183,6 +214,8 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
update: [mappings: string[]]
|
update: [mappings: string[]]
|
||||||
refresh: []
|
refresh: []
|
||||||
|
linkProvider: [providerId: string]
|
||||||
|
linkProviders: [providerIds: string[]] // 批量关联
|
||||||
}>()
|
}>()
|
||||||
// 安全限制常量(与后端保持一致)
|
// 安全限制常量(与后端保持一致)
|
||||||
const MAX_MAPPINGS_PER_MODEL = 50
|
const MAX_MAPPINGS_PER_MODEL = 50
|
||||||
@@ -279,9 +312,17 @@ interface MatchedKeyForMapping {
|
|||||||
keyName: string
|
keyName: string
|
||||||
maskedKey: string
|
maskedKey: string
|
||||||
providerName: string
|
providerName: string
|
||||||
|
providerId: string
|
||||||
matchedModels: string[]
|
matchedModels: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProviderGroup {
|
||||||
|
providerId: string
|
||||||
|
providerName: string
|
||||||
|
keys: MatchedKeyForMapping[]
|
||||||
|
isLinked: boolean // 是否已关联到当前模型
|
||||||
|
}
|
||||||
|
|
||||||
interface ValidationResult {
|
interface ValidationResult {
|
||||||
valid: boolean
|
valid: boolean
|
||||||
error?: string
|
error?: string
|
||||||
@@ -410,6 +451,7 @@ function getMatchedKeysForMapping(mapping: string): MatchedKeyForMapping[] {
|
|||||||
keyName: keyItem.key_name,
|
keyName: keyItem.key_name,
|
||||||
maskedKey: keyItem.masked_key,
|
maskedKey: keyItem.masked_key,
|
||||||
providerName: keyItem.provider_name,
|
providerName: keyItem.provider_name,
|
||||||
|
providerId: keyItem.provider_id,
|
||||||
matchedModels,
|
matchedModels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -419,6 +461,33 @@ function getMatchedKeysForMapping(mapping: string): MatchedKeyForMapping[] {
|
|||||||
return Array.from(keyMap.values())
|
return Array.from(keyMap.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 按提供商分组匹配的 Key
|
||||||
|
function getMatchedKeysGroupedByProvider(mapping: string): ProviderGroup[] {
|
||||||
|
const keys = getMatchedKeysForMapping(mapping)
|
||||||
|
const providerMap = new Map<string, ProviderGroup>()
|
||||||
|
|
||||||
|
// 获取已关联的提供商 ID 集合
|
||||||
|
const linkedProviderIds = new Set(
|
||||||
|
(routingData.value?.providers || []).map(p => p.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const existing = providerMap.get(key.providerId)
|
||||||
|
if (existing) {
|
||||||
|
existing.keys.push(key)
|
||||||
|
} else {
|
||||||
|
providerMap.set(key.providerId, {
|
||||||
|
providerId: key.providerId,
|
||||||
|
providerName: key.providerName,
|
||||||
|
keys: [key],
|
||||||
|
isLinked: linkedProviderIds.has(key.providerId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(providerMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
// 获取指定映射的匹配数量
|
// 获取指定映射的匹配数量
|
||||||
function getMatchCount(mapping: string): number {
|
function getMatchCount(mapping: string): number {
|
||||||
return getMatchedKeysForMapping(mapping).reduce((sum, item) => sum + item.matchedModels.length, 0)
|
return getMatchedKeysForMapping(mapping).reduce((sum, item) => sum + item.matchedModels.length, 0)
|
||||||
@@ -492,8 +561,26 @@ async function saveMappings() {
|
|||||||
originalMappings.value = [...cleanedMappings] // 更新原始值
|
originalMappings.value = [...cleanedMappings] // 更新原始值
|
||||||
isDirty.value = false
|
isDirty.value = false
|
||||||
|
|
||||||
toastSuccess('映射规则已保存')
|
// 收集所有未关联的提供商 ID
|
||||||
emit('update', cleanedMappings)
|
const unlinkedProviderIds: string[] = []
|
||||||
|
for (const mapping of cleanedMappings) {
|
||||||
|
const groups = getMatchedKeysGroupedByProvider(mapping)
|
||||||
|
for (const group of groups) {
|
||||||
|
if (!group.isLinked && !unlinkedProviderIds.includes(group.providerId)) {
|
||||||
|
unlinkedProviderIds.push(group.providerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动关联未关联的提供商
|
||||||
|
if (unlinkedProviderIds.length > 0) {
|
||||||
|
toastSuccess(`映射规则已保存,正在关联 ${unlinkedProviderIds.length} 个提供商...`)
|
||||||
|
// linkProviders 处理完成后会由父组件统一刷新数据,无需再 emit update
|
||||||
|
emit('linkProviders', unlinkedProviderIds)
|
||||||
|
} else {
|
||||||
|
toastSuccess('映射规则已保存')
|
||||||
|
emit('update', cleanedMappings)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('保存映射规则失败:', err)
|
log.error('保存映射规则失败:', err)
|
||||||
toastError('保存失败,请重试')
|
toastError('保存失败,请重试')
|
||||||
@@ -526,4 +613,9 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
regexCache.clear()
|
regexCache.clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 暴露刷新方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
refresh: loadMatchPreview
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -431,6 +431,8 @@
|
|||||||
@delete-provider="confirmDeleteProviderImplementation"
|
@delete-provider="confirmDeleteProviderImplementation"
|
||||||
@toggle-provider-status="toggleProviderStatus"
|
@toggle-provider-status="toggleProviderStatus"
|
||||||
@refresh-model="refreshSelectedModel"
|
@refresh-model="refreshSelectedModel"
|
||||||
|
@link-provider="linkProviderToModel"
|
||||||
|
@link-providers="linkProvidersToModel"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 批量添加关联提供商对话框 -->
|
<!-- 批量添加关联提供商对话框 -->
|
||||||
@@ -953,11 +955,23 @@ function handleRowClick(event: MouseEvent, model: GlobalModelResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function selectModel(model: GlobalModelResponse) {
|
async function selectModel(model: GlobalModelResponse) {
|
||||||
|
// 先显示缓存数据,提升响应速度
|
||||||
selectedModel.value = model
|
selectedModel.value = model
|
||||||
detailTab.value = 'basic'
|
detailTab.value = 'basic'
|
||||||
|
|
||||||
// 加载该模型的关联提供商
|
// 并行加载最新模型数据和关联提供商
|
||||||
await loadModelProviders(model.id)
|
const [latestModel] = await Promise.all([
|
||||||
|
getGlobalModel(model.id).catch(err => {
|
||||||
|
log.error('获取最新模型数据失败:', err)
|
||||||
|
return null
|
||||||
|
}),
|
||||||
|
loadModelProviders(model.id)
|
||||||
|
])
|
||||||
|
|
||||||
|
// 更新为最新数据(如果获取成功)
|
||||||
|
if (latestModel) {
|
||||||
|
selectedModel.value = latestModel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新当前选中的模型数据
|
// 刷新当前选中的模型数据
|
||||||
@@ -1036,6 +1050,37 @@ function openAddProviderDialog() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关联指定提供商到当前模型
|
||||||
|
async function linkProviderToModel(providerId: string) {
|
||||||
|
await linkProvidersToModel([providerId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量关联提供商到当前模型
|
||||||
|
async function linkProvidersToModel(providerIds: string[]) {
|
||||||
|
if (!selectedModel.value || providerIds.length === 0) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await batchAssignToProviders(selectedModel.value.id, {
|
||||||
|
provider_ids: providerIds,
|
||||||
|
create_models: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示关联结果
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
showError(`${result.errors.length} 个提供商关联失败`, '部分失败')
|
||||||
|
} else {
|
||||||
|
success(`${providerIds.length} 个提供商已关联`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
await loadModelProviders(selectedModel.value.id)
|
||||||
|
await loadGlobalModels()
|
||||||
|
modelDetailDrawerRef.value?.refreshRoutingData?.()
|
||||||
|
} catch (err: any) {
|
||||||
|
showError(parseApiError(err, '关联失败'), '错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理批量添加 Provider 对话框关闭事件
|
// 处理批量添加 Provider 对话框关闭事件
|
||||||
function handleBatchAddProvidersDialogUpdate(value: boolean) {
|
function handleBatchAddProvidersDialogUpdate(value: boolean) {
|
||||||
// 只有在不处于提交状态时才允许关闭
|
// 只有在不处于提交状态时才允许关闭
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ def check_model_allowed_with_mappings(
|
|||||||
|
|
||||||
# 获取 allowed_models 的集合
|
# 获取 allowed_models 的集合
|
||||||
allowed_set = normalize_allowed_models(allowed_models)
|
allowed_set = normalize_allowed_models(allowed_models)
|
||||||
|
|
||||||
if allowed_set is None:
|
if allowed_set is None:
|
||||||
# 不限制,已在 check_model_allowed 中返回 True
|
# 不限制,已在 check_model_allowed 中返回 True
|
||||||
return True, None
|
return True, None
|
||||||
|
|||||||
5
src/services/cache/aware_scheduler.py
vendored
5
src/services/cache/aware_scheduler.py
vendored
@@ -830,11 +830,6 @@ class CacheAwareScheduler:
|
|||||||
# 查询该 Provider 是否有实现这个 GlobalModel
|
# 查询该 Provider 是否有实现这个 GlobalModel
|
||||||
for model in provider.models:
|
for model in provider.models:
|
||||||
if model.global_model_id == global_model.id and model.is_active:
|
if model.global_model_id == global_model.id and model.is_active:
|
||||||
logger.debug(
|
|
||||||
f"[_check_model_support_for_global_model] Provider={provider.name}, "
|
|
||||||
f"GlobalModel={global_model.name}, "
|
|
||||||
f"provider_model_name={model.provider_model_name}"
|
|
||||||
)
|
|
||||||
# 检查流式支持
|
# 检查流式支持
|
||||||
if is_stream:
|
if is_stream:
|
||||||
supports_streaming = model.get_effective_supports_streaming()
|
supports_streaming = model.get_effective_supports_streaming()
|
||||||
|
|||||||
Reference in New Issue
Block a user