diff --git a/apps/ui-admin/src/pages/LlmRouterPage.test.ts b/apps/ui-admin/src/pages/LlmRouterPage.test.ts index 54cf12398..2a0e2c5bc 100644 --- a/apps/ui-admin/src/pages/LlmRouterPage.test.ts +++ b/apps/ui-admin/src/pages/LlmRouterPage.test.ts @@ -74,6 +74,9 @@ describe('llm router page', () => { expect(host.textContent).toContain('Provider configuration') expect(host.textContent).toContain('LLM') expect(host.textContent).toContain('TTS') + expect(host.textContent).toContain('Streaming TTS') + expect(host.textContent).toContain('ASR') + expect(host.textContent).not.toContain('LLM and TTS providers') expect(host.textContent).toContain('OpenRouter') expect(host.textContent).not.toContain('Router Config Request') }) @@ -124,7 +127,7 @@ describe('llm router page', () => { expect(host.textContent).toContain('Current router config response is not a valid JSON object.') }) - it('separates LLM and TTS provider slices by tab', async () => { + it('separates LLM, TTS, Streaming TTS, and ASR provider slices by tab', async () => { buttonByText('TTS').click() await nextTick() @@ -135,6 +138,28 @@ describe('llm router page', () => { await nextTick() expect(host.textContent).toContain('Azure Speech') + + buttonByText('Streaming TTS').click() + await nextTick() + + expect(host.textContent).toContain('No Streaming TTS provider slices') + expect(sectionHeadings(host)).not.toContain('1. Azure Speech') + + buttonByText('Add').click() + await nextTick() + + expect(host.textContent).toContain('UnSpeech') + + buttonByText('ASR').click() + await nextTick() + + expect(host.textContent).toContain('No ASR provider slices') + expect(sectionHeadings(host)).not.toContain('1. UnSpeech') + + buttonByText('Add').click() + await nextTick() + + expect(host.textContent).toContain('Aliyun NLS ASR') }) it('keeps preview disabled until required fields are complete', async () => { diff --git a/apps/ui-admin/src/pages/LlmRouterPage.vue b/apps/ui-admin/src/pages/LlmRouterPage.vue index 02be39614..c6bfee22a 100644 --- a/apps/ui-admin/src/pages/LlmRouterPage.vue +++ b/apps/ui-admin/src/pages/LlmRouterPage.vue @@ -26,13 +26,15 @@ import { } from '../modules/router-config-form' type BusyState = 'preview' | 'apply' | 'advanced-preview' | 'advanced-apply' -type ProviderTab = 'llm' | 'tts' +type ProviderTab = 'llm' | 'tts' | 'streamingTts' | 'asr' const form = reactive(createRouterConfigFormState()) const activeProviderTab = shallowRef('llm') const selectedKindByTab = reactive>({ llm: 'openrouter', tts: 'azure', + streamingTts: 'unspeech', + asr: 'aliyun-nls-asr', }) const previewResult = shallowRef(null) const applyResult = shallowRef(null) @@ -57,26 +59,39 @@ const pendingSummary = computed(() => { return { slices: form.slices.length, llmSlices: form.slices.filter(slice => slice.kind === 'openrouter').length, - ttsSlices: form.slices.filter(slice => slice.kind !== 'openrouter' && slice.kind !== 'unspeech').length, - unspeech: form.slices.some(slice => slice.kind === 'unspeech'), + ttsSlices: form.slices.filter(isTtsSlice).length, + streamingTtsSlices: form.slices.filter(isStreamingTtsSlice).length, + asrSlices: form.slices.filter(isAsrSlice).length, defaults: Object.keys(defaults).length, } }) const providerTabs = computed(() => [ { value: 'llm' as const, label: 'LLM', count: pendingSummary.value.llmSlices }, - { value: 'tts' as const, label: 'TTS', count: pendingSummary.value.ttsSlices + (pendingSummary.value.unspeech ? 1 : 0) }, + { value: 'tts' as const, label: 'TTS', count: pendingSummary.value.ttsSlices }, + { value: 'streamingTts' as const, label: 'Streaming TTS', count: pendingSummary.value.streamingTtsSlices }, + { value: 'asr' as const, label: 'ASR', count: pendingSummary.value.asrSlices }, ]) const providerKindOptions = computed(() => ROUTER_SLICE_KIND_OPTIONS.filter((option) => { if (activeProviderTab.value === 'llm') return option.value === 'openrouter' + if (activeProviderTab.value === 'streamingTts') + return option.value === 'unspeech' + if (activeProviderTab.value === 'asr') + return option.value === 'aliyun-nls-asr' - return option.value !== 'openrouter' + return isTtsSliceKind(option.value) })) const visibleSlices = computed(() => form.slices .map((slice, index) => ({ slice, index })) - .filter(({ slice }) => activeProviderTab.value === 'llm' - ? isLlmSlice(slice) - : !isLlmSlice(slice))) + .filter(({ slice }) => { + if (activeProviderTab.value === 'llm') + return isLlmSlice(slice) + if (activeProviderTab.value === 'streamingTts') + return isStreamingTtsSlice(slice) + if (activeProviderTab.value === 'asr') + return isAsrSlice(slice) + return isTtsSlice(slice) + })) const currentStatusLabel = computed(() => { if (loadingCurrent.value) return 'Loading current config' @@ -237,6 +252,37 @@ function isLlmSlice(slice: RouterSliceDraft) { return slice.kind === 'openrouter' } +function isTtsSlice(slice: RouterSliceDraft) { + return isTtsSliceKind(slice.kind) +} + +function isStreamingTtsSlice(slice: RouterSliceDraft) { + return slice.kind === 'unspeech' +} + +function isAsrSlice(slice: RouterSliceDraft) { + return slice.kind === 'aliyun-nls-asr' +} + +function isTtsSliceKind(kind: RouterSliceKind) { + return kind === 'azure' + || kind === 'dashscope-cosyvoice' + || kind === 'stepfun' +} + +function activeProviderLabel() { + switch (activeProviderTab.value) { + case 'llm': + return 'LLM' + case 'tts': + return 'TTS' + case 'streamingTts': + return 'Streaming TTS' + case 'asr': + return 'ASR' + } +} + function isCurrentConfigResponse(value: AdminRouterConfigCurrent | null): value is AdminRouterConfigCurrent { return value != null && typeof value === 'object' @@ -300,7 +346,7 @@ function isCurrentConfigResponse(value: AdminRouterConfigCurrent | null): value Provider configuration

- LLM and TTS providers are edited separately, then applied as one router config request. + LLM, TTS, Streaming TTS, and ASR providers are edited separately, then applied as one router config request.

@@ -308,7 +354,7 @@ function isCurrentConfigResponse(value: AdminRouterConfigCurrent | null): value
- No {{ activeProviderTab === 'llm' ? 'LLM' : 'TTS' }} provider slices + No {{ activeProviderLabel() }} provider slices
- UnSpeech is limited to one TTS slice per request. + UnSpeech is limited to one Streaming TTS slice per request. ASR writes to LLM_ROUTER_CONFIG.asr.
@@ -382,7 +434,7 @@ function isCurrentConfigResponse(value: AdminRouterConfigCurrent | null): value
-
+
{{ pendingSummary.slices }} slices @@ -396,8 +448,12 @@ function isCurrentConfigResponse(value: AdminRouterConfigCurrent | null): value TTS
- {{ pendingSummary.unspeech ? 'yes' : 'no' }} - unspeech + {{ pendingSummary.streamingTtsSlices }} + Streaming TTS +
+
+ {{ pendingSummary.asrSlices }} + ASR
{{ pendingSummary.defaults }}