mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 05:00:19 +08:00
fix(routing): decouple model overrides from allowed scope
This commit is contained in:
@@ -277,7 +277,10 @@ mod tests {
|
|||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::actions::{RoutingJsonPatchOperation, RoutingRulePhase};
|
use crate::actions::{
|
||||||
|
RoutingJsonPatchOperation, RoutingRulePhase, RoutingSchedulingMode,
|
||||||
|
RoutingSetPriorityMode,
|
||||||
|
};
|
||||||
use crate::conditions::{RoutingCondition, RoutingConditionOp};
|
use crate::conditions::{RoutingCondition, RoutingConditionOp};
|
||||||
use crate::model::{RoutingDefaultPolicy, RoutingRule};
|
use crate::model::{RoutingDefaultPolicy, RoutingRule};
|
||||||
|
|
||||||
@@ -352,6 +355,89 @@ mod tests {
|
|||||||
assert_eq!(policy.mutation_plan.body_patch.len(), 1);
|
assert_eq!(policy.mutation_plan.body_patch.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_allowlist_keeps_default_policy_for_models_without_an_override() {
|
||||||
|
let config = RoutingGroupConfig {
|
||||||
|
allowed_models: vec![],
|
||||||
|
default_policy: RoutingDefaultPolicy {
|
||||||
|
priority_mode: RoutingSetPriorityMode::GlobalKey,
|
||||||
|
scheduling_mode: RoutingSchedulingMode::LoadBalance,
|
||||||
|
keep_priority_on_conversion: true,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
)]),
|
||||||
|
..RoutingModelPolicy::default()
|
||||||
|
}],
|
||||||
|
rules: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let special = resolve_routing_policy(
|
||||||
|
&config,
|
||||||
|
RoutingPolicyInput {
|
||||||
|
group_id: Some("group-1"),
|
||||||
|
group_version: Some(1),
|
||||||
|
selection_source: "test",
|
||||||
|
requested_model: "special-model",
|
||||||
|
resolved_model: "special-model",
|
||||||
|
api_format: "openai:chat",
|
||||||
|
user_id: None,
|
||||||
|
api_key_id: None,
|
||||||
|
headers: &json!({}),
|
||||||
|
body: &json!({}),
|
||||||
|
phase: RoutingRulePhase::ClientRequest,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("the specially configured model should resolve");
|
||||||
|
|
||||||
|
assert_eq!(special.priority_mode, RoutingSetPriorityMode::GlobalKey);
|
||||||
|
assert_eq!(special.scheduling_mode, RoutingSchedulingMode::LoadBalance);
|
||||||
|
assert!(special.keep_priority_on_conversion);
|
||||||
|
assert_eq!(
|
||||||
|
special.ranking_overlay.allowed_providers,
|
||||||
|
vec!["provider-special"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
special
|
||||||
|
.ranking_overlay
|
||||||
|
.provider_priority_overrides
|
||||||
|
.get("provider-special"),
|
||||||
|
Some(&0)
|
||||||
|
);
|
||||||
|
|
||||||
|
let ordinary = resolve_routing_policy(
|
||||||
|
&config,
|
||||||
|
RoutingPolicyInput {
|
||||||
|
group_id: Some("group-1"),
|
||||||
|
group_version: Some(1),
|
||||||
|
selection_source: "test",
|
||||||
|
requested_model: "ordinary-model",
|
||||||
|
resolved_model: "ordinary-model",
|
||||||
|
api_format: "openai:chat",
|
||||||
|
user_id: None,
|
||||||
|
api_key_id: None,
|
||||||
|
headers: &json!({}),
|
||||||
|
body: &json!({}),
|
||||||
|
phase: RoutingRulePhase::ClientRequest,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("an unconfigured model should keep using the default policy");
|
||||||
|
|
||||||
|
assert_eq!(ordinary.priority_mode, RoutingSetPriorityMode::GlobalKey);
|
||||||
|
assert_eq!(ordinary.scheduling_mode, RoutingSchedulingMode::LoadBalance);
|
||||||
|
assert!(ordinary.keep_priority_on_conversion);
|
||||||
|
assert!(ordinary.ranking_overlay.allowed_providers.is_empty());
|
||||||
|
assert!(ordinary.ranking_overlay.allowed_keys.is_empty());
|
||||||
|
assert!(ordinary
|
||||||
|
.ranking_overlay
|
||||||
|
.provider_priority_overrides
|
||||||
|
.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_disallowed_model() {
|
fn rejects_disallowed_model() {
|
||||||
let config = RoutingGroupConfig {
|
let config = RoutingGroupConfig {
|
||||||
|
|||||||
@@ -2,14 +2,24 @@ import { describe, expect, it } from 'vitest'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_ROUTING_POLICY_MODEL,
|
DEFAULT_ROUTING_POLICY_MODEL,
|
||||||
|
allowedModelsMirrorPerModelPolicies,
|
||||||
|
clearAllowedModels,
|
||||||
|
copyPerModelRoutingConfig,
|
||||||
createEmptyModelPolicy,
|
createEmptyModelPolicy,
|
||||||
createEmptyRoutingGroupConfig,
|
createEmptyRoutingGroupConfig,
|
||||||
|
formatAllowedModelsInput,
|
||||||
getDefaultModelPolicy,
|
getDefaultModelPolicy,
|
||||||
getModelScheduling,
|
getModelScheduling,
|
||||||
modelSchedulingRuleId,
|
modelSchedulingRuleId,
|
||||||
normalizeRoutingGroupConfig,
|
normalizeRoutingGroupConfig,
|
||||||
|
parseAllowedModelsInput,
|
||||||
|
removePerModelRoutingConfig,
|
||||||
|
routingModelScopeLabel,
|
||||||
|
savePerModelRoutingConfig,
|
||||||
setDefaultPoolPriorityOverrides,
|
setDefaultPoolPriorityOverrides,
|
||||||
setDefaultProviderPriorityOverrides,
|
setDefaultProviderPriorityOverrides,
|
||||||
|
setRoutingSortingScope,
|
||||||
|
updateAllowedModelsFromInput,
|
||||||
upsertModelSchedulingRule,
|
upsertModelSchedulingRule,
|
||||||
upsertModelPolicy,
|
upsertModelPolicy,
|
||||||
} from '../utils/routingPolicy'
|
} from '../utils/routingPolicy'
|
||||||
@@ -83,6 +93,98 @@ describe('routingPolicy', () => {
|
|||||||
scheduling_mode: 'fixed_order',
|
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', () => {
|
describe('routingTrace', () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type RoutingPriorityMode = 'provider' | 'global_key'
|
export type RoutingPriorityMode = 'provider' | 'global_key'
|
||||||
export type RoutingSchedulingMode = 'fixed_order' | 'cache_affinity' | 'load_balance'
|
export type RoutingSchedulingMode = 'fixed_order' | 'cache_affinity' | 'load_balance'
|
||||||
export type RoutingRulePhase = 'client_request' | 'provider_request'
|
export type RoutingRulePhase = 'client_request' | 'provider_request'
|
||||||
|
export type RoutingSortingScope = 'unified' | 'per_model'
|
||||||
|
|
||||||
export interface RoutingDefaultPolicy {
|
export interface RoutingDefaultPolicy {
|
||||||
priority_mode: RoutingPriorityMode
|
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 {
|
export function upsertModelPolicy(config: RoutingGroupConfig, policy: RoutingModelPolicy): RoutingGroupConfig {
|
||||||
const model = policy.model.trim()
|
const model = policy.model.trim()
|
||||||
if (!model) {
|
if (!model) {
|
||||||
@@ -332,6 +390,58 @@ export function removeGeneratedModelSchedulingRules(config: RoutingGroupConfig):
|
|||||||
return next
|
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> {
|
export function normalizePriorityOverrides(overrides: Record<string, number>): Record<string, number> {
|
||||||
const normalized: Record<string, number> = {}
|
const normalized: Record<string, number> = {}
|
||||||
for (const [rawId, rawPriority] of Object.entries(overrides)) {
|
for (const [rawId, rawPriority] of Object.entries(overrides)) {
|
||||||
|
|||||||
@@ -323,6 +323,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="space-y-3 rounded-lg border border-border/60 p-4"
|
||||||
|
aria-labelledby="model-allowlist-heading"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<h3
|
||||||
|
id="model-allowlist-heading"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>
|
||||||
|
模型白名单
|
||||||
|
</h3>
|
||||||
|
<Badge :variant="draft.config_json.allowed_models.length ? 'outline' : 'secondary'">
|
||||||
|
{{ draft.config_json.allowed_models.length ? `${draft.config_json.allowed_models.length} 项` : '全部模型' }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
|
控制此策略分组适用于哪些模型;留空表示全部模型。它与“区分模型”中的专属调度覆盖相互独立,支持精确值、* 和前缀通配符(如 gpt-*),多个值用英文逗号或换行分隔。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="draft.config_json.allowed_models.length"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="shrink-0 text-muted-foreground hover:text-foreground"
|
||||||
|
data-testid="clear-allowed-models"
|
||||||
|
@click="clearAllowedModelScope"
|
||||||
|
>
|
||||||
|
改为全部模型
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row">
|
||||||
|
<Input
|
||||||
|
v-model="allowedModelsInput"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
data-testid="allowed-models-input"
|
||||||
|
aria-label="模型白名单"
|
||||||
|
placeholder="留空表示全部模型,例如:gpt-5, claude-*, legacy-model"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
class="shrink-0"
|
||||||
|
data-testid="apply-allowed-models"
|
||||||
|
@click="applyAllowedModelScope"
|
||||||
|
>
|
||||||
|
应用范围
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="draft.config_json.allowed_models.length"
|
||||||
|
class="flex flex-wrap gap-2"
|
||||||
|
data-testid="allowed-model-values"
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
v-for="(model, index) in draft.config_json.allowed_models"
|
||||||
|
:key="`${model}:${index}`"
|
||||||
|
variant="outline"
|
||||||
|
class="font-mono font-normal"
|
||||||
|
>
|
||||||
|
{{ model }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="allowedModelsLookLikeLegacyMirror"
|
||||||
|
class="rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs text-muted-foreground"
|
||||||
|
data-testid="allowed-models-legacy-mirror"
|
||||||
|
>
|
||||||
|
当前白名单与按模型策略列表一致,可能来自旧版界面的联动保存。现有范围会原样保留;如需让其他模型也使用默认策略,请显式点击“改为全部模型”。
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
v-if="sortingScope === 'unified'"
|
v-if="sortingScope === 'unified'"
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
@@ -667,21 +744,26 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuIte
|
|||||||
import { AlertDialog } from '@/components/common'
|
import { AlertDialog } from '@/components/common'
|
||||||
import {
|
import {
|
||||||
DEFAULT_ROUTING_POLICY_MODEL,
|
DEFAULT_ROUTING_POLICY_MODEL,
|
||||||
|
allowedModelsMirrorPerModelPolicies,
|
||||||
|
clearAllowedModels,
|
||||||
|
copyPerModelRoutingConfig,
|
||||||
createEmptyModelPolicy,
|
createEmptyModelPolicy,
|
||||||
createEmptyRoutingGroupConfig,
|
createEmptyRoutingGroupConfig,
|
||||||
getModelPolicy,
|
formatAllowedModelsInput,
|
||||||
getModelScheduling,
|
getModelScheduling,
|
||||||
isGeneratedModelSchedulingRule,
|
isGeneratedModelSchedulingRule,
|
||||||
modelSchedulingRuleId,
|
modelSchedulingRuleId,
|
||||||
normalizeRoutingGroupConfig,
|
normalizeRoutingGroupConfig,
|
||||||
removeGeneratedModelSchedulingRules,
|
removePerModelRoutingConfig,
|
||||||
removeModelPolicy,
|
routingModelScopeLabel,
|
||||||
removeModelSchedulingRule,
|
savePerModelRoutingConfig,
|
||||||
upsertModelPolicy,
|
setRoutingSortingScope,
|
||||||
|
updateAllowedModelsFromInput,
|
||||||
upsertModelSchedulingRule,
|
upsertModelSchedulingRule,
|
||||||
type RoutingGroupConfig,
|
type RoutingGroupConfig,
|
||||||
type RoutingPriorityMode,
|
type RoutingPriorityMode,
|
||||||
type RoutingSchedulingMode,
|
type RoutingSchedulingMode,
|
||||||
|
type RoutingSortingScope,
|
||||||
} from '@/features/routing/utils/routingPolicy'
|
} from '@/features/routing/utils/routingPolicy'
|
||||||
import { RoutingPriorityPolicyEditor } from '@/features/routing/components'
|
import { RoutingPriorityPolicyEditor } from '@/features/routing/components'
|
||||||
import {
|
import {
|
||||||
@@ -707,7 +789,6 @@ interface RoutingGroupDraft {
|
|||||||
updated_at?: number | null
|
updated_at?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortingScope = 'unified' | 'per_model'
|
|
||||||
type ModelFilter = 'configured' | 'unconfigured'
|
type ModelFilter = 'configured' | 'unconfigured'
|
||||||
|
|
||||||
const modelFilters: Array<{ value: ModelFilter; label: string }> = [
|
const modelFilters: Array<{ value: ModelFilter; label: string }> = [
|
||||||
@@ -729,9 +810,10 @@ const groups = ref<RoutingGroupRecord[]>([])
|
|||||||
const selectedGroupId = ref<string | null>(null)
|
const selectedGroupId = ref<string | null>(null)
|
||||||
const draft = ref<RoutingGroupDraft | null>(null)
|
const draft = ref<RoutingGroupDraft | null>(null)
|
||||||
const savedDraftSnapshot = ref<string | null>(null)
|
const savedDraftSnapshot = ref<string | null>(null)
|
||||||
const sortingScope = ref<SortingScope>('unified')
|
const sortingScope = ref<RoutingSortingScope>('unified')
|
||||||
const selectedPerModelName = ref<string | null>(null)
|
const selectedPerModelName = ref<string | null>(null)
|
||||||
const editingConfig = ref<RoutingGroupConfig | null>(null)
|
const editingConfig = ref<RoutingGroupConfig | null>(null)
|
||||||
|
const allowedModelsInput = ref('')
|
||||||
const globalModelSearch = ref('')
|
const globalModelSearch = ref('')
|
||||||
const modelFilter = ref<ModelFilter>('unconfigured')
|
const modelFilter = ref<ModelFilter>('unconfigured')
|
||||||
const globalModels = ref<GlobalModelResponse[]>([])
|
const globalModels = ref<GlobalModelResponse[]>([])
|
||||||
@@ -773,6 +855,11 @@ const firstStepSchedulingMode = computed<RoutingSchedulingMode>(() => {
|
|||||||
}
|
}
|
||||||
return draft.value?.config_json.default_policy.scheduling_mode ?? 'cache_affinity'
|
return draft.value?.config_json.default_policy.scheduling_mode ?? 'cache_affinity'
|
||||||
})
|
})
|
||||||
|
const allowedModelsLookLikeLegacyMirror = computed(() => {
|
||||||
|
return draft.value
|
||||||
|
? allowedModelsMirrorPerModelPolicies(draft.value.config_json)
|
||||||
|
: false
|
||||||
|
})
|
||||||
|
|
||||||
interface ModelRow {
|
interface ModelRow {
|
||||||
name: string
|
name: string
|
||||||
@@ -869,6 +956,7 @@ function clearDraftState(): void {
|
|||||||
savedDraftSnapshot.value = null
|
savedDraftSnapshot.value = null
|
||||||
selectedPerModelName.value = null
|
selectedPerModelName.value = null
|
||||||
editingConfig.value = null
|
editingConfig.value = null
|
||||||
|
allowedModelsInput.value = ''
|
||||||
switchModelTarget.value = null
|
switchModelTarget.value = null
|
||||||
switchModelDialogOpen.value = false
|
switchModelDialogOpen.value = false
|
||||||
deleteDialogOpen.value = false
|
deleteDialogOpen.value = false
|
||||||
@@ -879,6 +967,7 @@ function selectGroup(group: RoutingGroupRecord): void {
|
|||||||
isCreating.value = false
|
isCreating.value = false
|
||||||
selectedGroupId.value = normalized.id
|
selectedGroupId.value = normalized.id
|
||||||
draft.value = buildDraft(normalized)
|
draft.value = buildDraft(normalized)
|
||||||
|
allowedModelsInput.value = formatAllowedModelsInput(draft.value.config_json.allowed_models)
|
||||||
savedDraftSnapshot.value = draftSnapshotValue(draft.value)
|
savedDraftSnapshot.value = draftSnapshotValue(draft.value)
|
||||||
syncEditorStateFromConfig(draft.value.config_json)
|
syncEditorStateFromConfig(draft.value.config_json)
|
||||||
resetEditingConfig()
|
resetEditingConfig()
|
||||||
@@ -902,6 +991,7 @@ function startCreate(): void {
|
|||||||
updated_at: null,
|
updated_at: null,
|
||||||
}
|
}
|
||||||
savedDraftSnapshot.value = null
|
savedDraftSnapshot.value = null
|
||||||
|
allowedModelsInput.value = ''
|
||||||
syncEditorStateFromConfig(draft.value.config_json)
|
syncEditorStateFromConfig(draft.value.config_json)
|
||||||
resetEditingConfig()
|
resetEditingConfig()
|
||||||
}
|
}
|
||||||
@@ -958,13 +1048,7 @@ function groupSortingScopeLabel(group: RoutingGroupRecord): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function groupModelScopeLabel(group: RoutingGroupRecord): string {
|
function groupModelScopeLabel(group: RoutingGroupRecord): string {
|
||||||
const config = normalizeRoutingGroupConfig(group.config_json)
|
return routingModelScopeLabel(group.config_json)
|
||||||
if (hasPerModelSorting(config)) {
|
|
||||||
const count = config.model_policies.filter(policy => policy.model !== DEFAULT_ROUTING_POLICY_MODEL).length
|
|
||||||
|| config.allowed_models.length
|
|
||||||
return count ? `${count} 个模型` : '未选择模型'
|
|
||||||
}
|
|
||||||
return config.allowed_models.length ? `${config.allowed_models.length} 个模型` : '全部模型'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupSchedulingSummary(group: RoutingGroupRecord): string {
|
function groupSchedulingSummary(group: RoutingGroupRecord): string {
|
||||||
@@ -1042,13 +1126,11 @@ function hasPerModelSorting(config: RoutingGroupConfig): boolean {
|
|||||||
|| config.rules.some(isGeneratedModelSchedulingRule)
|
|| config.rules.some(isGeneratedModelSchedulingRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSortingScope(scope: SortingScope): void {
|
function setSortingScope(scope: RoutingSortingScope): void {
|
||||||
if (!draft.value) return
|
if (!draft.value) return
|
||||||
sortingScope.value = scope
|
sortingScope.value = scope
|
||||||
if (scope === 'unified') {
|
if (scope === 'unified') {
|
||||||
const next = removeGeneratedModelSchedulingRules(draft.value.config_json)
|
const next = setRoutingSortingScope(draft.value.config_json, scope)
|
||||||
next.model_policies = next.model_policies.filter(policy => policy.model === DEFAULT_ROUTING_POLICY_MODEL)
|
|
||||||
next.allowed_models = []
|
|
||||||
updateDraftConfig(next)
|
updateDraftConfig(next)
|
||||||
resetEditingConfig()
|
resetEditingConfig()
|
||||||
return
|
return
|
||||||
@@ -1092,9 +1174,7 @@ function removePerModelPolicy(model: string): void {
|
|||||||
showError('请先保存当前改动后再移除模型')
|
showError('请先保存当前改动后再移除模型')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let next = removeModelPolicy(draft.value.config_json, model)
|
const next = removePerModelRoutingConfig(draft.value.config_json, model)
|
||||||
next = removeModelSchedulingRule(next, model)
|
|
||||||
next.allowed_models = next.allowed_models.filter(item => item !== model)
|
|
||||||
if (selectedPerModelName.value === model) {
|
if (selectedPerModelName.value === model) {
|
||||||
selectedPerModelName.value = null
|
selectedPerModelName.value = null
|
||||||
}
|
}
|
||||||
@@ -1157,19 +1237,12 @@ function copyModelConfig(sourceModel: string): void {
|
|||||||
if (!draft.value || !editingConfig.value) return
|
if (!draft.value || !editingConfig.value) return
|
||||||
const target = selectedPerModelName.value
|
const target = selectedPerModelName.value
|
||||||
if (!target || target === sourceModel) return
|
if (!target || target === sourceModel) return
|
||||||
const sourcePolicy = getModelPolicy(draft.value.config_json, sourceModel)
|
const next = copyPerModelRoutingConfig(
|
||||||
const sourceScheduling = getModelScheduling(draft.value.config_json, sourceModel)
|
editingConfig.value,
|
||||||
let next = upsertModelPolicy(editingConfig.value, {
|
draft.value.config_json,
|
||||||
...sourcePolicy,
|
sourceModel,
|
||||||
model: target,
|
target,
|
||||||
})
|
)
|
||||||
next = upsertModelSchedulingRule(next, target, {
|
|
||||||
priority_mode: sourceScheduling.priority_mode,
|
|
||||||
scheduling_mode: sourceScheduling.scheduling_mode,
|
|
||||||
})
|
|
||||||
if (!next.allowed_models.includes(target)) {
|
|
||||||
next = { ...next, allowed_models: [...next.allowed_models, target] }
|
|
||||||
}
|
|
||||||
updateEditingConfig(next)
|
updateEditingConfig(next)
|
||||||
success(`已加载 ${globalModelLabel(sourceModel)} 的配置,点击保存生效`)
|
success(`已加载 ${globalModelLabel(sourceModel)} 的配置,点击保存生效`)
|
||||||
}
|
}
|
||||||
@@ -1236,6 +1309,26 @@ function globalModelLabel(modelName: string): string {
|
|||||||
return `${model.display_name} (${model.name})`
|
return `${model.display_name} (${model.name})`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyAllowedModelScope(): void {
|
||||||
|
if (!draft.value) return
|
||||||
|
const next = updateAllowedModelsFromInput(draft.value.config_json, allowedModelsInput.value)
|
||||||
|
updateDraftConfig(next)
|
||||||
|
if (editingConfig.value) {
|
||||||
|
editingConfig.value = updateAllowedModelsFromInput(editingConfig.value, allowedModelsInput.value)
|
||||||
|
}
|
||||||
|
allowedModelsInput.value = formatAllowedModelsInput(next.allowed_models)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllowedModelScope(): void {
|
||||||
|
if (!draft.value) return
|
||||||
|
const next = clearAllowedModels(draft.value.config_json)
|
||||||
|
updateDraftConfig(next)
|
||||||
|
if (editingConfig.value) {
|
||||||
|
editingConfig.value = clearAllowedModels(editingConfig.value)
|
||||||
|
}
|
||||||
|
allowedModelsInput.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
function replaceGroup(group: RoutingGroupRecord): void {
|
function replaceGroup(group: RoutingGroupRecord): void {
|
||||||
const normalized = normalizeRecord(group)
|
const normalized = normalizeRecord(group)
|
||||||
const index = groups.value.findIndex(item => item.id === normalized.id)
|
const index = groups.value.findIndex(item => item.id === normalized.id)
|
||||||
@@ -1326,13 +1419,7 @@ function saveCurrentModel(): void {
|
|||||||
showError('请先选择模型')
|
showError('请先选择模型')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let next = editingConfig.value
|
const next = savePerModelRoutingConfig(editingConfig.value, model)
|
||||||
if (!next.model_policies.some(policy => policy.model === model)) {
|
|
||||||
next = upsertModelPolicy(next, createEmptyModelPolicy(model))
|
|
||||||
}
|
|
||||||
if (!next.allowed_models.includes(model)) {
|
|
||||||
next = { ...next, allowed_models: [...next.allowed_models, model] }
|
|
||||||
}
|
|
||||||
updateDraftConfig(next)
|
updateDraftConfig(next)
|
||||||
modelFilter.value = 'configured'
|
modelFilter.value = 'configured'
|
||||||
resetEditingConfig()
|
resetEditingConfig()
|
||||||
|
|||||||
Reference in New Issue
Block a user