diff --git a/crates/aether-routing-core/src/policy.rs b/crates/aether-routing-core/src/policy.rs index 0cdda40e2..3e5da3b6f 100644 --- a/crates/aether-routing-core/src/policy.rs +++ b/crates/aether-routing-core/src/policy.rs @@ -278,8 +278,7 @@ mod tests { use serde_json::json; use crate::actions::{ - RoutingJsonPatchOperation, RoutingRulePhase, RoutingSchedulingMode, - RoutingSetPriorityMode, + RoutingJsonPatchOperation, RoutingRulePhase, RoutingSchedulingMode, RoutingSetPriorityMode, }; use crate::conditions::{RoutingCondition, RoutingConditionOp}; use crate::model::{RoutingDefaultPolicy, RoutingRule}; @@ -367,10 +366,7 @@ mod tests { model_policies: vec![RoutingModelPolicy { model: "special-model".to_string(), allowed_providers: vec!["provider-special".to_string()], - provider_priority_overrides: BTreeMap::from([( - "provider-special".to_string(), - 0, - )]), + provider_priority_overrides: BTreeMap::from([("provider-special".to_string(), 0)]), ..RoutingModelPolicy::default() }], rules: vec![], diff --git a/frontend/src/features/routing/__tests__/routingPolicy.spec.ts b/frontend/src/features/routing/__tests__/routingPolicy.spec.ts index 2c04df7ba..b476ab790 100644 --- a/frontend/src/features/routing/__tests__/routingPolicy.spec.ts +++ b/frontend/src/features/routing/__tests__/routingPolicy.spec.ts @@ -99,7 +99,7 @@ describe('routingPolicy', () => { allowed_models: ['legacy-model'], }) - expect(parseAllowedModelsInput(' gpt-5, claude-*\nlegacy-model, gpt-5 ')).toEqual([ + expect(parseAllowedModelsInput(' gpt-5\nclaude-*\nlegacy-model\ngpt-5 ')).toEqual([ 'gpt-5', 'claude-*', 'legacy-model', @@ -107,10 +107,10 @@ describe('routingPolicy', () => { const restricted = updateAllowedModelsFromInput( config, - 'gpt-5, claude-*\nlegacy-model, gpt-5', + 'gpt-5\nclaude-*\nlegacy-model\ngpt-5', ) expect(restricted.allowed_models).toEqual(['gpt-5', 'claude-*', 'legacy-model']) - expect(formatAllowedModelsInput(restricted.allowed_models)).toBe('gpt-5, claude-*, legacy-model') + expect(formatAllowedModelsInput(restricted.allowed_models)).toBe('gpt-5\nclaude-*\nlegacy-model') expect(routingModelScopeLabel(restricted)).toBe('3 个模型') const unrestricted = clearAllowedModels(restricted) @@ -118,6 +118,23 @@ describe('routingPolicy', () => { expect(routingModelScopeLabel(unrestricted)).toBe('全部模型') }) + it('round-trips selectors containing commas and labels wildcard scope as unrestricted', () => { + const selectors = ['vendor,model', 'gpt-*'] + expect(parseAllowedModelsInput(formatAllowedModelsInput(selectors))).toEqual(selectors) + + const wildcard = normalizeRoutingGroupConfig({ allowed_models: ['gpt-*', '*'] }) + expect(routingModelScopeLabel(wildcard)).toBe('全部模型') + }) + + it('preserves historical empty selectors until unrestricted scope is explicit', () => { + const legacy = normalizeRoutingGroupConfig({ allowed_models: ['', ' '] }) + + expect(updateAllowedModelsFromInput(legacy, ' \n')).toMatchObject({ + allowed_models: ['', ' '], + }) + expect(clearAllowedModels(legacy).allowed_models).toEqual([]) + }) + it('preserves an explicit model allowlist across per-model editing actions', () => { const allowlist = ['gpt-*', 'legacy-model'] let config = normalizeRoutingGroupConfig({ diff --git a/frontend/src/features/routing/utils/routingPolicy.ts b/frontend/src/features/routing/utils/routingPolicy.ts index 4e31ca6fc..2aad08fb9 100644 --- a/frontend/src/features/routing/utils/routingPolicy.ts +++ b/frontend/src/features/routing/utils/routingPolicy.ts @@ -114,7 +114,7 @@ export function normalizeRoutingGroupConfig(value: Partial | export function parseAllowedModelsInput(value: string): string[] { const seen = new Set() return value - .split(/[,\r\n]+/u) + .split(/\r\n?|\n/u) .map(item => item.trim()) .filter(Boolean) .filter((model) => { @@ -125,7 +125,7 @@ export function parseAllowedModelsInput(value: string): string[] { } export function formatAllowedModelsInput(models: string[]): string { - return models.join(', ') + return models.join('\n') } export function updateAllowedModelsFromInput( @@ -133,6 +133,14 @@ export function updateAllowedModelsFromInput( value: string, ): RoutingGroupConfig { const next = normalizeRoutingGroupConfig(config) + // Preserve the historical "empty selector" form until the user explicitly + // chooses the unrestricted scope. It is distinct from an empty allowlist in + // the routing core, where it matches no normal model. + const hasHistoricalEmptySelector = next.allowed_models.length > 0 + && next.allowed_models.every(model => model.trim() === '') + if (value.trim() === '' && hasHistoricalEmptySelector) { + return next + } next.allowed_models = parseAllowedModelsInput(value) return next } @@ -144,8 +152,11 @@ export function clearAllowedModels(config: RoutingGroupConfig): RoutingGroupConf } export function routingModelScopeLabel(config: RoutingGroupConfig): string { - const count = normalizeRoutingGroupConfig(config).allowed_models.length - return count ? `${count} 个模型` : '全部模型' + const models = normalizeRoutingGroupConfig(config).allowed_models + if (models.length === 0 || models.some(model => model.trim() === '*')) { + return '全部模型' + } + return `${models.length} 个模型` } export function allowedModelsMirrorPerModelPolicies(config: RoutingGroupConfig): boolean { diff --git a/frontend/src/views/admin/RoutingProfiles.vue b/frontend/src/views/admin/RoutingProfiles.vue index eef10b728..1ffeab168 100644 --- a/frontend/src/views/admin/RoutingProfiles.vue +++ b/frontend/src/views/admin/RoutingProfiles.vue @@ -187,6 +187,8 @@
@@ -336,20 +338,21 @@ > 模型白名单 - - {{ draft.config_json.allowed_models.length ? `${draft.config_json.allowed_models.length} 项` : '全部模型' }} + + {{ routingModelScopeLabel(draft.config_json) }}

- 控制此策略分组适用于哪些模型;留空表示全部模型。它与“区分模型”中的专属调度覆盖相互独立,支持精确值、* 和前缀通配符(如 gpt-*),多个值用英文逗号或换行分隔。 + 控制此策略分组适用于哪些模型;留空表示全部模型。它与“区分模型”中的专属调度覆盖相互独立,支持精确值、* 和前缀通配符(如 gpt-*),每行填写一个值。

-
- - -
+