feat(routing): make routing profiles the sole scheduler policy source

Bootstrap an enabled system-default routing group from the legacy
scheduler config keys on startup, resolve the default ordering config
from that group before falling back to the legacy keys, and stop merging
keep_priority_on_conversion with the legacy flag when a policy is
resolved. Thread the policy-derived ordering config into candidate
preselection so it no longer reads system config independently.

Add per-API-format key priority overrides so a key serving several
formats keeps independent ordering, matching the legacy
global_priority_by_format semantics. Expose keep_priority_on_conversion
in the routing profile editor and read the effective policy in the
model routing preview, monitoring metrics and provider page badge.
This commit is contained in:
elky
2026-09-02 17:04:04 +08:00
parent 166236c2ee
commit 415b2da81b
32 changed files with 1001 additions and 128 deletions
@@ -272,6 +272,8 @@ import {
type ProviderWithEndpointsSummary,
} from '@/api/endpoints'
import { adminApi } from '@/api/admin'
import { listRoutingGroups } from '@/api/routing-profiles'
import { normalizeRoutingGroupConfig } from '@/features/routing/utils/routingPolicy'
import { parseApiError } from '@/utils/errorParser'
import { useI18n } from '@/i18n'
@@ -533,8 +535,18 @@ const maxProviderPriority = computed(() => {
return priorities.length > 0 ? Math.max(...priorities) : undefined
})
// 加载优先级模式
// 加载优先级模式:优先使用启用中的系统默认调度策略,旧的系统配置键仅作兜底
async function loadPriorityMode(options: { cacheTtlMs?: number } = {}) {
try {
const groups = await listRoutingGroups()
const systemDefault = groups.items.find(group => group.is_system_default && group.enabled)
if (systemDefault) {
priorityMode.value = normalizeRoutingGroupConfig(systemDefault.config_json).default_policy.priority_mode
return
}
} catch {
// 路由策略不可用时继续尝试旧配置
}
try {
const response = await adminApi.getSystemConfig('provider_priority_mode', {
cacheTtlMs: options.cacheTtlMs ?? 0,
@@ -454,6 +454,23 @@
</div>
</div>
</div>
<label
class="flex items-start gap-3 rounded-lg border border-border/60 px-3 py-2 text-sm"
data-testid="keep-priority-on-conversion"
>
<Switch
:model-value="keepPriorityOnConversion"
:disabled="saving"
aria-label="格式转换时保持优先级"
@update:model-value="updateKeepPriorityOnConversion"
/>
<span class="min-w-0">
<span class="block font-medium">格式转换时保持优先级</span>
<span class="mt-0.5 block text-xs text-muted-foreground">
开启后,需要跨 API 格式转换的候选不会被降级到同格式候选之后。作用于本策略范围内的全部模型;Provider 自身的同名开关仍单独生效。
</span>
</span>
</label>
</div>
<RoutingPriorityPolicyEditor
@@ -749,6 +766,7 @@ import {
Button,
Card,
Input,
Switch,
Table,
TableBody,
TableCard,
@@ -874,6 +892,9 @@ const firstStepSchedulingMode = computed<RoutingSchedulingMode>(() => {
}
return draft.value?.config_json.default_policy.scheduling_mode ?? 'cache_affinity'
})
const keepPriorityOnConversion = computed<boolean>(() => (
draft.value?.config_json.default_policy.keep_priority_on_conversion ?? false
))
const allowedModelsLookLikeLegacyMirror = computed(() => {
return draft.value
? allowedModelsMirrorPerModelPolicies(draft.value.config_json)
@@ -1190,6 +1211,17 @@ function updateFirstStepSchedulingMode(mode: RoutingSchedulingMode): void {
})
}
function updateKeepPriorityOnConversion(value: boolean): void {
if (!draft.value) return
updateDraftConfig({
...draft.value.config_json,
default_policy: {
...draft.value.config_json.default_policy,
keep_priority_on_conversion: value,
},
})
}
function removePerModelPolicy(model: string): void {
if (!draft.value) return
if (perModelEditingActive.value && editingDirty.value) {
@@ -131,11 +131,31 @@ vi.mock('@/components/ui', async () => {
},
})
const Switch = defineComponent({
inheritAttrs: false,
props: {
modelValue: { type: Boolean, default: false },
disabled: Boolean,
},
emits: ['update:modelValue'],
setup(props, { attrs, emit }) {
return () => h('button', {
...attrs,
type: 'button',
role: 'switch',
'aria-checked': props.modelValue,
disabled: props.disabled,
onClick: () => emit('update:modelValue', !props.modelValue),
})
},
})
return {
Badge: wrapper(),
Button,
Card: wrapper('section'),
Input,
Switch,
Table: wrapper('table'),
TableBody: wrapper('tbody'),
TableCard: wrapper(),