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
@@ -14,10 +14,12 @@ import {
normalizeRoutingGroupConfig,
parseAllowedModelsInput,
removePerModelRoutingConfig,
resolveModelKeyPriorityOverride,
routingModelScopeLabel,
savePerModelRoutingConfig,
setDefaultPoolPriorityOverrides,
setDefaultProviderPriorityOverrides,
setModelKeyPriorityOverridesForFormat,
setRoutingSortingScope,
updateAllowedModelsFromInput,
upsertModelSchedulingRule,
@@ -75,6 +77,52 @@ describe('routingPolicy', () => {
expect(policy.key_priority_overrides).toEqual({})
})
it('keeps key priority overrides independent per api format', () => {
let config = setModelKeyPriorityOverridesForFormat(
createEmptyRoutingGroupConfig(),
DEFAULT_ROUTING_POLICY_MODEL,
'OpenAI:Chat',
{ 'key-a': 0, 'key-b': 1 },
)
config = setModelKeyPriorityOverridesForFormat(
config,
DEFAULT_ROUTING_POLICY_MODEL,
'claude:messages',
{ 'key-a': 3 },
)
const policy = getDefaultModelPolicy(config)
expect(policy.key_priority_overrides).toEqual({})
expect(policy.key_priority_overrides_by_format).toEqual({
'openai:chat': { 'key-a': 0, 'key-b': 1 },
'claude:messages': { 'key-a': 3 },
})
expect(resolveModelKeyPriorityOverride(config, DEFAULT_ROUTING_POLICY_MODEL, 'openai:chat', 'key-a')).toBe(0)
expect(resolveModelKeyPriorityOverride(config, DEFAULT_ROUTING_POLICY_MODEL, 'claude:messages', 'key-a')).toBe(3)
expect(resolveModelKeyPriorityOverride(config, DEFAULT_ROUTING_POLICY_MODEL, 'claude:messages', 'key-b')).toBeUndefined()
const cleared = setModelKeyPriorityOverridesForFormat(config, DEFAULT_ROUTING_POLICY_MODEL, 'claude:messages', {})
expect(getDefaultModelPolicy(cleared).key_priority_overrides_by_format).toEqual({
'openai:chat': { 'key-a': 0, 'key-b': 1 },
})
})
it('falls back to format-agnostic key overrides and normalizes legacy configs', () => {
const config = normalizeRoutingGroupConfig({
model_policies: [{
...createEmptyModelPolicy('gpt-5'),
key_priority_overrides: { 'key-a': 7 },
key_priority_overrides_by_format: { ' OpenAI:Chat ': { 'key-a': 1 } },
}],
})
expect(config.model_policies[0].key_priority_overrides_by_format).toEqual({
'openai:chat': { 'key-a': 1 },
})
expect(resolveModelKeyPriorityOverride(config, 'gpt-5', 'openai:chat', 'key-a')).toBe(1)
expect(resolveModelKeyPriorityOverride(config, 'gpt-5', 'gemini:generate_content', 'key-a')).toBe(7)
})
it('stores per-model scheduling as generated routing rules', () => {
const next = upsertModelSchedulingRule(createEmptyRoutingGroupConfig(), 'gpt-5', {
priority_mode: 'global_key',
@@ -315,7 +315,8 @@ import {
getDefaultModelPolicy,
getModelPolicy,
normalizeRoutingGroupConfig,
setModelKeyPriorityOverrides,
normalizeRoutingApiFormatKey,
setModelKeyPriorityOverridesForFormat,
setModelPoolPriorityOverrides,
setModelProviderPriorityOverrides,
type RoutingDefaultPolicy,
@@ -452,9 +453,18 @@ const providerRows = computed<ProviderPriorityRow[]>(() => {
.sort(comparePriorityRows)
})
const selectedFormatKey = computed(() => normalizeRoutingApiFormatKey(selectedApiFormat.value))
const selectedFormatKeyOverrides = computed<Record<string, number>>(() => (
targetModelPolicy.value.key_priority_overrides_by_format[selectedFormatKey.value] ?? {}
))
const keyRows = computed<KeyPriorityRow[]>(() => {
const format = selectedApiFormat.value
const keyOverrides = targetModelPolicy.value.key_priority_overrides
// 按格式覆盖优先;旧的不分格式覆盖仅作为兜底展示
const keyOverrides: Record<string, number> = {
...targetModelPolicy.value.key_priority_overrides,
...selectedFormatKeyOverrides.value,
}
const poolOverrides = targetModelPolicy.value.pool_priority_overrides
const normalRows: KeyPriorityRow[] = []
const poolGroups = new Map<string, GlobalKeySource[]>()
@@ -686,7 +696,7 @@ function setKeyPriority(keyId: string, event: Event): void {
})
} else {
updateKeyOverrides({
...targetModelPolicy.value.key_priority_overrides,
...selectedFormatKeyOverrides.value,
[row.target_id]: priority,
})
}
@@ -697,8 +707,14 @@ function moveKey(keyId: string, direction: -1 | 1): void {
updateVisibleKeyAndPoolOverrides(rows)
}
// Key 覆盖始终写入当前选中的 API 格式,不同格式互不影响
function updateKeyOverrides(overrides: Record<string, number>): void {
updateConfig(setModelKeyPriorityOverrides(config.value, targetModel.value, overrides))
updateConfig(setModelKeyPriorityOverridesForFormat(
config.value,
targetModel.value,
selectedApiFormat.value,
overrides,
))
}
function updatePoolOverrides(overrides: Record<string, number>): void {
@@ -710,7 +726,12 @@ function updateKeyAndPoolOverrides(
poolOverrides: Record<string, number>,
): void {
const next = setModelPoolPriorityOverrides(
setModelKeyPriorityOverrides(config.value, targetModel.value, keyOverrides),
setModelKeyPriorityOverridesForFormat(
config.value,
targetModel.value,
selectedApiFormat.value,
keyOverrides,
),
targetModel.value,
poolOverrides,
)
@@ -718,7 +739,7 @@ function updateKeyAndPoolOverrides(
}
function updateVisibleKeyAndPoolOverrides(rows: KeyPriorityRow[]): void {
const keyOverrides = { ...targetModelPolicy.value.key_priority_overrides }
const keyOverrides = { ...selectedFormatKeyOverrides.value }
const poolOverrides = { ...targetModelPolicy.value.pool_priority_overrides }
for (const row of keyRows.value) {
@@ -25,6 +25,8 @@ export interface RoutingModelPolicy {
allowed_keys: string[]
provider_priority_overrides: Record<string, number>
key_priority_overrides: Record<string, number>
/** api_format -> key_id -> priority;同一 Key 在不同 API 格式下可独立排序 */
key_priority_overrides_by_format: Record<string, Record<string, number>>
pool_priority_overrides: Record<string, number>
pool_policy_overrides: Record<string, RoutingPoolPolicyOverride>
}
@@ -81,6 +83,7 @@ export function createEmptyModelPolicy(model = ''): RoutingModelPolicy {
allowed_keys: [],
provider_priority_overrides: {},
key_priority_overrides: {},
key_priority_overrides_by_format: {},
pool_priority_overrides: {},
pool_policy_overrides: {},
}
@@ -103,6 +106,9 @@ export function normalizeRoutingGroupConfig(value: Partial<RoutingGroupConfig> |
allowed_keys: Array.isArray(policy.allowed_keys) ? [...policy.allowed_keys] : [],
provider_priority_overrides: { ...(policy.provider_priority_overrides ?? {}) },
key_priority_overrides: { ...(policy.key_priority_overrides ?? {}) },
key_priority_overrides_by_format: normalizeKeyPriorityOverridesByFormat(
policy.key_priority_overrides_by_format,
),
pool_priority_overrides: { ...(policy.pool_priority_overrides ?? {}) },
pool_policy_overrides: { ...(policy.pool_policy_overrides ?? {}) },
}))
@@ -296,6 +302,64 @@ export function setModelKeyPriorityOverrides(
})
}
export function normalizeRoutingApiFormatKey(apiFormat: string): string {
return apiFormat.trim().toLowerCase()
}
export function getModelKeyPriorityOverridesForFormat(
config: RoutingGroupConfig,
model: string,
apiFormat: string,
): Record<string, number> {
const policy = getModelPolicy(config, model)
const format = normalizeRoutingApiFormatKey(apiFormat)
return { ...(policy.key_priority_overrides_by_format[format] ?? {}) }
}
/**
* 某个 Key 在指定 API 格式下的生效覆盖值:按格式覆盖优先,其次是不分格式的 Key 覆盖。
*/
export function resolveModelKeyPriorityOverride(
config: RoutingGroupConfig,
model: string,
apiFormat: string,
keyId: string,
): number | undefined {
const policy = getModelPolicy(config, model)
const format = normalizeRoutingApiFormatKey(apiFormat)
return policy.key_priority_overrides_by_format[format]?.[keyId]
?? policy.key_priority_overrides[keyId]
}
export function setModelKeyPriorityOverridesForFormat(
config: RoutingGroupConfig,
model: string,
apiFormat: string,
overrides: Record<string, number>,
): RoutingGroupConfig {
const normalizedModel = model.trim() || DEFAULT_ROUTING_POLICY_MODEL
const format = normalizeRoutingApiFormatKey(apiFormat)
if (!format) return normalizeRoutingGroupConfig(config)
const current = getModelPolicy(config, normalizedModel)
const byFormat = { ...current.key_priority_overrides_by_format }
const normalized = normalizePriorityOverrides(overrides)
if (Object.keys(normalized).length > 0) {
byFormat[format] = normalized
} else {
delete byFormat[format]
}
if (normalizedModel === DEFAULT_ROUTING_POLICY_MODEL) {
return upsertDefaultModelPolicy(config, { key_priority_overrides_by_format: byFormat })
}
return upsertModelPolicy(config, {
...current,
model: normalizedModel,
key_priority_overrides_by_format: byFormat,
})
}
export function setModelPoolPriorityOverrides(
config: RoutingGroupConfig,
model: string,
@@ -464,6 +528,25 @@ export function normalizePriorityOverrides(overrides: Record<string, number>): R
return normalized
}
function normalizeKeyPriorityOverridesByFormat(
value: Record<string, Record<string, number>> | null | undefined,
): Record<string, Record<string, number>> {
const normalized: Record<string, Record<string, number>> = {}
if (!value || typeof value !== 'object') return normalized
for (const [rawFormat, overrides] of Object.entries(value)) {
const format = normalizeRoutingApiFormatKey(rawFormat)
if (!format || !overrides || typeof overrides !== 'object') continue
const merged = normalizePriorityOverrides({
...(normalized[format] ?? {}),
...overrides,
})
if (Object.keys(merged).length > 0) {
normalized[format] = merged
}
}
return normalized
}
function isSetSchedulingAction(action: unknown): action is RoutingSetSchedulingAction {
if (!action || typeof action !== 'object') return false
const candidate = action as Partial<RoutingSetSchedulingAction>