Skip to content

Commit 66beb66

Browse files
committed
feat(stage-ui): refactor speech/transcription providers to unified defineProvider pattern
Refactor OpenAI and OpenAI-compatible speech/transcription providers to use the unified `defineProvider()` pattern (matching PR #968 n1n provider pattern) instead of separate registries. This unifies all providers under a single consistent API. Changes: - Refactor OpenAI speech/transcription providers to use `defineProvider()` with `tasks` and `extraMethods` instead of `defineSpeechProvider`/`defineTranscriptionProvider` - Refactor OpenAI-compatible speech/transcription providers similarly - Add `convertProviderDefinitionToMetadata()` converter function to bridge unified pattern with existing store's `ProviderMetadata` format - Update store to use new converter function instead of old separate converters - Mark old base interfaces and registry exports as deprecated for backward compatibility - Fix settings index page to filter out routes with empty titles (preventing empty menu items from rendering) BREAKING CHANGE: Speech and transcription providers now use the unified `defineProvider()` pattern. Old `defineSpeechProvider` and `defineTranscriptionProvider` functions are deprecated but still available for backward compatibility. Refs: PR #961, PR #968
1 parent 95849b0 commit 66beb66

File tree

10 files changed

+560
-398
lines changed

10 files changed

+560
-398
lines changed

packages/stage-pages/src/pages/settings/index.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const settings = computed(() => {
3737
icon: route.meta?.icon as string | undefined,
3838
to: route.path,
3939
}))
40+
.filter(setting => setting.title)
4041
})
4142
</script>
4243

packages/stage-ui/src/libs/providers/base-speech.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export interface BaseSpeechProviderConfig {
2323
/**
2424
* Base interface that all speech provider implementations should follow.
2525
*
26+
* @deprecated Use the unified `defineProvider()` pattern instead (see `providers/registry.ts`).
27+
* This interface is kept for backward compatibility with existing converters.
28+
*
2629
* This provides a consistent contract for speech/TTS providers beyond
2730
* what's defined in the external @xsai-ext/providers/utils library.
2831
*

packages/stage-ui/src/libs/providers/base-transcription.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export interface BaseTranscriptionProviderConfig {
2828
/**
2929
* Base interface that all transcription provider implementations should follow.
3030
*
31+
* @deprecated Use the unified `defineProvider()` pattern instead (see `providers/registry.ts`).
32+
* This interface is kept for backward compatibility with existing converters.
33+
*
3134
* This provides a consistent contract for transcription providers beyond
3235
* what's defined in the external @xsai-ext/providers/utils library.
3336
*

packages/stage-ui/src/libs/providers/providers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export {
3030
listProviders,
3131
} from './registry'
3232

33+
// Legacy exports for backward compatibility (deprecated - use unified defineProvider pattern)
34+
// These will be removed in a future version
3335
export {
3436
defineSpeechProvider,
3537
getDefinedSpeechProvider,

packages/stage-ui/src/libs/providers/providers/openai-compatible/speech.ts

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,77 +4,82 @@ import type {
44
} from '@xsai-ext/providers/utils'
55

66
import type { ModelInfo, VoiceInfo } from '../../../../stores/providers'
7-
import type {
8-
BaseSpeechProviderConfig,
9-
} from '../../base-speech'
10-
import type { ProviderValidationResult } from '../../base-types'
117

128
import { createSpeechProvider } from '@xsai-ext/providers/utils'
9+
import { z } from 'zod'
1310

1411
import { normalizeBaseUrl } from '../../utils'
15-
import { defineSpeechProvider } from '../registry-speech'
12+
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
13+
import { defineProvider } from '../registry'
14+
15+
const openAICompatibleSpeechConfigSchema = z.object({
16+
apiKey: z.string('API Key'),
17+
baseUrl: z.string('Base URL'),
18+
})
19+
20+
type OpenAICompatibleSpeechConfig = z.input<typeof openAICompatibleSpeechConfigSchema>
1621

1722
/**
1823
* OpenAI Compatible Speech/TTS Provider Implementation
1924
*
20-
* Implements BaseSpeechProviderDefinition for any API that follows the OpenAI specification.
25+
* Uses the unified defineProvider pattern for any API that follows the OpenAI specification.
2126
* This is a generic implementation that works with OpenAI-compatible endpoints.
2227
*/
23-
export const openaiCompatibleSpeechProvider = defineSpeechProvider({
28+
export const providerOpenAICompatibleSpeech = defineProvider<OpenAICompatibleSpeechConfig>({
2429
id: 'openai-compatible-audio-speech',
25-
defaultModel: 'tts-1',
26-
defaultVoice: 'alloy',
27-
28-
async validateConfig(config: BaseSpeechProviderConfig): Promise<ProviderValidationResult> {
29-
const errors: Error[] = []
30-
31-
if (!config.apiKey) {
32-
errors.push(new Error('API Key is required'))
33-
}
30+
order: 3,
31+
name: 'OpenAI Compatible',
32+
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openai-compatible.title'),
33+
description: 'OpenAI-compatible text-to-speech API',
34+
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.openai-compatible.description'),
35+
tasks: ['text-to-speech', 'speech'],
36+
icon: 'i-lobe-icons:openai',
3437

35-
if (!config.baseUrl) {
36-
errors.push(new Error('Base URL is required'))
37-
}
38+
createProviderConfig: ({ t }) => openAICompatibleSpeechConfigSchema.extend({
39+
apiKey: openAICompatibleSpeechConfigSchema.shape.apiKey.meta({
40+
labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.label'),
41+
descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.description'),
42+
placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.placeholder'),
43+
type: 'password',
44+
}),
45+
baseUrl: openAICompatibleSpeechConfigSchema.shape.baseUrl.meta({
46+
labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.label'),
47+
descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.description'),
48+
placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.placeholder'),
49+
}),
50+
}),
3851

39-
if (errors.length > 0) {
40-
return {
41-
errors,
42-
reason: errors.map(e => e.message).join(', '),
43-
valid: false,
44-
}
45-
}
46-
47-
return {
48-
errors: [],
49-
reason: '',
50-
valid: true,
51-
}
52-
},
53-
54-
async createProvider(config: BaseSpeechProviderConfig) {
52+
createProvider(config) {
5553
const apiKey = typeof config.apiKey === 'string' ? config.apiKey.trim() : ''
5654
const baseUrl = normalizeBaseUrl(config.baseUrl)
5755

5856
return createSpeechProvider({ apiKey, baseURL: baseUrl }) as SpeechProvider | SpeechProviderWithExtraOptions<string, any>
5957
},
6058

61-
async listModels(_config: BaseSpeechProviderConfig): Promise<ModelInfo[]> {
62-
// OpenAI Compatible providers don't have hardcoded models
63-
// Models are typically discovered via the API
64-
return []
59+
validationRequiredWhen(config) {
60+
return !!config.apiKey?.trim()
6561
},
6662

67-
async listVoices(_config: BaseSpeechProviderConfig): Promise<VoiceInfo[]> {
68-
// OpenAI Compatible providers don't have hardcoded voices
69-
// Voices are typically discovered via the API or user-configured
70-
return []
63+
validators: {
64+
...createOpenAICompatibleValidators<OpenAICompatibleSpeechConfig>({
65+
checks: ['connectivity'],
66+
}),
7167
},
7268

73-
getDefaultConfig(): Partial<BaseSpeechProviderConfig> {
74-
return {}
75-
},
69+
extraMethods: {
70+
async listModels(_config): Promise<ModelInfo[]> {
71+
// OpenAI Compatible providers don't have hardcoded models
72+
// Models are typically discovered via the API
73+
return []
74+
},
7675

77-
supportsSSML(): boolean {
78-
return false
76+
async listVoices(_config): Promise<VoiceInfo[]> {
77+
// OpenAI Compatible providers don't have hardcoded voices
78+
// Voices are typically discovered via the API or user-configured
79+
return []
80+
},
7981
},
8082
})
83+
84+
// Keep export for backward compatibility during migration
85+
export const openaiCompatibleSpeechProvider = providerOpenAICompatibleSpeech

packages/stage-ui/src/libs/providers/providers/openai-compatible/transcription.ts

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,71 +4,85 @@ import type {
44
} from '@xsai-ext/providers/utils'
55

66
import type { ModelInfo } from '../../../../stores/providers'
7-
import type {
8-
BaseTranscriptionProviderConfig,
9-
} from '../../base-transcription'
10-
import type { ProviderValidationResult } from '../../base-types'
117

128
import { createTranscriptionProvider } from '@xsai-ext/providers/utils'
9+
import { z } from 'zod'
1310

1411
import { normalizeBaseUrl } from '../../utils'
15-
import { defineTranscriptionProvider } from '../registry-transcription'
12+
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
13+
import { defineProvider } from '../registry'
14+
15+
const openAICompatibleTranscriptionConfigSchema = z.object({
16+
apiKey: z.string('API Key'),
17+
baseUrl: z.string('Base URL'),
18+
})
19+
20+
type OpenAICompatibleTranscriptionConfig = z.input<typeof openAICompatibleTranscriptionConfigSchema>
1621

1722
/**
1823
* OpenAI Compatible Transcription/STT Provider Implementation
1924
*
20-
* Implements BaseTranscriptionProviderDefinition for any API that follows the OpenAI specification.
25+
* Uses the unified defineProvider pattern for any API that follows the OpenAI specification.
2126
* This is a generic implementation that works with OpenAI-compatible endpoints.
2227
*/
23-
export const openaiCompatibleTranscriptionProvider = defineTranscriptionProvider({
28+
export const providerOpenAICompatibleTranscription = defineProvider<OpenAICompatibleTranscriptionConfig>({
2429
id: 'openai-compatible-audio-transcription',
25-
defaultModel: 'whisper-1',
26-
transcriptionFeatures: {
27-
supportsGenerate: true,
28-
supportsStreamOutput: false,
29-
supportsStreamInput: false,
30-
},
31-
32-
async validateConfig(config: BaseTranscriptionProviderConfig): Promise<ProviderValidationResult> {
33-
const errors: Error[] = []
34-
35-
if (!config.apiKey) {
36-
errors.push(new Error('API Key is required'))
37-
}
38-
39-
if (!config.baseUrl) {
40-
errors.push(new Error('Base URL is required'))
41-
}
30+
order: 3,
31+
name: 'OpenAI Compatible',
32+
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openai-compatible.title'),
33+
description: 'OpenAI-compatible transcription API',
34+
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.openai-compatible.description'),
35+
tasks: ['speech-to-text', 'automatic-speech-recognition', 'asr', 'stt'],
36+
icon: 'i-lobe-icons:openai',
4237

43-
if (errors.length > 0) {
44-
return {
45-
errors,
46-
reason: errors.map(e => e.message).join(', '),
47-
valid: false,
48-
}
49-
}
38+
createProviderConfig: ({ t }) => openAICompatibleTranscriptionConfigSchema.extend({
39+
apiKey: openAICompatibleTranscriptionConfigSchema.shape.apiKey.meta({
40+
labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.label'),
41+
descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.description'),
42+
placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.placeholder'),
43+
type: 'password',
44+
}),
45+
baseUrl: openAICompatibleTranscriptionConfigSchema.shape.baseUrl.meta({
46+
labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.label'),
47+
descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.description'),
48+
placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.placeholder'),
49+
}),
50+
}),
5051

51-
return {
52-
errors: [],
53-
reason: '',
54-
valid: true,
55-
}
56-
},
57-
58-
async createProvider(config: BaseTranscriptionProviderConfig) {
52+
createProvider(config) {
5953
const apiKey = typeof config.apiKey === 'string' ? config.apiKey.trim() : ''
6054
const baseUrl = normalizeBaseUrl(config.baseUrl)
6155

6256
return createTranscriptionProvider({ apiKey, baseURL: baseUrl }) as TranscriptionProvider | TranscriptionProviderWithExtraOptions<string, any>
6357
},
6458

65-
async listModels(_config: BaseTranscriptionProviderConfig): Promise<ModelInfo[]> {
66-
// OpenAI Compatible providers don't have hardcoded models
67-
// Models are typically discovered via the API
68-
return []
59+
validationRequiredWhen(config) {
60+
return !!config.apiKey?.trim()
61+
},
62+
63+
validators: {
64+
...createOpenAICompatibleValidators<OpenAICompatibleTranscriptionConfig>({
65+
checks: ['connectivity'],
66+
}),
6967
},
7068

71-
getDefaultConfig(): Partial<BaseTranscriptionProviderConfig> {
72-
return {}
69+
capabilities: {
70+
transcription: {
71+
protocol: 'http',
72+
generateOutput: true,
73+
streamOutput: false,
74+
streamInput: false,
75+
},
76+
},
77+
78+
extraMethods: {
79+
async listModels(_config): Promise<ModelInfo[]> {
80+
// OpenAI Compatible providers don't have hardcoded models
81+
// Models are typically discovered via the API
82+
return []
83+
},
7384
},
7485
})
86+
87+
// Keep export for backward compatibility during migration
88+
export const openaiCompatibleTranscriptionProvider = providerOpenAICompatibleTranscription

0 commit comments

Comments
 (0)