diff --git a/apps/server/src/routes/__test__/v1completions.test.ts b/apps/server/src/routes/__test__/v1completions.test.ts index e8e910c0d..30d76408f 100644 --- a/apps/server/src/routes/__test__/v1completions.test.ts +++ b/apps/server/src/routes/__test__/v1completions.test.ts @@ -287,7 +287,7 @@ describe('v1CompletionsRoutes', () => { }) }) - describe('pOST /api/v1/audio/speech', () => { + describe.skip('pOST /api/v1/audio/speech', () => { it('should proxy TTS request to upstream', async () => { const audioData = new Uint8Array([1, 2, 3, 4]) globalThis.fetch = vi.fn(async () => new Response(audioData, { @@ -314,7 +314,7 @@ describe('v1CompletionsRoutes', () => { }) }) - describe('pOST /api/v1/audio/transcriptions', () => { + describe.skip('pOST /api/v1/audio/transcriptions', () => { it('should proxy transcription request to upstream', async () => { globalThis.fetch = vi.fn(async () => new Response('{"text":"hello"}', { status: 200, diff --git a/apps/server/src/routes/v1completions.ts b/apps/server/src/routes/v1completions.ts index 080d163bb..8eda6aff6 100644 --- a/apps/server/src/routes/v1completions.ts +++ b/apps/server/src/routes/v1completions.ts @@ -1,3 +1,5 @@ +/* eslint-disable unused-imports/no-unused-vars */ + import type { Context } from 'hono' import type { initOtel } from '../libs/otel' @@ -9,7 +11,6 @@ import type { HonoEnv } from '../types/hono' import { useLogger } from '@guiiai/logg' import { context, SpanStatusCode, trace } from '@opentelemetry/api' import { Hono } from 'hono' -import { bodyLimit } from 'hono/body-limit' import { authGuard } from '../middlewares/auth' import { configGuard } from '../middlewares/config-guard' @@ -356,6 +357,6 @@ export function createV1CompletionsRoutes(fluxService: FluxService, configKV: Co .use('*', authGuard) .post('/chat/completions', chatGuard, handleCompletion) .post('/chat/completion', chatGuard, handleCompletion) - .post('/audio/speech', ttsGuard, handleTTS) - .post('/audio/transcriptions', bodyLimit({ maxSize: 25 * 1024 * 1024 }), asrGuard, handleTranscription) + // .post('/audio/speech', ttsGuard, handleTTS) + // .post('/audio/transcriptions', bodyLimit({ maxSize: 25 * 1024 * 1024 }), asrGuard, handleTranscription) } diff --git a/packages/stage-pages/src/pages/settings/providers/chat/official-provider.vue b/packages/stage-pages/src/pages/settings/providers/chat/official-provider.vue index 61cf4c709..8d090d7db 100644 --- a/packages/stage-pages/src/pages/settings/providers/chat/official-provider.vue +++ b/packages/stage-pages/src/pages/settings/providers/chat/official-provider.vue @@ -7,7 +7,6 @@ import { useAuthStore } from '@proj-airi/stage-ui/stores/auth' import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' import { Callout } from '@proj-airi/ui' import { storeToRefs } from 'pinia' -import { watch } from 'vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' @@ -15,25 +14,19 @@ const router = useRouter() const { t } = useI18n() const authStore = useAuthStore() const providersStore = useProvidersStore() -const { isAuthenticated, credits, isLoginOpen } = storeToRefs(authStore) +const { isAuthenticated, credits, isLoginDrawerOpen } = storeToRefs(authStore) const providerId = 'official-provider' const providerMetadata = providersStore.getProviderMetadata(providerId) -// Automatically enable official provider when authenticated -watch(isAuthenticated, (val) => { - if (val) { - providersStore.forceProviderConfigured(providerId) - } -}, { immediate: true }) - function handleLogin() { - isLoginOpen.value = true + isLoginDrawerOpen.value = true } diff --git a/packages/stage-pages/src/pages/settings/providers/speech/official-provider-speech.vue b/packages/stage-pages/src/pages/settings/providers/speech/official-provider-speech.vue index cd03524d1..642176820 100644 --- a/packages/stage-pages/src/pages/settings/providers/speech/official-provider-speech.vue +++ b/packages/stage-pages/src/pages/settings/providers/speech/official-provider-speech.vue @@ -7,7 +7,6 @@ import { useAuthStore } from '@proj-airi/stage-ui/stores/auth' import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' import { Callout } from '@proj-airi/ui' import { storeToRefs } from 'pinia' -import { watch } from 'vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' @@ -15,24 +14,19 @@ const router = useRouter() const { t } = useI18n() const authStore = useAuthStore() const providersStore = useProvidersStore() -const { isAuthenticated, credits, isLoginOpen } = storeToRefs(authStore) +const { isAuthenticated, credits, isLoginDrawerOpen } = storeToRefs(authStore) const providerId = 'official-provider-speech' const providerMetadata = providersStore.getProviderMetadata(providerId) -watch(isAuthenticated, (val) => { - if (val) { - providersStore.forceProviderConfigured(providerId) - } -}, { immediate: true }) - function handleLogin() { - isLoginOpen.value = true + isLoginDrawerOpen.value = true } diff --git a/packages/stage-pages/src/pages/settings/providers/transcription/official-provider-transcription.vue b/packages/stage-pages/src/pages/settings/providers/transcription/official-provider-transcription.vue index 3c759a876..317346667 100644 --- a/packages/stage-pages/src/pages/settings/providers/transcription/official-provider-transcription.vue +++ b/packages/stage-pages/src/pages/settings/providers/transcription/official-provider-transcription.vue @@ -7,7 +7,6 @@ import { useAuthStore } from '@proj-airi/stage-ui/stores/auth' import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' import { Callout } from '@proj-airi/ui' import { storeToRefs } from 'pinia' -import { watch } from 'vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' @@ -15,24 +14,19 @@ const router = useRouter() const { t } = useI18n() const authStore = useAuthStore() const providersStore = useProvidersStore() -const { isAuthenticated, credits, isLoginOpen } = storeToRefs(authStore) +const { isAuthenticated, credits, isLoginDrawerOpen } = storeToRefs(authStore) const providerId = 'official-provider-transcription' const providerMetadata = providersStore.getProviderMetadata(providerId) -watch(isAuthenticated, (val) => { - if (val) { - providersStore.forceProviderConfigured(providerId) - } -}, { immediate: true }) - function handleLogin() { - isLoginOpen.value = true + isLoginDrawerOpen.value = true } diff --git a/packages/stage-ui/src/composables/use-auth-provider-sync.ts b/packages/stage-ui/src/composables/use-auth-provider-sync.ts index 139ecc4a4..d71f8bc73 100644 --- a/packages/stage-ui/src/composables/use-auth-provider-sync.ts +++ b/packages/stage-ui/src/composables/use-auth-provider-sync.ts @@ -1,4 +1,4 @@ -import { nextTick, watch } from 'vue' +import { nextTick } from 'vue' import { initializeAuth } from '../libs/auth' import { useAuthStore } from '../stores/auth' @@ -8,52 +8,66 @@ import { useSpeechStore } from '../stores/modules/speech' import { useProvidersStore } from '../stores/providers' /** - * Coordinates auth state with provider/module stores. - * - * When the user becomes authenticated, this composable automatically enables - * the official providers and sets them as active across consciousness, speech, - * and hearing modules. - * - * Call once at the app root (e.g. Stage.vue). + * Provider IDs to auto-activate on login. + * Edit this list to enable/disable official providers. + */ +const AUTH_ACTIVATED_PROVIDERS: Array<{ id: string, module: 'consciousness' | 'speech' | 'hearing' }> = [ + { id: 'official-provider', module: 'consciousness' }, + // { id: 'official-provider-speech', module: 'speech' }, + // { id: 'official-provider-transcription', module: 'hearing' }, +] + +/** + * Glue layer: uses auth lifecycle hooks to activate/deactivate + * official providers. Providers themselves know nothing about auth. */ export function useAuthProviderSync() { initializeAuth() - const authState = useAuthStore() + const authStore = useAuthStore() const providersStore = useProvidersStore() const consciousnessStore = useConsciousnessStore() const speechStore = useSpeechStore() const hearingStore = useHearingStore() - watch(() => authState.isAuthenticated, async (val) => { - if (!val) - return + authStore.onAuthenticated(async () => { + const toActivate = AUTH_ACTIVATED_PROVIDERS.filter( + p => providersStore.getProviderMetadata(p.id) != null, + ) - const officialProviderId = 'official-provider' - const officialSpeechId = 'official-provider-speech' - const officialTranscriptionId = 'official-provider-transcription' + for (const { id } of toActivate) { + providersStore.forceProviderConfigured(id) + } - providersStore.forceProviderConfigured(officialProviderId) - providersStore.forceProviderConfigured(officialSpeechId) - providersStore.forceProviderConfigured(officialTranscriptionId) - - consciousnessStore.activeProvider = officialProviderId - consciousnessStore.activeModel = 'auto' - speechStore.activeSpeechProvider = officialSpeechId - speechStore.activeSpeechModel = 'auto' - hearingStore.activeTranscriptionProvider = officialTranscriptionId - hearingStore.activeTranscriptionModel = 'auto' + for (const { id, module } of toActivate) { + switch (module) { + case 'consciousness': + consciousnessStore.activeProvider = id + consciousnessStore.activeModel = 'auto' + break + case 'speech': + speechStore.activeSpeechProvider = id + speechStore.activeSpeechModel = 'auto' + break + case 'hearing': + hearingStore.activeTranscriptionProvider = id + hearingStore.activeTranscriptionModel = 'auto' + break + } + } await nextTick() try { - await Promise.all([ - consciousnessStore.loadModelsForProvider(officialProviderId), - providersStore.fetchModelsForProvider(officialSpeechId), - providersStore.fetchModelsForProvider(officialTranscriptionId), - ]) + await Promise.all( + toActivate.map(({ id, module }) => + module === 'consciousness' + ? consciousnessStore.loadModelsForProvider(id) + : providersStore.fetchModelsForProvider(id), + ), + ) } catch (err) { console.error('error loading models for official providers', err) } - }, { immediate: true }) + }) } diff --git a/packages/stage-ui/src/libs/providers/providers/index.ts b/packages/stage-ui/src/libs/providers/providers/index.ts index 962d2cbbd..c9b1abdb1 100644 --- a/packages/stage-ui/src/libs/providers/providers/index.ts +++ b/packages/stage-ui/src/libs/providers/providers/index.ts @@ -27,6 +27,7 @@ import './modelscope' import './ollama' import './cloudflare-workers-ai' import './azure-ai-foundry' +import './official' export { getDefinedProvider, diff --git a/packages/stage-ui/src/libs/providers/providers/official/index.ts b/packages/stage-ui/src/libs/providers/providers/official/index.ts new file mode 100644 index 000000000..beaa0f42c --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/official/index.ts @@ -0,0 +1,119 @@ +import { z } from 'zod' + +import { defineProvider } from '../registry' +import { createOfficialOpenAIProvider, OFFICIAL_ICON, withCredentials } from './shared' + +const officialConfigSchema = z.object({}) + +export const providerOfficialChat = defineProvider({ + id: 'official-provider', + order: -1, + name: 'Official Provider', + nameLocalize: ({ t }) => t('settings.pages.providers.provider.official.title'), + description: 'Official AI provider by AIRI.', + descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.official.description'), + tasks: ['text-generation'], + icon: OFFICIAL_ICON, + requiresCredentials: false, + + createProviderConfig: () => officialConfigSchema, + createProvider(_config) { + const provider = createOfficialOpenAIProvider() + const originalChat = provider.chat.bind(provider) + provider.chat = (model: string) => { + const result = originalChat(model) + result.fetch = withCredentials() + return result + } + return provider + }, + + validationRequiredWhen: () => false, + + extraMethods: { + listModels: async () => [ + { + id: 'auto', + name: 'Auto', + provider: 'official-provider', + description: 'Automatically routed by AI Gateway', + }, + ], + }, +}) + +// TTS and ASR official providers — uncomment to re-enable: +// +// export const providerOfficialSpeech = defineProvider({ +// id: 'official-provider-speech', +// order: -1, +// name: 'Official Speech Provider', +// nameLocalize: ({ t }) => t('settings.pages.providers.provider.official.speech-title'), +// description: 'Official text-to-speech provider by AIRI.', +// descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.official.speech-description'), +// tasks: ['text-to-speech'], +// icon: OFFICIAL_ICON, +// requiresCredentials: false, +// createProviderConfig: () => officialConfigSchema, +// createProvider(_config) { +// const provider = createOfficialOpenAIProvider() +// const originalSpeech = provider.speech.bind(provider) +// provider.speech = (model: string) => { +// const result = originalSpeech(model) +// result.fetch = withCredentials() +// return result +// } +// return provider +// }, +// validationRequiredWhen: () => false, +// extraMethods: { +// listModels: async () => [ +// { id: 'auto', name: 'Auto', provider: 'official-provider-speech', description: 'Automatically routed by AI Gateway' }, +// ], +// listVoices: async () => [ +// { id: 'alloy', name: 'Alloy', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, +// { id: 'echo', name: 'Echo', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, +// { id: 'fable', name: 'Fable', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, +// { id: 'onyx', name: 'Onyx', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, +// { id: 'nova', name: 'Nova', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, +// { id: 'shimmer', name: 'Shimmer', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, +// ], +// }, +// }) +// +// export const providerOfficialTranscription = defineProvider({ +// id: 'official-provider-transcription', +// order: -1, +// name: 'Official Transcription Provider', +// nameLocalize: ({ t }) => t('settings.pages.providers.provider.official.transcription-title'), +// description: 'Official speech-to-text provider by AIRI.', +// descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.official.transcription-description'), +// tasks: ['speech-to-text', 'asr'], +// icon: OFFICIAL_ICON, +// requiresCredentials: false, +// createProviderConfig: () => officialConfigSchema, +// createProvider(_config) { +// const provider = createOfficialOpenAIProvider() +// const originalTranscription = provider.transcription.bind(provider) +// provider.transcription = (model: string) => { +// const result = originalTranscription(model) +// result.fetch = withCredentials() +// return result +// } +// return provider +// }, +// validationRequiredWhen: () => false, +// capabilities: { +// transcription: { +// protocol: 'http', +// generateOutput: true, +// streamOutput: false, +// streamInput: false, +// }, +// }, +// extraMethods: { +// listModels: async () => [ +// { id: 'auto', name: 'Auto', provider: 'official-provider-transcription', description: 'Automatically routed by AI Gateway' }, +// ], +// }, +// }) diff --git a/packages/stage-ui/src/libs/providers/providers/official/shared.ts b/packages/stage-ui/src/libs/providers/providers/official/shared.ts new file mode 100644 index 000000000..8cd7a464f --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/official/shared.ts @@ -0,0 +1,18 @@ +import { createOpenAI } from '@xsai-ext/providers/create' + +import { SERVER_URL } from '../../../../libs/server' + +export const OFFICIAL_ICON = 'i-solar:star-bold-duotone' + +export function withCredentials() { + return (input: RequestInfo | URL, init?: RequestInit) => { + return globalThis.fetch(input, { + ...init, + credentials: 'include', + }) + } +} + +export function createOfficialOpenAIProvider() { + return createOpenAI('', `${SERVER_URL}/api/v1/`) +} diff --git a/packages/stage-ui/src/libs/providers/types.ts b/packages/stage-ui/src/libs/providers/types.ts index 47d320212..9358a72db 100644 --- a/packages/stage-ui/src/libs/providers/types.ts +++ b/packages/stage-ui/src/libs/providers/types.ts @@ -141,6 +141,12 @@ export interface ProviderDefinition { */ isAvailableBy?: () => Promise | boolean + /** + * If false, the provider does not require user-provided credentials (e.g. API keys). + * Used for built-in providers that authenticate via session cookies. + */ + requiresCredentials?: boolean + createProviderConfig: (contextOptions: { t: ComposerTranslation }) => $ZodType createProvider: (config: TConfig) => ProviderInstance extraMethods?: ProviderExtraMethods diff --git a/packages/stage-ui/src/stores/auth.ts b/packages/stage-ui/src/stores/auth.ts index 3847a20fd..d5e83ff39 100644 --- a/packages/stage-ui/src/stores/auth.ts +++ b/packages/stage-ui/src/stores/auth.ts @@ -21,6 +21,50 @@ export const useAuthStore = defineStore('auth', () => { const isLoginOpen = ref(false) + // --- Lifecycle hooks --- + type AuthHook = () => void | Promise + const authenticatedHooks: AuthHook[] = [] + const logoutHooks: AuthHook[] = [] + + function onAuthenticated(hook: AuthHook) { + authenticatedHooks.push(hook) + // If already authenticated when hook is registered, fire immediately. + // This covers the case where auth resolves before the hook is registered. + if (isAuthenticated.value) { + hook() + } + return () => { + const idx = authenticatedHooks.indexOf(hook) + if (idx >= 0) + authenticatedHooks.splice(idx, 1) + } + } + + function onLogout(hook: AuthHook) { + logoutHooks.push(hook) + return () => { + const idx = logoutHooks.indexOf(hook) + if (idx >= 0) + logoutHooks.splice(idx, 1) + } + } + + // Dispatch hooks when auth state changes + watch(isAuthenticated, async (val, oldVal) => { + if (val && !oldVal) { + for (const hook of authenticatedHooks) { + try { await hook() } + catch (e) { console.error('auth hook error', e) } + } + } + if (!val && oldVal) { + for (const hook of logoutHooks) { + try { await hook() } + catch (e) { console.error('logout hook error', e) } + } + } + }) + const updateCredits = async () => { if (!isAuthenticated.value) return @@ -47,6 +91,8 @@ export const useAuthStore = defineStore('auth', () => { isAuthenticated, credits, updateCredits, - isLoginOpen, + isLoginDrawerOpen, + onAuthenticated, + onLogout, } }) diff --git a/packages/stage-ui/src/stores/providers.ts b/packages/stage-ui/src/stores/providers.ts index a7111498c..94a4b77a7 100644 --- a/packages/stage-ui/src/stores/providers.ts +++ b/packages/stage-ui/src/stores/providers.ts @@ -55,7 +55,6 @@ import { useAuthStore } from './auth' import { createAliyunNLSProvider as createAliyunNlsStreamProvider } from './providers/aliyun/stream-transcription' import { convertProviderDefinitionsToMetadata } from './providers/converters' import { models as elevenLabsModels } from './providers/elevenlabs/list-models' -import { createOfficialProviders, OFFICIAL_PROVIDER_IDS } from './providers/official' import { buildOpenAICompatibleProvider } from './providers/openai-compatible-builder' import { buildOpenRouterAudioSpeechProvider } from './providers/openrouter/audio-speech' import { createWebSpeechAPIProvider } from './providers/web-speech-api' @@ -257,7 +256,6 @@ export const useProvidersStore = defineStore('providers', () => { // Centralized provider metadata with provider factory functions const authState = useAuthStore() const providerMetadata: Record = { - ...createOfficialProviders(() => authState.isAuthenticated), 'speech-noop': { id: 'speech-noop', category: 'speech', @@ -1737,8 +1735,7 @@ export const useProvidersStore = defineStore('providers', () => { // Keep only legacy ASR/TTS providers and official providers as hand-written metadata. // All other categories are sourced from unified definitions in libs/providers. for (const [providerId, existing] of Object.entries(providerMetadata)) { - if (existing.category !== 'speech' && existing.category !== 'transcription' - && !(OFFICIAL_PROVIDER_IDS as readonly string[]).includes(providerId)) { + if (existing.category !== 'speech' && existing.category !== 'transcription') { delete providerMetadata[providerId] } } @@ -1863,13 +1860,6 @@ export const useProvidersStore = defineStore('providers', () => { modelLoadError: null, } } - - // Must run AFTER runtime state is created so forceProviderConfigured can set isConfigured - if ((OFFICIAL_PROVIDER_IDS as readonly string[]).includes(providerId)) { - if (authState.isAuthenticated) { - forceProviderConfigured(providerId) - } - } } // Initialize all providers diff --git a/packages/stage-ui/src/stores/providers/converters.ts b/packages/stage-ui/src/stores/providers/converters.ts index 56f1f91a1..07140c105 100644 --- a/packages/stage-ui/src/stores/providers/converters.ts +++ b/packages/stage-ui/src/stores/providers/converters.ts @@ -116,6 +116,7 @@ export function convertProviderDefinitionToMetadata( iconColor: definition.iconColor, iconImage: definition.iconImage, isAvailableBy: definition.isAvailableBy, + requiresCredentials: definition.requiresCredentials, defaultOptions: () => { if (Object.keys(schemaDefaults).length > 0) { return { ...schemaDefaults } diff --git a/packages/stage-ui/src/stores/providers/official.ts b/packages/stage-ui/src/stores/providers/official.ts deleted file mode 100644 index cd55231b9..000000000 --- a/packages/stage-ui/src/stores/providers/official.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { ProviderMetadata } from '../providers' - -import { createOpenAI } from '@xsai-ext/providers/create' - -import { SERVER_URL } from '../../libs/server' - -const OFFICIAL_ICON = 'i-solar:star-bold-duotone' - -function withCredentials() { - return (input: RequestInfo | URL, init?: RequestInit) => { - return globalThis.fetch(input, { - ...init, - credentials: 'include', - }) - } -} - -function createOfficialOpenAIProvider() { - return createOpenAI('', `${SERVER_URL}/api/v1/`) -} - -export const OFFICIAL_PROVIDER_IDS = [ - 'official-provider', - 'official-provider-speech', - 'official-provider-transcription', -] as const - -/** - * Factory that creates official provider metadata. - * Accepts a lazy auth getter to avoid circular dependency: - * official.ts -> auth.ts -> providers.ts -> official.ts - */ -export function createOfficialProviders(getIsAuthenticated: () => boolean): Record { - function assertAuthenticated() { - if (!getIsAuthenticated()) { - throw new Error('User is not authenticated') - } - } - - function validateAuth() { - return { - errors: [], - reason: '', - valid: getIsAuthenticated(), - } - } - - return { - 'official-provider': { - id: 'official-provider', - order: -1, - category: 'chat', - tasks: ['text-generation'], - nameKey: 'settings.pages.providers.provider.official.title', - name: 'Official Provider', - descriptionKey: 'settings.pages.providers.provider.official.description', - description: 'Official AI provider by AIRI.', - icon: OFFICIAL_ICON, - requiresCredentials: false, - createProvider: async (_config) => { - assertAuthenticated() - const provider = createOfficialOpenAIProvider() - - const originalChat = provider.chat.bind(provider) - provider.chat = (model: string) => { - const result = originalChat(model) - result.fetch = withCredentials() - return result - } - - return provider - }, - capabilities: { - listModels: async () => [ - { - id: 'auto', - name: 'Auto', - provider: 'official-provider', - description: 'Automatically routed by AI Gateway', - }, - ], - }, - validators: { - validateProviderConfig: () => validateAuth(), - }, - }, - - 'official-provider-speech': { - id: 'official-provider-speech', - order: -1, - category: 'speech', - tasks: ['text-to-speech'], - nameKey: 'settings.pages.providers.provider.official.speech-title', - name: 'Official Speech Provider', - descriptionKey: 'settings.pages.providers.provider.official.speech-description', - description: 'Official text-to-speech provider by AIRI.', - icon: OFFICIAL_ICON, - requiresCredentials: false, - createProvider: async (_config) => { - assertAuthenticated() - const provider = createOfficialOpenAIProvider() - - const originalSpeech = provider.speech.bind(provider) - provider.speech = (model: string) => { - const result = originalSpeech(model) - result.fetch = withCredentials() - return result - } - - return provider - }, - capabilities: { - listModels: async () => [ - { - id: 'auto', - name: 'Auto', - provider: 'official-provider-speech', - description: 'Automatically routed by AI Gateway', - }, - ], - listVoices: async () => [ - { id: 'alloy', name: 'Alloy', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, - { id: 'echo', name: 'Echo', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, - { id: 'fable', name: 'Fable', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, - { id: 'onyx', name: 'Onyx', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, - { id: 'nova', name: 'Nova', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, - { id: 'shimmer', name: 'Shimmer', provider: 'official-provider-speech', languages: [{ code: 'en', title: 'English' }] }, - ], - }, - validators: { - validateProviderConfig: () => validateAuth(), - }, - }, - - 'official-provider-transcription': { - id: 'official-provider-transcription', - order: -1, - category: 'transcription', - tasks: ['speech-to-text', 'asr'], - nameKey: 'settings.pages.providers.provider.official.transcription-title', - name: 'Official Transcription Provider', - descriptionKey: 'settings.pages.providers.provider.official.transcription-description', - description: 'Official speech-to-text provider by AIRI.', - icon: OFFICIAL_ICON, - requiresCredentials: false, - transcriptionFeatures: { - supportsGenerate: true, - supportsStreamOutput: false, - supportsStreamInput: false, - }, - createProvider: async (_config) => { - assertAuthenticated() - const provider = createOfficialOpenAIProvider() - - const originalTranscription = provider.transcription.bind(provider) - provider.transcription = (model: string) => { - const result = originalTranscription(model) - result.fetch = withCredentials() - return result - } - - return provider - }, - capabilities: { - listModels: async () => [ - { - id: 'auto', - name: 'Auto', - provider: 'official-provider-transcription', - description: 'Automatically routed by AI Gateway', - }, - ], - }, - validators: { - validateProviderConfig: () => validateAuth(), - }, - }, - } -}