diff --git a/frontend/src/api/endpoints/global-models.ts b/frontend/src/api/endpoints/global-models.ts
index d437ff3..ba0d4bf 100644
--- a/frontend/src/api/endpoints/global-models.ts
+++ b/frontend/src/api/endpoints/global-models.ts
@@ -6,6 +6,16 @@ import type {
GlobalModelWithStats,
GlobalModelListResponse,
ModelCatalogProviderDetail,
+ ModelRoutingPreviewResponse,
+} from './types'
+
+// 重新导出路由相关类型供外部使用
+export type {
+ RoutingKeyInfo,
+ RoutingEndpointInfo,
+ RoutingModelMapping,
+ RoutingProviderInfo,
+ ModelRoutingPreviewResponse,
} from './types'
/**
@@ -97,3 +107,15 @@ export async function getGlobalModelProviders(globalModelId: string): Promise<{
)
return response.data
}
+
+/**
+ * 获取 GlobalModel 的请求链路预览
+ */
+export async function getGlobalModelRoutingPreview(
+ globalModelId: string
+): Promise {
+ const response = await client.get(
+ `/api/admin/models/global/${globalModelId}/routing`
+ )
+ return response.data
+}
diff --git a/frontend/src/api/endpoints/types.ts b/frontend/src/api/endpoints/types.ts
index 0200809..c318766 100644
--- a/frontend/src/api/endpoints/types.ts
+++ b/frontend/src/api/endpoints/types.ts
@@ -622,3 +622,83 @@ export interface ImportFromUpstreamResponse {
success: ImportFromUpstreamSuccessItem[]
errors: ImportFromUpstreamErrorItem[]
}
+
+// ========== 路由预览相关类型 ==========
+
+/**
+ * Key 路由信息
+ */
+export interface RoutingKeyInfo {
+ id: string
+ name: string
+ masked_key: string
+ internal_priority: number
+ global_priority?: number | null
+ rpm_limit?: number | null
+ is_adaptive: boolean
+ effective_rpm?: number | null
+ cache_ttl_minutes: number
+ health_score: number
+ is_active: boolean
+ api_formats: string[]
+ circuit_breaker_open: boolean
+ circuit_breaker_formats: string[]
+}
+
+/**
+ * Endpoint 路由信息
+ */
+export interface RoutingEndpointInfo {
+ id: string
+ api_format: string
+ base_url: string
+ custom_path?: string | null
+ is_active: boolean
+ keys: RoutingKeyInfo[]
+ total_keys: number
+ active_keys: number
+}
+
+/**
+ * 模型名称映射信息
+ */
+export interface RoutingModelMapping {
+ name: string
+ priority: number
+ api_formats?: string[] | null
+}
+
+/**
+ * Provider 路由信息
+ */
+export interface RoutingProviderInfo {
+ id: string
+ name: string
+ model_id: string
+ provider_priority: number
+ billing_type?: string | null
+ monthly_quota_usd?: number | null
+ monthly_used_usd?: number | null
+ is_active: boolean
+ provider_model_name: string
+ model_mappings: RoutingModelMapping[]
+ model_is_active: boolean
+ endpoints: RoutingEndpointInfo[]
+ total_endpoints: number
+ active_endpoints: number
+}
+
+/**
+ * 模型请求链路预览响应
+ */
+export interface ModelRoutingPreviewResponse {
+ global_model_id: string
+ global_model_name: string
+ display_name: string
+ is_active: boolean
+ providers: RoutingProviderInfo[]
+ total_providers: number
+ active_providers: number
+ scheduling_mode: string
+ priority_mode: string
+}
diff --git a/frontend/src/api/global-models.ts b/frontend/src/api/global-models.ts
index 42884a9..1c92a3c 100644
--- a/frontend/src/api/global-models.ts
+++ b/frontend/src/api/global-models.ts
@@ -21,4 +21,5 @@ export {
deleteGlobalModel,
batchAssignToProviders,
getGlobalModelProviders,
+ getGlobalModelRoutingPreview,
} from './endpoints/global-models'
diff --git a/frontend/src/components/ui/collapsible-trigger.vue b/frontend/src/components/ui/collapsible-trigger.vue
index b12d852..27fca42 100644
--- a/frontend/src/components/ui/collapsible-trigger.vue
+++ b/frontend/src/components/ui/collapsible-trigger.vue
@@ -5,7 +5,10 @@ const props = defineProps()
-
+
diff --git a/frontend/src/features/models/components/ModelDetailDrawer.vue b/frontend/src/features/models/components/ModelDetailDrawer.vue
index abd5f6a..666e555 100644
--- a/frontend/src/features/models/components/ModelDetailDrawer.vue
+++ b/frontend/src/features/models/components/ModelDetailDrawer.vue
@@ -95,14 +95,14 @@
type="button"
class="flex-1 px-2 sm:px-4 py-2 text-xs sm:text-sm font-medium rounded-md transition-all duration-200"
:class="[
- detailTab === 'providers'
+ detailTab === 'routing'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
]"
- @click="detailTab = 'providers'"
+ @click="detailTab = 'routing'"
>
- 关联提供商
- 提供商
+ 链路控制
+ 链路
@@ -407,269 +407,17 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Provider
-
-
- 能力
-
-
- 价格 ($/M)
-
-
- 操作
-
-
-
-
-
-
-
-
- {{ provider.name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- 输入/输出:
- ${{ (provider.input_price_per_1m || 0).toFixed(1) }}/${{ (provider.output_price_per_1m || 0).toFixed(1) }}
-
- [阶梯]
-
-
-
- 缓存:
- ${{ (provider.cache_creation_price_per_1m || 0).toFixed(2) }}/${{ (provider.cache_read_price_per_1m || 0).toFixed(2) }}
-
-
-
- 1h 缓存:
- ${{ (provider.cache_1h_creation_price_per_1m || 0).toFixed(2) }}
-
-
-
- 按次:
- ${{ (provider.price_per_request || 0).toFixed(3) }}/次
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ provider.name }}
-
-
-
-
-
-
-
-
-
-
- ${{ (provider.input_price_per_1m || 0).toFixed(1) }}/${{ (provider.output_price_per_1m || 0).toFixed(1) }}
-
-
-
-
-
-
-
-
-
-
- 暂无关联提供商
-
-
-
-
+
+
+
@@ -688,12 +436,8 @@ import {
Zap,
Image,
Building2,
- Plus,
Edit,
- Trash2,
Power,
- Loader2,
- RefreshCw,
Copy,
Layers,
BarChart3
@@ -711,14 +455,15 @@ import TableBody from '@/components/ui/table-body.vue'
import TableRow from '@/components/ui/table-row.vue'
import TableHead from '@/components/ui/table-head.vue'
import TableCell from '@/components/ui/table-cell.vue'
+import RoutingTab from './RoutingTab.vue'
// 使用外部类型定义
import type { GlobalModelResponse } from '@/api/global-models'
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
import type { CapabilityDefinition } from '@/api/endpoints'
+import type { RoutingProviderInfo } from '@/api/global-models'
const props = withDefaults(defineProps(), {
- loadingProviders: false,
hasBlockingDialogOpen: false,
})
const emit = defineEmits<{
@@ -729,7 +474,6 @@ const emit = defineEmits<{
'editProvider': [provider: any]
'deleteProvider': [provider: any]
'toggleProviderStatus': [provider: any]
- 'refreshProviders': []
}>()
const { success: showSuccess, error: showError } = useToast()
const { copyToClipboard } = useClipboard()
@@ -737,12 +481,48 @@ const { copyToClipboard } = useClipboard()
interface Props {
model: GlobalModelResponse | null
open: boolean
- providers: any[]
- loadingProviders?: boolean
hasBlockingDialogOpen?: boolean
capabilities?: CapabilityDefinition[]
}
+// RoutingTab 引用
+const routingTabRef = ref | null>(null)
+
+// 将 RoutingProviderInfo 转换为父组件期望的格式
+function convertRoutingProviderToLegacyFormat(provider: RoutingProviderInfo) {
+ return {
+ id: provider.id,
+ model_id: provider.model_id,
+ name: provider.name,
+ is_active: provider.model_is_active
+ }
+}
+
+// 处理从 RoutingTab 来的编辑事件
+function handleEditProviderFromRouting(provider: RoutingProviderInfo) {
+ emit('editProvider', convertRoutingProviderToLegacyFormat(provider))
+}
+
+// 处理从 RoutingTab 来的状态切换事件
+function handleToggleProviderFromRouting(provider: RoutingProviderInfo) {
+ emit('toggleProviderStatus', convertRoutingProviderToLegacyFormat(provider))
+}
+
+// 处理从 RoutingTab 来的删除事件
+function handleDeleteProviderFromRouting(provider: RoutingProviderInfo) {
+ emit('deleteProvider', convertRoutingProviderToLegacyFormat(provider))
+}
+
+// 刷新路由数据
+function refreshRoutingData() {
+ routingTabRef.value?.loadRoutingData?.()
+}
+
+// 暴露刷新方法给父组件
+defineExpose({
+ refreshRoutingData
+})
+
// 根据能力名称获取显示名称
function getCapabilityDisplayName(capName: string): string {
const cap = props.capabilities?.find(c => c.name === capName)
diff --git a/frontend/src/features/models/components/RoutingTab.vue b/frontend/src/features/models/components/RoutingTab.vue
new file mode 100644
index 0000000..ce948f4
--- /dev/null
+++ b/frontend/src/features/models/components/RoutingTab.vue
@@ -0,0 +1,1014 @@
+
+
+
+
+
+
+
+ 请求链路预览
+
+
+ ·
+
+ {{ getSchedulingModeLabel(routingData.scheduling_mode) }}
+
+ ·
+
+ {{ getPriorityModeLabel(routingData.priority_mode) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无关联提供商
+
+
+ 请先为此模型添加提供商关联
+
+
+
+
+
+
+
+
+
+ {{ formatGroup.api_format }}
+
+
+ {{ formatGroup.active_keys }}/{{ formatGroup.total_keys }} Keys
+
+ ·
+
+ {{ formatGroup.active_providers }}/{{ formatGroup.total_providers }} 提供商
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 负载均衡
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 首选
+ P{{ keyGroup.priority ?? '?' }}
+
+
+
+
+
+
+
+
+
+ {{ keyEntry.key.name }}
+
+
+ {{ keyEntry.key.masked_key }}
+
+
+
+
+
+ {{ keyEntry.provider.name }}
+
+ ({{ keyEntry.provider.provider_model_name }})
+
+
+ · {{ getBillingLabel(keyEntry.provider) }}
+
+
+ · {{ keyEntry.endpoint.base_url }}
+
+
+
+
+
+
+
+
+
+
+ {{ Math.round(keyEntry.key.health_score) }}%
+
+
+
+
+ {{ keyEntry.key.is_adaptive ? '~' : '' }}{{ keyEntry.key.effective_rpm }}
+
+
+
+
+
+
+
+
+
+ 熔断中: {{ keyEntry.key.circuit_breaker_formats.join(', ') }}
+
+
+
+
+
+
+
+
+
+ {{ formatGroup.keyGroups[groupIndex + 1].keys.length > 1 ? '降级 · 负载均衡' : '降级' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 首选
+ P{{ providerEntry.provider.provider_priority }}
+
+
+
+
+
+
+
+
+
+ {{ providerEntry.provider.name }}
+
+ ({{ providerEntry.provider.provider_model_name }})
+
+
+
+
+ {{ providerEntry.endpoint.base_url }}{{ providerEntry.endpoint.custom_path || '' }}
+
+
+
+
+
+
+
+ {{ getBillingLabel(providerEntry.provider) }}
+
+
+
+ {{ providerEntry.active_keys }}/{{ providerEntry.keys.length }} Keys
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 负载均衡
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ key.name }}
+
+
+ {{ key.masked_key }}
+
+
+
+
+
+
+
+ {{ Math.round(key.health_score) }}%
+
+
+
+ {{ key.is_adaptive ? '~' : '' }}{{ key.effective_rpm }}
+
+
+
+
+
+
+
+
+
+ {{ getKeyPriorityGroups(providerEntry.keys)[groupIndex + 1].keys.length > 1 ? '降级 · 负载均衡' : '降级' }}
+
+
+
+
+
+
+ {{ key.name }} 熔断: {{ key.circuit_breaker_formats.join(', ') }}
+
+
+
+
+ 暂无可用 Key
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/features/providers/components/BatchAssignModelsDialog.vue b/frontend/src/features/providers/components/BatchAssignModelsDialog.vue
index 3fd230d..38f3f34 100644
--- a/frontend/src/features/providers/components/BatchAssignModelsDialog.vue
+++ b/frontend/src/features/providers/components/BatchAssignModelsDialog.vue
@@ -107,8 +107,12 @@
/>
-
{{ model.display_name }}
-
{{ model.name }}
+
+ {{ model.display_name }}
+
+
+ {{ model.name }}
+
@@ -159,8 +163,12 @@
/>
-
{{ model.id }}
-
{{ model.owned_by || model.id }}
+
+ {{ model.id }}
+
+
+ {{ model.owned_by || model.id }}
+
@@ -172,7 +180,9 @@
class="flex flex-col items-center justify-center py-12 text-muted-foreground"
>
- {{ searchQuery ? '无匹配结果' : '暂无可用模型' }}
+
+ {{ searchQuery ? '无匹配结果' : '暂无可用模型' }}
+
-
+
diff --git a/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue b/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue
index 7db2cea..a25e8ea 100644
--- a/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue
+++ b/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue
@@ -131,7 +131,10 @@
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
:class="selectedModels.includes(model) ? 'bg-primary border-primary' : ''"
>
-
+
{{ model }}
@@ -174,18 +177,28 @@
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
:class="selectedModels.includes(model.name) ? 'bg-primary border-primary' : ''"
>
-
+
-
{{ model.display_name }}
-
{{ model.name }}
+
+ {{ model.display_name }}
+
+
+ {{ model.name }}
+
-
+
@@ -235,8 +251,15 @@
class="flex flex-col items-center justify-center py-12 text-muted-foreground"
>
-
{{ searchQuery ? '无匹配结果' : '暂无可选模型' }}
-
点击闪电按钮从上游获取模型
+
+ {{ searchQuery ? '无匹配结果' : '暂无可选模型' }}
+
+
+ 点击闪电按钮从上游获取模型
+
@@ -250,10 +273,18 @@
{{ hasChanges ? '有未保存的更改' : '' }}
-
diff --git a/frontend/src/features/providers/components/ProviderDetailDrawer.vue b/frontend/src/features/providers/components/ProviderDetailDrawer.vue
index 20f66ff..0883669 100644
--- a/frontend/src/features/providers/components/ProviderDetailDrawer.vue
+++ b/frontend/src/features/providers/components/ProviderDetailDrawer.vue
@@ -83,7 +83,10 @@
-
+
- /
+ /
{{ API_FORMAT_SHORT[format] || format }} {{ getKeyRateMultiplier(key, format) }}x
| {{ key.rate_limit }}rpm
diff --git a/frontend/src/features/providers/components/ProviderFormDialog.vue b/frontend/src/features/providers/components/ProviderFormDialog.vue
index 0cc0854..17ea951 100644
--- a/frontend/src/features/providers/components/ProviderFormDialog.vue
+++ b/frontend/src/features/providers/components/ProviderFormDialog.vue
@@ -68,9 +68,15 @@
- 月卡额度
- 按量付费
- 免费套餐
+
+ 月卡额度
+
+
+ 按量付费
+
+
+ 免费套餐
+
diff --git a/frontend/src/utils/errorParser.ts b/frontend/src/utils/errorParser.ts
index 9beb59f..4679259 100644
--- a/frontend/src/utils/errorParser.ts
+++ b/frontend/src/utils/errorParser.ts
@@ -314,11 +314,11 @@ export function parseUpstreamModelError(error: string): string {
}
// 没有匹配特定关键词,但有详细信息,使用它作为补充
if (friendlyMsg) {
- const truncated = detailMsg.length > 80 ? detailMsg.substring(0, 80) + '...' : detailMsg
+ const truncated = detailMsg.length > 80 ? `${detailMsg.substring(0, 80) }...` : detailMsg
return `${friendlyMsg}: ${truncated}`
}
// 没有友好消息,直接使用详细信息
- const truncated = detailMsg.length > 100 ? detailMsg.substring(0, 100) + '...' : detailMsg
+ const truncated = detailMsg.length > 100 ? `${detailMsg.substring(0, 100) }...` : detailMsg
return truncated
}
} catch {
@@ -358,7 +358,7 @@ export function parseUpstreamModelError(error: string): string {
// 默认返回原始错误(截断过长的部分)
if (error.length > 100) {
- return error.substring(0, 100) + '...'
+ return `${error.substring(0, 100) }...`
}
return error
}
diff --git a/frontend/src/views/admin/ModelManagement.vue b/frontend/src/views/admin/ModelManagement.vue
index 4e74ed3..65c1714 100644
--- a/frontend/src/views/admin/ModelManagement.vue
+++ b/frontend/src/views/admin/ModelManagement.vue
@@ -418,6 +418,7 @@