mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +08:00
fix(routing): 按调度配置所选模型筛选提供商
This commit is contained in:
+166
@@ -0,0 +1,166 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createApp, h, nextTick, ref, type App } from 'vue'
|
||||||
|
import { getProvidersSummary, type ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||||
|
import type { GlobalModelResponse } from '@/api/global-models'
|
||||||
|
import RoutingSchedulingPolicyEditor from '../components/RoutingSchedulingPolicyEditor.vue'
|
||||||
|
import {
|
||||||
|
createEmptyRoutingGroupConfig,
|
||||||
|
getModelPolicy,
|
||||||
|
type RoutingGroupConfig,
|
||||||
|
} from '../utils/routingPolicy'
|
||||||
|
import { createSchedulingPolicy, writeSchedulingPolicies } from '../utils/schedulingPolicies'
|
||||||
|
|
||||||
|
vi.mock('@/api/endpoints', () => ({ getProvidersSummary: vi.fn() }))
|
||||||
|
|
||||||
|
const globalModels = ['a', 'b', 'c'].map(name => ({
|
||||||
|
id: `id-${name}`, name: `model-${name}`, display_name: `模型 ${name.toUpperCase()}`,
|
||||||
|
})) as GlobalModelResponse[]
|
||||||
|
const providerSources = [
|
||||||
|
{ id: 'provider-a', name: '提供商 A', global_model_ids: ['id-a'] },
|
||||||
|
{ id: 'provider-b', name: '提供商 B', global_model_ids: ['id-b'] },
|
||||||
|
{ id: 'provider-shared', name: '共享提供商', global_model_ids: ['id-a', 'id-b'] },
|
||||||
|
{ id: 'provider-other', name: '无关提供商', global_model_ids: ['id-other'] },
|
||||||
|
].map((provider, index) => ({
|
||||||
|
...provider,
|
||||||
|
provider_priority: index,
|
||||||
|
is_active: true,
|
||||||
|
api_formats: ['openai:chat'],
|
||||||
|
})) as ProviderWithEndpointsSummary[]
|
||||||
|
const mounted: Array<{ app: App, root: HTMLElement }> = []
|
||||||
|
|
||||||
|
function mountEditor(selectedModels?: string[], initialModels = globalModels) {
|
||||||
|
const initial = createEmptyRoutingGroupConfig()
|
||||||
|
const config = ref(selectedModels === undefined ? initial : writeSchedulingPolicies(initial, [{
|
||||||
|
...createSchedulingPolicy(initial, 'selected'),
|
||||||
|
models: selectedModels,
|
||||||
|
}]))
|
||||||
|
const models = ref(initialModels)
|
||||||
|
const root = document.createElement('div')
|
||||||
|
document.body.appendChild(root)
|
||||||
|
const app = createApp({
|
||||||
|
setup: () => () => h(RoutingSchedulingPolicyEditor, {
|
||||||
|
config: config.value,
|
||||||
|
globalModels: models.value,
|
||||||
|
'onUpdate:config': (value: RoutingGroupConfig) => { config.value = value },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
app.mount(root)
|
||||||
|
mounted.push({ app, root })
|
||||||
|
return { root, config, models }
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerNames(root: HTMLElement): string[] {
|
||||||
|
return [...root.querySelectorAll('[draggable="true"] .font-medium')]
|
||||||
|
.map(element => element.textContent?.trim() ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickButton(root: HTMLElement, label: string) {
|
||||||
|
const button = [...root.querySelectorAll<HTMLButtonElement>('button')]
|
||||||
|
.find(element => element.getAttribute('aria-label') === label || element.textContent?.trim() === label)
|
||||||
|
expect(button, `Missing button: ${label}`).toBeTruthy()
|
||||||
|
button!.click()
|
||||||
|
await nextTick()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleModel(root: HTMLElement, name: string) {
|
||||||
|
if (!root.querySelector('[aria-label="全局模型选择列表"]')) {
|
||||||
|
await clickButton(root, '选择适用模型')
|
||||||
|
}
|
||||||
|
const checkbox = root.querySelector<HTMLInputElement>(`[aria-label="选择模型 ${name}"]`)
|
||||||
|
expect(checkbox).toBeTruthy()
|
||||||
|
checkbox!.click()
|
||||||
|
await nextTick()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(getProvidersSummary).mockReset()
|
||||||
|
vi.mocked(getProvidersSummary).mockResolvedValue({
|
||||||
|
items: providerSources, total: providerSources.length, page: 1, page_size: 9999,
|
||||||
|
})
|
||||||
|
vi.stubGlobal('ResizeObserver', class {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const { app, root } of mounted.splice(0)) {
|
||||||
|
app.unmount()
|
||||||
|
root.remove()
|
||||||
|
}
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('scheduling provider filtering', () => {
|
||||||
|
it('filters providers after choosing a global model and restores all-model mode', async () => {
|
||||||
|
const { root } = mountEditor()
|
||||||
|
await vi.waitFor(() => expect(providerNames(root)).toHaveLength(4))
|
||||||
|
await clickButton(root, '区分模型')
|
||||||
|
expect(providerNames(root)).toEqual([])
|
||||||
|
await toggleModel(root, 'model-a')
|
||||||
|
await vi.waitFor(() => expect(providerNames(root)).toEqual(['提供商 A', '共享提供商']))
|
||||||
|
await clickButton(root, '全部模型')
|
||||||
|
await vi.waitFor(() => expect(providerNames(root)).toHaveLength(4))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the union for multiple models once and updates immediately when deselected', async () => {
|
||||||
|
const { root } = mountEditor(['model-a'])
|
||||||
|
await vi.waitFor(() => expect(providerNames(root)).toEqual(['提供商 A', '共享提供商']))
|
||||||
|
await toggleModel(root, 'model-b')
|
||||||
|
expect(providerNames(root)).toEqual(['提供商 A', '提供商 B', '共享提供商'])
|
||||||
|
await toggleModel(root, 'model-a')
|
||||||
|
expect(providerNames(root)).toEqual(['提供商 B', '共享提供商'])
|
||||||
|
expect(getProvidersSummary).toHaveBeenCalledTimes(1)
|
||||||
|
await toggleModel(root, 'model-b')
|
||||||
|
expect(providerNames(root)).toEqual([])
|
||||||
|
expect(root.textContent).not.toContain('提供商排序')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the shared ranking attached to every selected model after filtering', async () => {
|
||||||
|
const { root, config } = mountEditor(['model-a', 'model-b'])
|
||||||
|
await vi.waitFor(() => expect(providerNames(root)).toHaveLength(3))
|
||||||
|
const row = [...root.querySelectorAll<HTMLElement>('[draggable="true"]')]
|
||||||
|
.find(element => element.textContent?.includes('共享提供商'))!
|
||||||
|
const input = row.querySelector<HTMLInputElement>('input[type="number"]')!
|
||||||
|
input.value = '7'
|
||||||
|
input.dispatchEvent(new Event('change', { bubbles: true }))
|
||||||
|
await nextTick()
|
||||||
|
for (const model of ['model-a', 'model-b']) {
|
||||||
|
expect(getModelPolicy(config.value, model).provider_priority_overrides).toEqual({ 'provider-shared': 7 })
|
||||||
|
}
|
||||||
|
expect(getModelPolicy(config.value, '*').provider_priority_overrides).toEqual({})
|
||||||
|
expect(providerNames(root)).toEqual(['提供商 A', '提供商 B', '共享提供商'])
|
||||||
|
expect(getProvidersSummary).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('waits for global model IDs without briefly displaying all providers', async () => {
|
||||||
|
const { root, models } = mountEditor(['model-a'], [])
|
||||||
|
await vi.waitFor(() => expect(root.textContent).toContain('暂无 Provider'))
|
||||||
|
expect(providerNames(root)).toEqual([])
|
||||||
|
models.value = globalModels
|
||||||
|
await nextTick()
|
||||||
|
expect(providerNames(root)).toEqual(['提供商 A', '共享提供商'])
|
||||||
|
expect(getProvidersSummary).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(['model-c', 'missing-model'])('shows an empty list when no providers match %s', async model => {
|
||||||
|
const { root } = mountEditor([model])
|
||||||
|
await vi.waitFor(() => expect(root.textContent).toContain('暂无 Provider'))
|
||||||
|
expect(providerNames(root)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drops hidden providers from the temporary multiselection when models change', async () => {
|
||||||
|
const { root } = mountEditor(['model-a', 'model-b'])
|
||||||
|
await vi.waitFor(() => expect(providerNames(root)).toHaveLength(3))
|
||||||
|
await clickButton(root, '多选')
|
||||||
|
const checkbox = root.querySelector<HTMLInputElement>('[aria-label="选择 提供商 A"]')!
|
||||||
|
checkbox.click()
|
||||||
|
await nextTick()
|
||||||
|
expect(checkbox.checked).toBe(true)
|
||||||
|
await toggleModel(root, 'model-a')
|
||||||
|
expect(providerNames(root)).toEqual(['提供商 B', '共享提供商'])
|
||||||
|
await toggleModel(root, 'model-a')
|
||||||
|
expect(root.querySelector<HTMLInputElement>('[aria-label="选择 提供商 A"]')!.checked).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -369,6 +369,7 @@ const props = defineProps<{
|
|||||||
config: RoutingGroupConfig
|
config: RoutingGroupConfig
|
||||||
model?: string
|
model?: string
|
||||||
modelId?: string
|
modelId?: string
|
||||||
|
providerModelIds?: string[]
|
||||||
priorityMode?: RoutingPriorityMode
|
priorityMode?: RoutingPriorityMode
|
||||||
schedulingMode?: RoutingSchedulingMode
|
schedulingMode?: RoutingSchedulingMode
|
||||||
showPriorityMode?: boolean
|
showPriorityMode?: boolean
|
||||||
@@ -445,7 +446,10 @@ const poolProviderIds = computed(() => {
|
|||||||
|
|
||||||
const providerRows = computed<ProviderPriorityRow[]>(() => {
|
const providerRows = computed<ProviderPriorityRow[]>(() => {
|
||||||
const overrides = targetModelPolicy.value.provider_priority_overrides
|
const overrides = targetModelPolicy.value.provider_priority_overrides
|
||||||
|
// 多选模型取提供商并集;空数组表示模型尚未解析,不能回退到全部提供商。
|
||||||
|
const modelIds = props.providerModelIds === undefined ? null : new Set(props.providerModelIds)
|
||||||
return providers.value
|
return providers.value
|
||||||
|
.filter(provider => !modelIds || provider.global_model_ids?.some(id => modelIds.has(id)))
|
||||||
.map(provider => ({
|
.map(provider => ({
|
||||||
id: provider.id,
|
id: provider.id,
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
|
|||||||
@@ -178,6 +178,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<RoutingPriorityPolicyEditor
|
<RoutingPriorityPolicyEditor
|
||||||
:config="schedulingPolicyEditorConfig(config, entry)"
|
:config="schedulingPolicyEditorConfig(config, entry)"
|
||||||
|
:provider-model-ids="entry.scope === 'selected' ? providerModelIds(entry) : undefined"
|
||||||
:show-priority-mode="false"
|
:show-priority-mode="false"
|
||||||
:show-scheduling-mode="false"
|
:show-scheduling-mode="false"
|
||||||
subtitle="所选模型共用此排序,仅对各模型可用的候选生效"
|
subtitle="所选模型共用此排序,仅对各模型可用的候选生效"
|
||||||
@@ -294,6 +295,11 @@ function otherModelOwners(entryId: string): Record<string, number> {
|
|||||||
: []))
|
: []))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function providerModelIds(entry: SchedulingPolicy): string[] {
|
||||||
|
const names = new Set(entry.models)
|
||||||
|
return props.globalModels.filter(model => names.has(model.name)).map(model => model.id)
|
||||||
|
}
|
||||||
|
|
||||||
function publish(): void {
|
function publish(): void {
|
||||||
emit('update:config', writeSchedulingPolicies({
|
emit('update:config', writeSchedulingPolicies({
|
||||||
...props.config,
|
...props.config,
|
||||||
|
|||||||
Reference in New Issue
Block a user