mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 09:27:46 +08:00
fix(routing): decouple model overrides from allowed scope
This commit is contained in:
@@ -2,14 +2,24 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
DEFAULT_ROUTING_POLICY_MODEL,
|
||||
allowedModelsMirrorPerModelPolicies,
|
||||
clearAllowedModels,
|
||||
copyPerModelRoutingConfig,
|
||||
createEmptyModelPolicy,
|
||||
createEmptyRoutingGroupConfig,
|
||||
formatAllowedModelsInput,
|
||||
getDefaultModelPolicy,
|
||||
getModelScheduling,
|
||||
modelSchedulingRuleId,
|
||||
normalizeRoutingGroupConfig,
|
||||
parseAllowedModelsInput,
|
||||
removePerModelRoutingConfig,
|
||||
routingModelScopeLabel,
|
||||
savePerModelRoutingConfig,
|
||||
setDefaultPoolPriorityOverrides,
|
||||
setDefaultProviderPriorityOverrides,
|
||||
setRoutingSortingScope,
|
||||
updateAllowedModelsFromInput,
|
||||
upsertModelSchedulingRule,
|
||||
upsertModelPolicy,
|
||||
} from '../utils/routingPolicy'
|
||||
@@ -83,6 +93,98 @@ describe('routingPolicy', () => {
|
||||
scheduling_mode: 'fixed_order',
|
||||
})
|
||||
})
|
||||
|
||||
it('updates the model allowlist only through explicit scope controls', () => {
|
||||
const config = normalizeRoutingGroupConfig({
|
||||
allowed_models: ['legacy-model'],
|
||||
})
|
||||
|
||||
expect(parseAllowedModelsInput(' gpt-5, claude-*\nlegacy-model, gpt-5 ')).toEqual([
|
||||
'gpt-5',
|
||||
'claude-*',
|
||||
'legacy-model',
|
||||
])
|
||||
|
||||
const restricted = updateAllowedModelsFromInput(
|
||||
config,
|
||||
'gpt-5, claude-*\nlegacy-model, gpt-5',
|
||||
)
|
||||
expect(restricted.allowed_models).toEqual(['gpt-5', 'claude-*', 'legacy-model'])
|
||||
expect(formatAllowedModelsInput(restricted.allowed_models)).toBe('gpt-5, claude-*, legacy-model')
|
||||
expect(routingModelScopeLabel(restricted)).toBe('3 个模型')
|
||||
|
||||
const unrestricted = clearAllowedModels(restricted)
|
||||
expect(unrestricted.allowed_models).toEqual([])
|
||||
expect(routingModelScopeLabel(unrestricted)).toBe('全部模型')
|
||||
})
|
||||
|
||||
it('preserves an explicit model allowlist across per-model editing actions', () => {
|
||||
const allowlist = ['gpt-*', 'legacy-model']
|
||||
let config = normalizeRoutingGroupConfig({
|
||||
allowed_models: allowlist,
|
||||
model_policies: [{
|
||||
...createEmptyModelPolicy('special-model'),
|
||||
allowed_providers: ['provider-special'],
|
||||
}],
|
||||
})
|
||||
config = upsertModelSchedulingRule(config, 'special-model', {
|
||||
priority_mode: 'global_key',
|
||||
scheduling_mode: 'fixed_order',
|
||||
})
|
||||
|
||||
const perModel = setRoutingSortingScope(config, 'per_model')
|
||||
expect(perModel.allowed_models).toEqual(allowlist)
|
||||
expect(getModelScheduling(perModel, 'special-model')).toMatchObject({
|
||||
priority_mode: 'global_key',
|
||||
scheduling_mode: 'fixed_order',
|
||||
})
|
||||
|
||||
const saved = savePerModelRoutingConfig(perModel, 'new-special-model')
|
||||
expect(saved.allowed_models).toEqual(allowlist)
|
||||
expect(saved.model_policies.map(policy => policy.model)).toContain('new-special-model')
|
||||
|
||||
const copied = copyPerModelRoutingConfig(
|
||||
saved,
|
||||
saved,
|
||||
'special-model',
|
||||
'copied-special-model',
|
||||
)
|
||||
expect(copied.allowed_models).toEqual(allowlist)
|
||||
expect(copied.model_policies.find(policy => policy.model === 'copied-special-model'))
|
||||
.toMatchObject({ allowed_providers: ['provider-special'] })
|
||||
expect(getModelScheduling(copied, 'copied-special-model')).toMatchObject({
|
||||
priority_mode: 'global_key',
|
||||
scheduling_mode: 'fixed_order',
|
||||
})
|
||||
|
||||
const removed = removePerModelRoutingConfig(copied, 'special-model')
|
||||
expect(removed.allowed_models).toEqual(allowlist)
|
||||
expect(removed.model_policies.map(policy => policy.model)).not.toContain('special-model')
|
||||
expect(removed.rules.map(rule => rule.id)).not.toContain(modelSchedulingRuleId('special-model'))
|
||||
|
||||
const unified = setRoutingSortingScope(removed, 'unified')
|
||||
expect(unified.allowed_models).toEqual(allowlist)
|
||||
expect(unified.model_policies.filter(policy => policy.model !== DEFAULT_ROUTING_POLICY_MODEL))
|
||||
.toEqual([])
|
||||
expect(unified.rules.some(rule => rule.id.startsWith('ui_model_scheduling:'))).toBe(false)
|
||||
})
|
||||
|
||||
it('recognizes legacy allowlist mirrors without mutating historical values', () => {
|
||||
const config = normalizeRoutingGroupConfig({
|
||||
allowed_models: [' model-b ', 'model-a', 'model-a'],
|
||||
model_policies: [
|
||||
createEmptyModelPolicy('model-a'),
|
||||
createEmptyModelPolicy('model-b'),
|
||||
],
|
||||
})
|
||||
|
||||
expect(allowedModelsMirrorPerModelPolicies(config)).toBe(true)
|
||||
expect(config.allowed_models).toEqual([' model-b ', 'model-a', 'model-a'])
|
||||
expect(allowedModelsMirrorPerModelPolicies({
|
||||
...config,
|
||||
allowed_models: ['model-*'],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('routingTrace', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type RoutingPriorityMode = 'provider' | 'global_key'
|
||||
export type RoutingSchedulingMode = 'fixed_order' | 'cache_affinity' | 'load_balance'
|
||||
export type RoutingRulePhase = 'client_request' | 'provider_request'
|
||||
export type RoutingSortingScope = 'unified' | 'per_model'
|
||||
|
||||
export interface RoutingDefaultPolicy {
|
||||
priority_mode: RoutingPriorityMode
|
||||
@@ -110,6 +111,63 @@ export function normalizeRoutingGroupConfig(value: Partial<RoutingGroupConfig> |
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAllowedModelsInput(value: string): string[] {
|
||||
const seen = new Set<string>()
|
||||
return value
|
||||
.split(/[,\r\n]+/u)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
.filter((model) => {
|
||||
if (seen.has(model)) return false
|
||||
seen.add(model)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function formatAllowedModelsInput(models: string[]): string {
|
||||
return models.join(', ')
|
||||
}
|
||||
|
||||
export function updateAllowedModelsFromInput(
|
||||
config: RoutingGroupConfig,
|
||||
value: string,
|
||||
): RoutingGroupConfig {
|
||||
const next = normalizeRoutingGroupConfig(config)
|
||||
next.allowed_models = parseAllowedModelsInput(value)
|
||||
return next
|
||||
}
|
||||
|
||||
export function clearAllowedModels(config: RoutingGroupConfig): RoutingGroupConfig {
|
||||
const next = normalizeRoutingGroupConfig(config)
|
||||
next.allowed_models = []
|
||||
return next
|
||||
}
|
||||
|
||||
export function routingModelScopeLabel(config: RoutingGroupConfig): string {
|
||||
const count = normalizeRoutingGroupConfig(config).allowed_models.length
|
||||
return count ? `${count} 个模型` : '全部模型'
|
||||
}
|
||||
|
||||
export function allowedModelsMirrorPerModelPolicies(config: RoutingGroupConfig): boolean {
|
||||
const normalized = normalizeRoutingGroupConfig(config)
|
||||
const allowedModels = normalized.allowed_models
|
||||
.map(model => model.trim())
|
||||
.filter(Boolean)
|
||||
const perModelNames = normalized.model_policies
|
||||
.map(policy => policy.model)
|
||||
.map(model => model.trim())
|
||||
.filter(Boolean)
|
||||
.filter(model => model !== DEFAULT_ROUTING_POLICY_MODEL)
|
||||
|
||||
if (allowedModels.length === 0 || perModelNames.length === 0) return false
|
||||
if (allowedModels.some(model => model.includes('*'))) return false
|
||||
|
||||
const allowedSet = new Set(allowedModels)
|
||||
const perModelSet = new Set(perModelNames)
|
||||
return allowedSet.size === perModelSet.size
|
||||
&& [...allowedSet].every(model => perModelSet.has(model))
|
||||
}
|
||||
|
||||
export function upsertModelPolicy(config: RoutingGroupConfig, policy: RoutingModelPolicy): RoutingGroupConfig {
|
||||
const model = policy.model.trim()
|
||||
if (!model) {
|
||||
@@ -332,6 +390,58 @@ export function removeGeneratedModelSchedulingRules(config: RoutingGroupConfig):
|
||||
return next
|
||||
}
|
||||
|
||||
export function setRoutingSortingScope(
|
||||
config: RoutingGroupConfig,
|
||||
scope: RoutingSortingScope,
|
||||
): RoutingGroupConfig {
|
||||
if (scope === 'per_model') return normalizeRoutingGroupConfig(config)
|
||||
|
||||
const next = removeGeneratedModelSchedulingRules(config)
|
||||
next.model_policies = next.model_policies
|
||||
.filter(policy => policy.model === DEFAULT_ROUTING_POLICY_MODEL)
|
||||
return next
|
||||
}
|
||||
|
||||
export function removePerModelRoutingConfig(
|
||||
config: RoutingGroupConfig,
|
||||
model: string,
|
||||
): RoutingGroupConfig {
|
||||
return removeModelSchedulingRule(removeModelPolicy(config, model), model)
|
||||
}
|
||||
|
||||
export function copyPerModelRoutingConfig(
|
||||
config: RoutingGroupConfig,
|
||||
sourceConfig: RoutingGroupConfig,
|
||||
sourceModel: string,
|
||||
targetModel: string,
|
||||
): RoutingGroupConfig {
|
||||
const source = sourceModel.trim()
|
||||
const target = targetModel.trim()
|
||||
if (!source || !target || source === target) return normalizeRoutingGroupConfig(config)
|
||||
|
||||
const sourcePolicy = getModelPolicy(sourceConfig, source)
|
||||
const sourceScheduling = getModelScheduling(sourceConfig, source)
|
||||
const next = upsertModelPolicy(config, {
|
||||
...sourcePolicy,
|
||||
model: target,
|
||||
})
|
||||
return upsertModelSchedulingRule(next, target, {
|
||||
priority_mode: sourceScheduling.priority_mode,
|
||||
scheduling_mode: sourceScheduling.scheduling_mode,
|
||||
})
|
||||
}
|
||||
|
||||
export function savePerModelRoutingConfig(
|
||||
config: RoutingGroupConfig,
|
||||
model: string,
|
||||
): RoutingGroupConfig {
|
||||
const normalizedModel = model.trim()
|
||||
const next = normalizeRoutingGroupConfig(config)
|
||||
if (!normalizedModel || normalizedModel === DEFAULT_ROUTING_POLICY_MODEL) return next
|
||||
if (next.model_policies.some(policy => policy.model === normalizedModel)) return next
|
||||
return upsertModelPolicy(next, createEmptyModelPolicy(normalizedModel))
|
||||
}
|
||||
|
||||
export function normalizePriorityOverrides(overrides: Record<string, number>): Record<string, number> {
|
||||
const normalized: Record<string, number> = {}
|
||||
for (const [rawId, rawPriority] of Object.entries(overrides)) {
|
||||
|
||||
Reference in New Issue
Block a user