mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 03:39:49 +08:00
219 lines
12 KiB
TypeScript
219 lines
12 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
createEmptyModelPolicy,
|
|
createEmptyRoutingGroupConfig,
|
|
getDefaultModelPolicy,
|
|
getModelPolicy,
|
|
getModelScheduling,
|
|
modelSchedulingRuleId,
|
|
setDefaultProviderPriorityOverrides,
|
|
setModelKeyPriorityOverridesForFormat,
|
|
upsertModelPolicy,
|
|
upsertModelSchedulingRule,
|
|
type RoutingRule,
|
|
} from '../utils/routingPolicy'
|
|
import {
|
|
createSchedulingPolicy,
|
|
normalizeProviderSchedulingConfig,
|
|
readSchedulingPolicies,
|
|
schedulingPolicyEditorConfig,
|
|
validateSchedulingPolicies,
|
|
writeSchedulingPolicies,
|
|
} from '../utils/schedulingPolicies'
|
|
|
|
describe('strategy-scoped scheduling policies', () => {
|
|
it('normalizes legacy Key scheduling without mutating unrelated rules, scopes, or historical priorities', () => {
|
|
const config = createEmptyRoutingGroupConfig()
|
|
config.default_policy.priority_mode = 'global_key'
|
|
config.disabled_providers = ['disabled']
|
|
config.model_policies = [{ ...createEmptyModelPolicy('*'), key_priority_overrides: { key: 5 }, provider_priority_overrides: { provider: 3 } }]
|
|
config.rules = [{
|
|
id: 'custom-rule', priority: 1, enabled: true, phase: 'client_request', stop_processing: true,
|
|
conditions: { field: 'model', op: 'prefix', value: 'example' },
|
|
actions: [
|
|
{ type: 'set_scheduling', priority_mode: 'global_key', scheduling_mode: 'load_balance', sticky_key_attempts: 4 },
|
|
{ type: 'set_scheduling', scheduling_mode: 'fixed_order' },
|
|
{ type: 'set_header', name: 'x-test', value: 'kept' },
|
|
],
|
|
}]
|
|
const before = JSON.stringify(config)
|
|
const normalized = normalizeProviderSchedulingConfig(config)
|
|
expect(normalized.default_policy.priority_mode).toBe('provider')
|
|
expect(normalized.disabled_providers).toEqual(config.disabled_providers)
|
|
expect(normalized.model_policies).toEqual(config.model_policies)
|
|
expect(normalized.rules[0]).toEqual({ ...config.rules[0], actions: [
|
|
{ type: 'set_scheduling', priority_mode: 'provider', scheduling_mode: 'load_balance', sticky_key_attempts: 4 },
|
|
...config.rules[0].actions.slice(1),
|
|
] })
|
|
expect(readSchedulingPolicies(config).every(entry => entry.priorityMode === 'provider')).toBe(true)
|
|
expect(createSchedulingPolicy(config).priorityMode).toBe('provider')
|
|
expect(JSON.stringify(config)).toBe(before)
|
|
})
|
|
|
|
it('starts with one all-model strategy', () => {
|
|
const entries = readSchedulingPolicies(createEmptyRoutingGroupConfig())
|
|
expect(entries).toHaveLength(1)
|
|
expect(entries[0]).toMatchObject({ scope: 'all', priorityMode: 'provider', schedulingMode: 'cache_affinity' })
|
|
expect(validateSchedulingPolicies(entries)).toBeNull()
|
|
})
|
|
|
|
it('generates unique policy ids on HTTP pages without crypto.randomUUID', () => {
|
|
vi.stubGlobal('crypto', {})
|
|
try {
|
|
const config = createEmptyRoutingGroupConfig()
|
|
expect(createSchedulingPolicy(config).id).not.toBe(createSchedulingPolicy(config).id)
|
|
expect(readSchedulingPolicies(config)).toHaveLength(1)
|
|
} finally {
|
|
vi.unstubAllGlobals()
|
|
}
|
|
})
|
|
|
|
it('persists all models as a wildcard rather than enumerating the current catalog', () => {
|
|
const config = createEmptyRoutingGroupConfig()
|
|
const entry = createSchedulingPolicy(config, 'all')
|
|
entry.models = ['model-a', 'model-b']
|
|
entry.priorityMode = 'global_key'
|
|
entry.schedulingMode = 'load_balance'
|
|
entry.policy.provider_priority_overrides = { provider: 2 }
|
|
const saved = JSON.parse(JSON.stringify(writeSchedulingPolicies(config, [entry])))
|
|
expect(saved.rules).toEqual([])
|
|
expect(saved.model_policies.map((policy: { model: string }) => policy.model)).toEqual(['*'])
|
|
expect(readSchedulingPolicies(saved)[0]).toMatchObject({ scope: 'all', models: [] })
|
|
expect(getDefaultModelPolicy(saved).provider_priority_overrides).toEqual({ provider: 2 })
|
|
for (const model of ['model-a', 'model-b', 'future-model']) {
|
|
expect(getModelScheduling(saved, model)).toMatchObject({ priority_mode: 'provider', scheduling_mode: 'load_balance' })
|
|
}
|
|
})
|
|
|
|
it('persists one strategy for multiple models with shared rankings', () => {
|
|
const config = createEmptyRoutingGroupConfig()
|
|
const entry = createSchedulingPolicy(config)
|
|
entry.models = ['model-a', 'model-b']
|
|
entry.priorityMode = 'global_key'
|
|
entry.schedulingMode = 'fixed_order'
|
|
entry.policy = {
|
|
...entry.policy,
|
|
allowed_providers: ['provider-a'],
|
|
provider_priority_overrides: { 'provider-a': 2 },
|
|
key_priority_overrides_by_format: { 'openai:chat': { 'key-a': 1 } },
|
|
pool_priority_overrides: { 'pool-a': 3 },
|
|
pool_policy_overrides: { 'pool-a': { scheduling_presets: [{ preset: 'cache_affinity', enabled: true }] } },
|
|
}
|
|
const saved = writeSchedulingPolicies(config, [entry])
|
|
expect(saved.rules).toHaveLength(1)
|
|
expect(saved.rules[0].conditions).toEqual({ any: [
|
|
{ field: 'model', op: 'eq', value: 'model-a' },
|
|
{ field: 'model', op: 'eq', value: 'model-b' },
|
|
] })
|
|
for (const model of entry.models) {
|
|
expect(getModelScheduling(saved, model)).toMatchObject({ priority_mode: 'provider', scheduling_mode: 'fixed_order' })
|
|
expect(getModelPolicy(saved, model)).toEqual({ ...entry.policy, model })
|
|
}
|
|
expect(getModelScheduling(saved, 'other-model').scheduling_mode).toBe('cache_affinity')
|
|
const reloaded = readSchedulingPolicies(JSON.parse(JSON.stringify(saved)))
|
|
expect(reloaded).toHaveLength(1)
|
|
expect(reloaded[0]).toMatchObject({ id: entry.id, models: ['model-a', 'model-b'], policy: entry.policy })
|
|
expect(schedulingPolicyEditorConfig(saved, reloaded[0]).model_policies).toEqual([entry.policy])
|
|
})
|
|
|
|
it('retains group exclusions in the isolated ranking editor config', () => {
|
|
const config = createEmptyRoutingGroupConfig()
|
|
config.disabled_providers = ['disabled-provider']
|
|
const entry = createSchedulingPolicy(config)
|
|
const editorConfig = schedulingPolicyEditorConfig(config, entry)
|
|
expect(editorConfig.disabled_providers).toEqual(['disabled-provider'])
|
|
editorConfig.disabled_providers.push('local-only')
|
|
expect(config.disabled_providers).toEqual(['disabled-provider'])
|
|
})
|
|
|
|
it('retains separate strategies even when their settings are identical', () => {
|
|
const config = createEmptyRoutingGroupConfig()
|
|
const first = { ...createSchedulingPolicy(config), models: ['model-a'] }
|
|
const second = { ...createSchedulingPolicy(config), models: ['model-b'] }
|
|
const entries = readSchedulingPolicies(writeSchedulingPolicies(config, [first, second]))
|
|
expect(entries.map(entry => entry.id)).toEqual([first.id, second.id])
|
|
})
|
|
|
|
it('loads legacy per-model policies, including rules without a ranking policy', () => {
|
|
let config = createEmptyRoutingGroupConfig()
|
|
for (const model of ['model-a', 'model-b']) {
|
|
config = upsertModelPolicy(config, { ...createEmptyModelPolicy(model), provider_priority_overrides: { provider: 2 } })
|
|
config = upsertModelSchedulingRule(config, model, { priority_mode: 'provider', scheduling_mode: 'load_balance' })
|
|
}
|
|
config = upsertModelSchedulingRule(config, 'model-c', { priority_mode: 'global_key', scheduling_mode: 'fixed_order' })
|
|
const entries = readSchedulingPolicies(config)
|
|
expect(entries).toHaveLength(2)
|
|
expect(entries[0].models).toEqual(['model-a', 'model-b'])
|
|
expect(entries[1].models).toEqual(['model-c'])
|
|
const saved = writeSchedulingPolicies(config, entries)
|
|
for (const model of ['model-a', 'model-b', 'model-c', 'other']) {
|
|
expect(getModelScheduling(saved, model)).toEqual({ ...getModelScheduling(config, model), priority_mode: 'provider' })
|
|
expect(getModelPolicy(saved, model)).toEqual(getModelPolicy(config, model))
|
|
}
|
|
expect(saved.rules.every(rule => !rule.id.startsWith('ui_model_scheduling:'))).toBe(true)
|
|
})
|
|
|
|
it('preserves group execution options, failover rules, and custom routing rules', () => {
|
|
const config = createEmptyRoutingGroupConfig()
|
|
config.default_policy.cancel_on_client_disconnect = true
|
|
config.default_policy.sticky_key_attempts = 5
|
|
config.default_policy.max_transfer_count = 7
|
|
config.default_policy.failover_rules.error_stop_patterns = [{ pattern: '', status_codes: [429] }]
|
|
const rule: RoutingRule = {
|
|
id: 'custom-header-rule', priority: 3, enabled: true, phase: 'provider_request',
|
|
conditions: { field: 'model', op: 'prefix', value: 'model-' },
|
|
actions: [{ type: 'set_header', name: 'x-test', value: 'kept' }], stop_processing: false,
|
|
}
|
|
config.rules.push(rule)
|
|
const entry = { ...createSchedulingPolicy(config), models: ['model-a'] }
|
|
const saved = writeSchedulingPolicies(config, [entry])
|
|
expect(saved.default_policy).toEqual(config.default_policy)
|
|
expect(saved.rules[0]).toEqual(rule)
|
|
expect(config.model_policies).toEqual([])
|
|
expect(config.rules).toEqual([rule])
|
|
})
|
|
|
|
it('keeps the wildcard fallback before specific rankings and preserves per-format keys', () => {
|
|
let config = setDefaultProviderPriorityOverrides(createEmptyRoutingGroupConfig(), { provider: 4 })
|
|
config = setModelKeyPriorityOverridesForFormat(config, 'model-a', 'openai:chat', { key: 2 })
|
|
const entries = readSchedulingPolicies(config)
|
|
expect(entries.map(entry => entry.scope)).toEqual(['selected', 'all'])
|
|
const saved = writeSchedulingPolicies(config, entries)
|
|
expect(saved.model_policies.map(policy => policy.model)).toEqual(['*', 'model-a'])
|
|
expect(getModelPolicy(saved, 'model-a').key_priority_overrides_by_format).toEqual({ 'openai:chat': { key: 2 } })
|
|
expect(getModelPolicy(saved, '*').provider_priority_overrides).toEqual({ provider: 4 })
|
|
})
|
|
|
|
it('removes obsolete model rules when a model leaves a strategy', () => {
|
|
const config = createEmptyRoutingGroupConfig()
|
|
const entry = { ...createSchedulingPolicy(config), models: ['model-a', 'model-b'], schedulingMode: 'load_balance' as const }
|
|
const previous = writeSchedulingPolicies(config, [entry])
|
|
const saved = writeSchedulingPolicies(previous, [{ ...entry, models: ['model-b', 'model-c'] }])
|
|
expect(saved.model_policies.map(policy => policy.model)).toEqual(['model-b', 'model-c'])
|
|
expect(saved.rules).toHaveLength(1)
|
|
expect(getModelScheduling(saved, 'model-a').scheduling_mode).toBe('cache_affinity')
|
|
expect(getModelScheduling(saved, 'model-c').scheduling_mode).toBe('load_balance')
|
|
})
|
|
|
|
it('preserves legacy model retry overrides and prefix matching', () => {
|
|
const config = upsertModelSchedulingRule(createEmptyRoutingGroupConfig(), 'legacy-*', {
|
|
priority_mode: 'provider', scheduling_mode: 'fixed_order',
|
|
})
|
|
const rule = config.rules.find(rule => rule.id === modelSchedulingRuleId('legacy-*'))!
|
|
rule.actions = [{ type: 'set_scheduling', priority_mode: 'provider', scheduling_mode: 'fixed_order', sticky_key_attempts: 4 }]
|
|
const saved = writeSchedulingPolicies(config, readSchedulingPolicies(config))
|
|
expect(getModelScheduling(saved, 'legacy-model').sticky_key_attempts).toBe(4)
|
|
expect(getModelScheduling(saved, 'other-model').scheduling_mode).toBe('cache_affinity')
|
|
})
|
|
|
|
it('rejects empty or overlapping scopes', () => {
|
|
const config = createEmptyRoutingGroupConfig()
|
|
const first = createSchedulingPolicy(config)
|
|
expect(validateSchedulingPolicies([first])).toContain('选择至少一个')
|
|
first.models = ['model-a']
|
|
expect(validateSchedulingPolicies([first, { ...createSchedulingPolicy(config), models: ['model-a'] }])).toContain('不能重复')
|
|
expect(validateSchedulingPolicies([createSchedulingPolicy(config, 'all'), createSchedulingPolicy(config, 'all')])).toContain('只能有一条')
|
|
expect(validateSchedulingPolicies([])).not.toBeNull()
|
|
})
|
|
})
|