fix(stage-ui): align module provider visibility with auth (#2353)
This commit is contained in:
@@ -10,7 +10,7 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { configureAsDefaultsIfEmpty } from '@proj-airi/stage-ui/stores/modules/default'
|
||||
import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from '@proj-airi/stage-ui/stores/modules/default'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
@@ -51,6 +51,16 @@ useSettingsStageModel()
|
||||
useVisionStore()
|
||||
|
||||
let stopAuthenticatedSetup: (() => void) | undefined
|
||||
let stopLoggedOutSetup: (() => void) | undefined
|
||||
|
||||
async function removeAuthenticationProviderConfiguration() {
|
||||
if (!syncedPinia.isLeader())
|
||||
return
|
||||
|
||||
if (await unconfigureAuthenticationProviders())
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
}
|
||||
|
||||
function registerAuthenticatedSetup() {
|
||||
stopAuthenticatedSetup ??= authStore.onAuthenticated(async () => {
|
||||
if (!syncedPinia.isLeader())
|
||||
@@ -60,6 +70,7 @@ function registerAuthenticatedSetup() {
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
await onboardingStore.closeAfterAuthentication()
|
||||
})
|
||||
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
||||
}
|
||||
|
||||
const primaryColor = computed(() => {
|
||||
@@ -103,6 +114,8 @@ onMounted(async () => {
|
||||
await displayModelsStore.initialize()
|
||||
await cardStore.initialize()
|
||||
registerAuthenticatedSetup()
|
||||
if (!authStore.isAuthenticated)
|
||||
await removeAuthenticationProviderConfiguration()
|
||||
|
||||
if (onboardingStore.needsOnboarding) {
|
||||
onboardingStore.showingSetup = true
|
||||
@@ -122,6 +135,7 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAuthenticatedSetup?.()
|
||||
stopLoggedOutSetup?.()
|
||||
contextBridgeStore.dispose()
|
||||
})
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { configureAsDefaultsIfEmpty } from '@proj-airi/stage-ui/stores/modules/default'
|
||||
import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from '@proj-airi/stage-ui/stores/modules/default'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
@@ -140,6 +140,16 @@ function createFullStageRuntime() {
|
||||
useVisionStore()
|
||||
|
||||
let stopAuthenticatedSetup: (() => void) | undefined
|
||||
let stopLoggedOutSetup: (() => void) | undefined
|
||||
|
||||
async function removeAuthenticationProviderConfiguration() {
|
||||
if (!syncedPinia.isLeader())
|
||||
return
|
||||
|
||||
if (await unconfigureAuthenticationProviders())
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
}
|
||||
|
||||
function registerAuthenticatedSetup() {
|
||||
stopAuthenticatedSetup ??= authStore.onAuthenticated(async () => {
|
||||
if (!syncedPinia.isLeader())
|
||||
@@ -149,6 +159,7 @@ function createFullStageRuntime() {
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
await onboardingStore.closeAfterAuthentication()
|
||||
})
|
||||
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
||||
}
|
||||
|
||||
const { activeProvider, artistryGlobals, activeModel, defaultPromptPrefix, providerOptions } = storeToRefs(artistryStore)
|
||||
@@ -254,6 +265,8 @@ function createFullStageRuntime() {
|
||||
await displayModelsStore.initialize()
|
||||
await cardStore.initialize()
|
||||
registerAuthenticatedSetup()
|
||||
if (!authStore.isAuthenticated)
|
||||
await removeAuthenticationProviderConfiguration()
|
||||
|
||||
await displayModelsStore.loadDisplayModelsFromIndexedDB()
|
||||
await settingsStore.initializeStageModel()
|
||||
@@ -299,6 +312,7 @@ function createFullStageRuntime() {
|
||||
},
|
||||
dispose() {
|
||||
stopAuthenticatedSetup?.()
|
||||
stopLoggedOutSetup?.()
|
||||
contextBridgeStore.dispose()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { configureAsDefaultsIfEmpty } from '@proj-airi/stage-ui/stores/modules/default'
|
||||
import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from '@proj-airi/stage-ui/stores/modules/default'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
@@ -58,6 +58,16 @@ useSettingsStageModel()
|
||||
useVisionStore()
|
||||
|
||||
let stopAuthenticatedSetup: (() => void) | undefined
|
||||
let stopLoggedOutSetup: (() => void) | undefined
|
||||
|
||||
async function removeAuthenticationProviderConfiguration() {
|
||||
if (!syncedPinia.isLeader())
|
||||
return
|
||||
|
||||
if (await unconfigureAuthenticationProviders())
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
}
|
||||
|
||||
function registerAuthenticatedSetup() {
|
||||
stopAuthenticatedSetup ??= authStore.onAuthenticated(async () => {
|
||||
if (!syncedPinia.isLeader())
|
||||
@@ -67,6 +77,7 @@ function registerAuthenticatedSetup() {
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
await onboardingStore.closeAfterAuthentication()
|
||||
})
|
||||
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
||||
}
|
||||
|
||||
const inferencePreload = useInferencePreload()
|
||||
@@ -118,6 +129,8 @@ onMounted(async () => {
|
||||
await displayModelsStore.initialize()
|
||||
await cardStore.initialize()
|
||||
registerAuthenticatedSetup()
|
||||
if (!authStore.isAuthenticated)
|
||||
await removeAuthenticationProviderConfiguration()
|
||||
|
||||
if (onboardingStore.needsOnboarding) {
|
||||
onboardingStore.showingSetup = true
|
||||
@@ -138,6 +151,7 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAuthenticatedSetup?.()
|
||||
stopLoggedOutSetup?.()
|
||||
stopLeadershipListener()
|
||||
contextBridgeStore.dispose()
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ const airiCardStore = useAiriCardStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const consciousnessSettingsStore = useConsciousnessSettingsStore()
|
||||
const { configuredProviders } = storeToRefs(providerStore)
|
||||
const { persistedChatProvidersMetadata } = storeToRefs(providersStore)
|
||||
const { moduleChatProvidersMetadata } = storeToRefs(providersStore)
|
||||
const { reasoning } = storeToRefs(consciousnessSettingsStore)
|
||||
const {
|
||||
activeProvider,
|
||||
@@ -88,13 +88,13 @@ async function updateReasoning(value: boolean) {
|
||||
See also: https://stackoverflow.com/a/33737340
|
||||
-->
|
||||
<fieldset
|
||||
v-if="persistedChatProvidersMetadata.length > 0"
|
||||
v-if="moduleChatProvidersMetadata.length > 0"
|
||||
flex="~ row gap-4"
|
||||
min-w-0 overflow-x-auto scroll-smooth
|
||||
role="radiogroup"
|
||||
>
|
||||
<RadioCardSimple
|
||||
v-for="metadata in persistedChatProvidersMetadata"
|
||||
v-for="metadata in moduleChatProvidersMetadata"
|
||||
:id="metadata.id"
|
||||
:key="metadata.id"
|
||||
v-model="activeProvider"
|
||||
|
||||
@@ -33,7 +33,7 @@ const {
|
||||
} = storeToRefs(hearingStore)
|
||||
const providersStore = useProviderStore()
|
||||
const providerStore = useProviderConfigStore()
|
||||
const { configuredTranscriptionProvidersMetadata } = storeToRefs(providersStore)
|
||||
const { moduleTranscriptionProvidersMetadata } = storeToRefs(providersStore)
|
||||
|
||||
const { trackProviderClick } = useAnalytics()
|
||||
const settingsAudioDeviceStore = useSettingsAudioDevice()
|
||||
@@ -364,13 +364,13 @@ onUnmounted(() => {
|
||||
See also: https://stackoverflow.com/a/33737340
|
||||
-->
|
||||
<fieldset
|
||||
v-if="configuredTranscriptionProvidersMetadata.length > 0"
|
||||
v-if="moduleTranscriptionProvidersMetadata.length > 0"
|
||||
flex="~ row gap-4"
|
||||
min-w-0 overflow-x-auto scroll-smooth
|
||||
role="radiogroup"
|
||||
>
|
||||
<RadioCardSimple
|
||||
v-for="metadata in configuredTranscriptionProvidersMetadata"
|
||||
v-for="metadata in moduleTranscriptionProvidersMetadata"
|
||||
:id="metadata.id"
|
||||
:key="metadata.id"
|
||||
v-model="activeTranscriptionProvider"
|
||||
|
||||
@@ -35,7 +35,7 @@ const providersStore = useProviderStore()
|
||||
const providerStore = useProviderConfigStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const airiCardStore = useAiriCardStore()
|
||||
const { allAudioSpeechProvidersMetadata, configuredSpeechProvidersMetadata } = storeToRefs(providersStore)
|
||||
const { allAudioSpeechProvidersMetadata, moduleSpeechProvidersMetadata } = storeToRefs(providersStore)
|
||||
const {
|
||||
activeSpeechProvider,
|
||||
activeSpeechModel,
|
||||
@@ -76,7 +76,7 @@ let lastOfficialTtsExposureKey = ''
|
||||
const STREAMING_MODEL_OPTION_PREFIX = 'streaming:'
|
||||
|
||||
const selectableSpeechSources = computed(() => {
|
||||
const configuredSources = configuredSpeechProvidersMetadata.value
|
||||
const configuredSources = moduleSpeechProvidersMetadata.value
|
||||
.filter(metadata =>
|
||||
metadata.id !== 'speech-noop'
|
||||
&& metadata.id !== OFFICIAL_SPEECH_STREAMING_PROVIDER_ID,
|
||||
|
||||
@@ -17,7 +17,7 @@ const airiCardStore = useAiriCardStore()
|
||||
const visionStore = useVisionStore()
|
||||
const visionProcessingStore = useVisionProcessingStore()
|
||||
const { configuredProviders } = storeToRefs(providerStore)
|
||||
const { persistedVisionProvidersMetadata } = storeToRefs(providersStore)
|
||||
const { moduleVisionProvidersMetadata } = storeToRefs(providersStore)
|
||||
const {
|
||||
activeProvider,
|
||||
activeModel,
|
||||
@@ -106,12 +106,12 @@ function formatRelativeTime(timestamp: number | null) {
|
||||
</div>
|
||||
<div :class="['max-w-full']">
|
||||
<fieldset
|
||||
v-if="persistedVisionProvidersMetadata.length > 0"
|
||||
v-if="moduleVisionProvidersMetadata.length > 0"
|
||||
:class="['flex', 'min-w-0', 'flex-row', 'gap-4', 'overflow-x-auto', 'scroll-smooth']"
|
||||
role="radiogroup"
|
||||
>
|
||||
<RadioCardSimple
|
||||
v-for="metadata in persistedVisionProvidersMetadata"
|
||||
v-for="metadata in moduleVisionProvidersMetadata"
|
||||
:id="metadata.id"
|
||||
:key="metadata.id"
|
||||
v-model="activeProvider"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
@@ -39,9 +38,7 @@ export const providerBrowserWebSpeechApi = defineProvider({
|
||||
},
|
||||
},
|
||||
|
||||
// Electron uses Chromium, but it does not include the Google API keys that
|
||||
// the Web Speech API needs. The provider only works in browser contexts.
|
||||
isAvailableBy: () => !isStageTamagotchi() && isWebSpeechApiAvailable(),
|
||||
isAvailableBy: isWebSpeechApiAvailable,
|
||||
createProviderConfig: () => webSpeechApiConfigSchema,
|
||||
createProvider: createWebSpeechAPIProvider,
|
||||
validationRequiredWhen: () => false,
|
||||
|
||||
@@ -10,9 +10,11 @@ import { SERVER_URL } from '../../../../libs/server'
|
||||
import { defineProvider } from '../registry'
|
||||
import { createOfficialAudioProvider, createOfficialOpenAIProvider, OFFICIAL_ICON, withCredentials } from './shared'
|
||||
|
||||
export const OFFICIAL_CHAT_PROVIDER_ID = 'official-provider'
|
||||
export const OFFICIAL_SPEECH_PROVIDER_ID = 'official-provider-speech'
|
||||
export const OFFICIAL_SPEECH_STREAMING_PROVIDER_ID = 'official-provider-speech-streaming'
|
||||
export const OFFICIAL_TRANSCRIPTION_PROVIDER_ID = 'official-provider-transcription'
|
||||
export const OFFICIAL_VISION_PROVIDER_ID = 'vision-official-provider'
|
||||
|
||||
// Locale → voice id map recommended by the server, keyed by provider id.
|
||||
// Populated by each speech provider's listVoices() from the response's
|
||||
@@ -63,7 +65,7 @@ function authHeaders(): Record<string, string> {
|
||||
}
|
||||
|
||||
export const providerOfficialChat = defineProvider({
|
||||
id: 'official-provider',
|
||||
id: OFFICIAL_CHAT_PROVIDER_ID,
|
||||
order: -1,
|
||||
name: 'Official Provider',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.official.title'),
|
||||
@@ -72,6 +74,7 @@ export const providerOfficialChat = defineProvider({
|
||||
tasks: ['text-generation'],
|
||||
icon: OFFICIAL_ICON,
|
||||
requiresCredentials: false,
|
||||
configuredBy: 'authentication',
|
||||
|
||||
createProviderConfig: () => officialConfigSchema,
|
||||
createProvider(_config) {
|
||||
@@ -92,7 +95,7 @@ export const providerOfficialChat = defineProvider({
|
||||
{
|
||||
id: 'auto',
|
||||
name: 'Auto',
|
||||
provider: 'official-provider',
|
||||
provider: OFFICIAL_CHAT_PROVIDER_ID,
|
||||
description: 'Automatically routed by AI Gateway',
|
||||
},
|
||||
],
|
||||
@@ -109,6 +112,7 @@ export const providerOfficialSpeech = defineProvider({
|
||||
tasks: ['text-to-speech'],
|
||||
icon: OFFICIAL_ICON,
|
||||
requiresCredentials: false,
|
||||
configuredBy: 'authentication',
|
||||
createProviderConfig: () => officialConfigSchema,
|
||||
createProvider(_config) {
|
||||
const provider = createOfficialAudioProvider()
|
||||
@@ -226,6 +230,7 @@ export const providerOfficialSpeechStreaming = defineProvider({
|
||||
tasks: ['text-to-speech'],
|
||||
icon: OFFICIAL_ICON,
|
||||
requiresCredentials: false,
|
||||
configuredBy: 'authentication',
|
||||
// Mark this provider as speaking the bidirectional ws TTS protocol so the
|
||||
// session adapter (`tts-session.ts`) picks the streaming path without
|
||||
// hard-coding provider id. Default for every other provider is `'rest'`.
|
||||
@@ -350,6 +355,7 @@ export const providerOfficialTranscription = defineProvider({
|
||||
tasks: ['speech-to-text', 'automatic-speech-recognition', 'asr', 'stt', 'streaming-transcription'],
|
||||
icon: OFFICIAL_ICON,
|
||||
requiresCredentials: false,
|
||||
configuredBy: 'authentication',
|
||||
capabilities: {
|
||||
transcription: {
|
||||
protocol: 'http',
|
||||
|
||||
@@ -29,6 +29,7 @@ export type ProviderInstance
|
||||
|
||||
/** Validation lifecycle for one serializable provider configuration. */
|
||||
export type ProviderValidationStatus = 'unconfigured' | 'validating' | 'configured' | 'invalid' | 'bypassed'
|
||||
export type ProviderConfiguredBy = 'user' | 'authentication'
|
||||
|
||||
/** Serializable configuration for one provider instance. */
|
||||
export interface InferenceServiceProvider {
|
||||
@@ -40,6 +41,8 @@ export interface InferenceServiceProvider {
|
||||
config: Record<string, unknown>
|
||||
/** Current validation state for this provider configuration. */
|
||||
status: ProviderValidationStatus
|
||||
/** Lifecycle owner that creates and revokes this provider configuration. */
|
||||
configuredBy: ProviderConfiguredBy
|
||||
}
|
||||
|
||||
export function isModelProvider(providerInstance: ProviderInstance): providerInstance is ModelProvider | ModelProviderWithExtraOptions {
|
||||
@@ -194,6 +197,13 @@ export interface ProviderDefinition<TConfig extends any = any> {
|
||||
*/
|
||||
requiresCredentials?: boolean
|
||||
|
||||
/**
|
||||
* Lifecycle owner for provider configurations created from this definition.
|
||||
*
|
||||
* @default 'user'
|
||||
*/
|
||||
configuredBy?: ProviderConfiguredBy
|
||||
|
||||
createProviderConfig: (contextOptions: { t: ComposerTranslation }) => $ZodType<TConfig>
|
||||
onboardingFields?: (ctx: { t: ComposerTranslation }) => ProviderOnboardingField[]
|
||||
createProvider: (config: TConfig) => ProviderInstance
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { parse as parseSchema } from 'zod/v4/core'
|
||||
|
||||
import { ATLASCLOUD_DEFAULT_BASE_URL, providerAtlasCloud } from '../libs/providers/providers/atlascloud'
|
||||
import { OFFICIAL_CHAT_PROVIDER_ID } from '../libs/providers/providers/official'
|
||||
import { providerOpenAICompatible } from '../libs/providers/providers/openai-compatible'
|
||||
import { inferenceServiceProvidersService } from './inference-service-providers'
|
||||
|
||||
@@ -21,6 +22,13 @@ describe('services inference-service-providers', () => {
|
||||
expect(provider.definitionId).toBe(providerOpenAICompatible.id)
|
||||
expect(provider.config).toEqual({})
|
||||
expect(provider.status).toBe('unconfigured')
|
||||
expect(provider.configuredBy).toBe('user')
|
||||
})
|
||||
|
||||
it('preserves definition-owned authentication configuration', () => {
|
||||
const provider = inferenceServiceProvidersService.buildLocal(OFFICIAL_CHAT_PROVIDER_ID, {})
|
||||
|
||||
expect(provider.configuredBy).toBe('authentication')
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -104,6 +112,7 @@ describe('services inference-service-providers', () => {
|
||||
config: { baseUrl: 'https://example.com/v1/' },
|
||||
id: 'provider-1',
|
||||
status: 'configured',
|
||||
configuredBy: 'user',
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -107,6 +107,7 @@ export function createInferenceServiceProvidersService(): InferenceServiceProvid
|
||||
definitionId,
|
||||
config: initialConfig,
|
||||
status: 'unconfigured',
|
||||
configuredBy: definition.configuredBy ?? 'user',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +127,7 @@ export function createInferenceServiceProvidersService(): InferenceServiceProvid
|
||||
definitionId: item.definitionId,
|
||||
config: item.config,
|
||||
status,
|
||||
configuredBy: getDefinedProvider(item.definitionId)?.configuredBy ?? 'user',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_TRANSCRIPTION_PROVIDER_ID } from '../../libs/providers/providers/official'
|
||||
import { OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID, OFFICIAL_TRANSCRIPTION_PROVIDER_ID } from '../../libs/providers/providers/official'
|
||||
import { useProviderConfigStore } from '../providers/config'
|
||||
import { useProviderStore } from '../providers/provider'
|
||||
import { useConsciousnessStore } from './consciousness'
|
||||
import { configureAsDefaultsIfEmpty } from './default'
|
||||
import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from './default'
|
||||
import { useHearingStore } from './hearing'
|
||||
import { useSpeechStore } from './speech'
|
||||
import { useVisionStore } from './vision'
|
||||
@@ -72,9 +72,13 @@ describe('official provider module defaults', () => {
|
||||
expect(visionStore.activeModel).toBe('auto')
|
||||
|
||||
expect(providerConfigStore.providers['official-provider']?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers['official-provider']?.configuredBy).toBe('authentication')
|
||||
expect(providerConfigStore.providers[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]?.configuredBy).toBe('authentication')
|
||||
expect(providerConfigStore.providers[OFFICIAL_SPEECH_PROVIDER_ID]?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_SPEECH_PROVIDER_ID]?.configuredBy).toBe('authentication')
|
||||
expect(providerConfigStore.providers['vision-official-provider']?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers['vision-official-provider']?.configuredBy).toBe('authentication')
|
||||
expect(providerConfigStore.addedProviders['official-provider']).toBe(true)
|
||||
expect(providerConfigStore.addedProviders[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]).toBe(true)
|
||||
expect(providerConfigStore.addedProviders[OFFICIAL_SPEECH_PROVIDER_ID]).toBe(true)
|
||||
@@ -193,11 +197,11 @@ describe('official provider module defaults', () => {
|
||||
expect(speechStore.activeSpeechVoiceId).toBe('')
|
||||
expect(visionStore.activeProvider).toBe('custom-vision')
|
||||
expect(visionStore.activeModel).toBe('custom-vision-model')
|
||||
expect(providerConfigStore.providers['official-provider']?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_SPEECH_PROVIDER_ID]?.status).toBe('configured')
|
||||
expect(providerConfigStore.addedProviders[OFFICIAL_SPEECH_PROVIDER_ID]).toBe(true)
|
||||
expect(providerConfigStore.providers['official-provider']).toBeUndefined()
|
||||
expect(providerConfigStore.providers[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]).toBeUndefined()
|
||||
expect(providerConfigStore.providers['vision-official-provider']).toBeUndefined()
|
||||
expect(providerConfigStore.providers['vision-official-provider']?.status).toBe('configured')
|
||||
})
|
||||
|
||||
it('adds a configured official provider to the visible provider list', async () => {
|
||||
@@ -224,10 +228,10 @@ describe('official provider module defaults', () => {
|
||||
|
||||
expect(providerConfigStore.providers['official-provider']?.status).toBe('configured')
|
||||
expect(providerConfigStore.addedProviders['official-provider']).toBe(true)
|
||||
expect(forceProviderConfigured).toHaveBeenCalledOnce()
|
||||
expect(forceProviderConfigured).toHaveBeenCalledTimes(4)
|
||||
|
||||
await expect(configureAsDefaultsIfEmpty()).resolves.toBe(false)
|
||||
expect(forceProviderConfigured).toHaveBeenCalledOnce()
|
||||
expect(forceProviderConfigured).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('keeps a custom module and configures the other empty modules', async () => {
|
||||
@@ -249,9 +253,126 @@ describe('official provider module defaults', () => {
|
||||
expect(speechStore.activeSpeechModel).toBe('auto')
|
||||
expect(visionStore.activeProvider).toBe('vision-official-provider')
|
||||
expect(visionStore.activeModel).toBe('auto')
|
||||
expect(providerConfigStore.providers['official-provider']).toBeUndefined()
|
||||
expect(providerConfigStore.providers['official-provider']?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_SPEECH_PROVIDER_ID]?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers['vision-official-provider']?.status).toBe('configured')
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// Authenticated setup only created an official provider record when its
|
||||
// module was empty or already used that provider. A custom module selection
|
||||
// therefore hid the official choice even while the user was signed in.
|
||||
//
|
||||
// We fixed this by configuring every standard official provider after login
|
||||
// while applying official module selections only to empty modules.
|
||||
it('configures official choices without replacing custom module selections', async () => {
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const hearingStore = useHearingStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const visionStore = useVisionStore()
|
||||
const providerConfigStore = useProviderConfigStore()
|
||||
|
||||
consciousnessStore.activeProvider = 'custom-chat'
|
||||
consciousnessStore.activeModel = 'custom-chat-model'
|
||||
hearingStore.activeTranscriptionProvider = 'custom-transcription'
|
||||
hearingStore.activeTranscriptionModel = 'custom-transcription-model'
|
||||
speechStore.activeSpeechProvider = 'custom-speech'
|
||||
speechStore.activeSpeechModel = 'custom-speech-model'
|
||||
visionStore.activeProvider = 'custom-vision'
|
||||
visionStore.activeModel = 'custom-vision-model'
|
||||
|
||||
await expect(configureAsDefaultsIfEmpty()).resolves.toBe(true)
|
||||
|
||||
expect(consciousnessStore.activeProvider).toBe('custom-chat')
|
||||
expect(consciousnessStore.activeModel).toBe('custom-chat-model')
|
||||
expect(hearingStore.activeTranscriptionProvider).toBe('custom-transcription')
|
||||
expect(hearingStore.activeTranscriptionModel).toBe('custom-transcription-model')
|
||||
expect(speechStore.activeSpeechProvider).toBe('custom-speech')
|
||||
expect(speechStore.activeSpeechModel).toBe('custom-speech-model')
|
||||
expect(visionStore.activeProvider).toBe('custom-vision')
|
||||
expect(visionStore.activeModel).toBe('custom-vision-model')
|
||||
expect(providerConfigStore.providers['official-provider']?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_SPEECH_PROVIDER_ID]?.status).toBe('configured')
|
||||
expect(providerConfigStore.providers['vision-official-provider']?.status).toBe('configured')
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// Official provider records and module selections survived logout. Module
|
||||
// pages therefore still treated official services as configured even though
|
||||
// the authenticated session that grants access no longer existed.
|
||||
//
|
||||
// We fixed this by unconfiguring official records on logout and clearing only
|
||||
// module selections that belong to those records. Custom selections remain.
|
||||
it('removes authenticated official configuration on logout', async () => {
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const hearingStore = useHearingStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const visionStore = useVisionStore()
|
||||
const providerStore = useProviderStore()
|
||||
const providerConfigStore = useProviderConfigStore()
|
||||
|
||||
await configureAsDefaultsIfEmpty()
|
||||
await providerStore.initializeProvider(OFFICIAL_SPEECH_STREAMING_PROVIDER_ID)
|
||||
await providerStore.forceProviderConfigured(OFFICIAL_SPEECH_STREAMING_PROVIDER_ID)
|
||||
speechStore.activeSpeechProvider = OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
|
||||
speechStore.activeSpeechModel = 'streaming-model'
|
||||
|
||||
await expect(unconfigureAuthenticationProviders()).resolves.toBe(true)
|
||||
|
||||
expect(consciousnessStore.activeProvider).toBe('')
|
||||
expect(consciousnessStore.activeModel).toBe('')
|
||||
expect(hearingStore.activeTranscriptionProvider).toBe('')
|
||||
expect(hearingStore.activeTranscriptionModel).toBe('')
|
||||
expect(speechStore.activeSpeechProvider).toBe('speech-noop')
|
||||
expect(speechStore.activeSpeechModel).toBe('')
|
||||
expect(speechStore.activeSpeechVoiceId).toBe('')
|
||||
expect(visionStore.activeProvider).toBe('')
|
||||
expect(visionStore.activeModel).toBe('')
|
||||
|
||||
expect(providerConfigStore.providers['official-provider']?.status).toBe('unconfigured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]?.status).toBe('unconfigured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_SPEECH_PROVIDER_ID]?.status).toBe('unconfigured')
|
||||
expect(providerConfigStore.providers[OFFICIAL_SPEECH_STREAMING_PROVIDER_ID]?.status).toBe('unconfigured')
|
||||
expect(providerConfigStore.providers['vision-official-provider']?.status).toBe('unconfigured')
|
||||
expect(providerConfigStore.addedProviders['official-provider']).toBeUndefined()
|
||||
expect(providerConfigStore.addedProviders[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]).toBeUndefined()
|
||||
expect(providerConfigStore.addedProviders[OFFICIAL_SPEECH_PROVIDER_ID]).toBeUndefined()
|
||||
expect(providerConfigStore.addedProviders[OFFICIAL_SPEECH_STREAMING_PROVIDER_ID]).toBeUndefined()
|
||||
expect(providerConfigStore.addedProviders['vision-official-provider']).toBeUndefined()
|
||||
|
||||
await expect(unconfigureAuthenticationProviders()).resolves.toBe(false)
|
||||
await expect(configureAsDefaultsIfEmpty()).resolves.toBe(true)
|
||||
expect(providerConfigStore.providers['official-provider']?.status).toBe('configured')
|
||||
})
|
||||
|
||||
it('keeps custom module selections when official providers are unconfigured', async () => {
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const hearingStore = useHearingStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const visionStore = useVisionStore()
|
||||
|
||||
consciousnessStore.activeProvider = 'custom-chat'
|
||||
consciousnessStore.activeModel = 'custom-chat-model'
|
||||
hearingStore.activeTranscriptionProvider = 'custom-transcription'
|
||||
hearingStore.activeTranscriptionModel = 'custom-transcription-model'
|
||||
speechStore.activeSpeechProvider = 'speech-noop'
|
||||
speechStore.activeSpeechModel = ''
|
||||
visionStore.activeProvider = 'custom-vision'
|
||||
visionStore.activeModel = 'custom-vision-model'
|
||||
|
||||
await expect(unconfigureAuthenticationProviders()).resolves.toBe(false)
|
||||
|
||||
expect(consciousnessStore.activeProvider).toBe('custom-chat')
|
||||
expect(consciousnessStore.activeModel).toBe('custom-chat-model')
|
||||
expect(hearingStore.activeTranscriptionProvider).toBe('custom-transcription')
|
||||
expect(hearingStore.activeTranscriptionModel).toBe('custom-transcription-model')
|
||||
expect(speechStore.activeSpeechProvider).toBe('speech-noop')
|
||||
expect(speechStore.activeSpeechModel).toBe('')
|
||||
expect(visionStore.activeProvider).toBe('custom-vision')
|
||||
expect(visionStore.activeModel).toBe('custom-vision-model')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_TRANSCRIPTION_PROVIDER_ID } from '../../libs/providers/providers/official'
|
||||
import { OFFICIAL_CHAT_PROVIDER_ID, OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_TRANSCRIPTION_PROVIDER_ID, OFFICIAL_VISION_PROVIDER_ID } from '../../libs/providers/providers/official'
|
||||
import { useProviderConfigStore } from '../providers/config'
|
||||
import { useProviderStore } from '../providers/provider'
|
||||
import { useConsciousnessStore } from './consciousness'
|
||||
@@ -8,20 +8,20 @@ import { useVisionStore } from './vision'
|
||||
|
||||
// Vision provider instances use a category prefix but share the official chat definition.
|
||||
const officialModuleDefaults = {
|
||||
consciousness: { provider: 'official-provider', model: 'auto' },
|
||||
consciousness: { provider: OFFICIAL_CHAT_PROVIDER_ID, model: 'auto' },
|
||||
hearing: { provider: OFFICIAL_TRANSCRIPTION_PROVIDER_ID, model: 'auto' },
|
||||
speech: { provider: OFFICIAL_SPEECH_PROVIDER_ID, model: 'auto' },
|
||||
vision: { provider: 'vision-official-provider', model: 'auto' },
|
||||
vision: { provider: OFFICIAL_VISION_PROVIDER_ID, model: 'auto' },
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Applies official defaults to empty inference modules.
|
||||
*
|
||||
* The caller must own synchronized Pinia leadership. This command keeps the
|
||||
* configuration of each custom provider unchanged. It also repairs missing
|
||||
* provider records for modules that already use an official provider.
|
||||
* Provider setup completes before module selections become visible to other
|
||||
* windows.
|
||||
* configuration of each custom provider unchanged. It configures the official
|
||||
* choice for every module, then applies it only to empty or incomplete
|
||||
* official selections. Provider setup completes before module selections
|
||||
* become visible to other windows.
|
||||
*
|
||||
* @returns `true` when the command changes module or provider state.
|
||||
*/
|
||||
@@ -42,15 +42,7 @@ export async function configureAsDefaultsIfEmpty(): Promise<boolean> {
|
||||
|
||||
const providerStore = useProviderStore()
|
||||
const providerConfigStore = useProviderConfigStore()
|
||||
const managedOfficialProviders = new Set<string>()
|
||||
if (needsConsciousnessDefault || usesOfficialConsciousness)
|
||||
managedOfficialProviders.add(officialModuleDefaults.consciousness.provider)
|
||||
if (needsHearingDefault || usesOfficialHearing)
|
||||
managedOfficialProviders.add(officialModuleDefaults.hearing.provider)
|
||||
if (needsSpeechDefault || usesOfficialSpeech)
|
||||
managedOfficialProviders.add(officialModuleDefaults.speech.provider)
|
||||
if (needsVisionDefault || usesOfficialVision)
|
||||
managedOfficialProviders.add(officialModuleDefaults.vision.provider)
|
||||
const managedOfficialProviders = new Set<string>(Object.values(officialModuleDefaults).map(module => module.provider))
|
||||
|
||||
let changed = false
|
||||
for (const provider of managedOfficialProviders) {
|
||||
@@ -112,3 +104,65 @@ export async function configureAsDefaultsIfEmpty(): Promise<boolean> {
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes provider state that is valid only during an authenticated session.
|
||||
*
|
||||
* The caller must own synchronized Pinia leadership. Custom module selections
|
||||
* remain unchanged. An official speech selection falls back to No Speech so
|
||||
* the speech pipeline keeps its account-free disabled state after logout.
|
||||
*
|
||||
* @returns `true` when the command changes module or provider state.
|
||||
*/
|
||||
export async function unconfigureAuthenticationProviders(): Promise<boolean> {
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const hearingStore = useHearingStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const visionStore = useVisionStore()
|
||||
const providerStore = useProviderStore()
|
||||
const providerConfigStore = useProviderConfigStore()
|
||||
let changed = false
|
||||
|
||||
if (providerConfigStore.providers[consciousnessStore.activeProvider]?.configuredBy === 'authentication') {
|
||||
consciousnessStore.activeProvider = ''
|
||||
consciousnessStore.activeModel = ''
|
||||
consciousnessStore.customModelName = ''
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (providerConfigStore.providers[hearingStore.activeTranscriptionProvider]?.configuredBy === 'authentication') {
|
||||
hearingStore.activeTranscriptionProvider = ''
|
||||
hearingStore.activeTranscriptionModel = ''
|
||||
hearingStore.activeCustomModelName = ''
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (providerConfigStore.providers[speechStore.activeSpeechProvider]?.configuredBy === 'authentication') {
|
||||
speechStore.activeSpeechProvider = 'speech-noop'
|
||||
speechStore.activeSpeechModel = ''
|
||||
speechStore.activeSpeechVoiceId = ''
|
||||
speechStore.activeSpeechVoice = undefined
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (providerConfigStore.providers[visionStore.activeProvider]?.configuredBy === 'authentication') {
|
||||
visionStore.activeProvider = ''
|
||||
visionStore.activeModel = ''
|
||||
visionStore.customModelName = ''
|
||||
changed = true
|
||||
}
|
||||
|
||||
for (const [providerId, provider] of Object.entries(providerConfigStore.providers)) {
|
||||
if (provider.configuredBy !== 'authentication')
|
||||
continue
|
||||
|
||||
const isPersistedAsAdded = providerConfigStore.addedProviders[providerId]
|
||||
if (provider.status === 'unconfigured' && !isPersistedAsAdded)
|
||||
continue
|
||||
|
||||
await providerStore.setProviderUnconfigured(providerId)
|
||||
changed = true
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const localProvider = {
|
||||
definitionId: 'openai-compatible',
|
||||
config: {},
|
||||
status: 'unconfigured',
|
||||
configuredBy: 'user',
|
||||
} satisfies InferenceServiceProvider
|
||||
|
||||
const remoteProvider = {
|
||||
|
||||
@@ -65,6 +65,20 @@ export const useProviderConfigStore = defineStore('provider-config', () => {
|
||||
definitionId,
|
||||
config,
|
||||
status: 'unconfigured',
|
||||
configuredBy: definition.configuredBy ?? 'user',
|
||||
}
|
||||
}
|
||||
|
||||
// Provider definitions own configuration lifecycle policy. Apply that
|
||||
// policy to persisted snapshots before module pages consume them. Providers
|
||||
// without an owner declaration remain user-configured.
|
||||
for (const provider of Object.values(providers.value)) {
|
||||
const configuredByDefinition = getDefinedProvider(provider.definitionId)?.configuredBy
|
||||
if (configuredByDefinition) {
|
||||
provider.configuredBy = configuredByDefinition
|
||||
}
|
||||
else if (!provider.configuredBy) {
|
||||
provider.configuredBy = 'user'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +133,7 @@ export const useProviderConfigStore = defineStore('provider-config', () => {
|
||||
definitionId,
|
||||
config,
|
||||
status: 'unconfigured' as const,
|
||||
configuredBy: definition.configuredBy ?? 'user',
|
||||
}
|
||||
providers.value[providerId] = provider
|
||||
return provider
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
import type { Session, User } from 'better-auth'
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { OFFICIAL_SPEECH_PROVIDER_ID } from '../../libs/providers/providers/official'
|
||||
import { OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID, OFFICIAL_TRANSCRIPTION_PROVIDER_ID } from '../../libs/providers/providers/official'
|
||||
import { useAuthStore } from '../auth'
|
||||
import { useProviderConfigStore } from './config'
|
||||
import { useProviderStore } from './provider'
|
||||
|
||||
@@ -89,6 +91,71 @@ describe('provider store synchronization boundary', () => {
|
||||
expect(getProviderCalls).toBe(0)
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// Module pages treated every credential-free provider as available before
|
||||
// configuration. The official providers also have credential-free local
|
||||
// definitions, but their availability belongs to the authenticated session.
|
||||
//
|
||||
// We fixed this by requiring a configured record for official providers
|
||||
// while keeping account-free browser and local providers available.
|
||||
it('lists official providers only after authenticated setup configures them', async () => {
|
||||
const store = useProviderStore()
|
||||
const configStore = useProviderConfigStore()
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(store.moduleSpeechProvidersMetadata.map(provider => provider.id)).toContain('speech-noop')
|
||||
expect(store.moduleChatProvidersMetadata.map(provider => provider.id)).not.toContain('official-provider')
|
||||
expect(store.moduleSpeechProvidersMetadata.map(provider => provider.id)).not.toContain(OFFICIAL_SPEECH_PROVIDER_ID)
|
||||
expect(store.moduleSpeechProvidersMetadata.map(provider => provider.id)).not.toContain(OFFICIAL_SPEECH_STREAMING_PROVIDER_ID)
|
||||
expect(store.moduleTranscriptionProvidersMetadata.map(provider => provider.id)).not.toContain(OFFICIAL_TRANSCRIPTION_PROVIDER_ID)
|
||||
expect(store.moduleVisionProvidersMetadata.map(provider => provider.id)).not.toContain('vision-official-provider')
|
||||
})
|
||||
|
||||
expect(configStore.providers['official-provider']).toBeUndefined()
|
||||
expect(configStore.providers[OFFICIAL_SPEECH_PROVIDER_ID]).toBeUndefined()
|
||||
expect(configStore.providers[OFFICIAL_TRANSCRIPTION_PROVIDER_ID]).toBeUndefined()
|
||||
expect(configStore.providers['vision-official-provider']).toBeUndefined()
|
||||
|
||||
await store.initializeProvider('official-provider')
|
||||
await store.forceProviderConfigured('official-provider')
|
||||
await store.initializeProvider(OFFICIAL_SPEECH_PROVIDER_ID)
|
||||
await store.forceProviderConfigured(OFFICIAL_SPEECH_PROVIDER_ID)
|
||||
await store.initializeProvider(OFFICIAL_TRANSCRIPTION_PROVIDER_ID)
|
||||
await store.forceProviderConfigured(OFFICIAL_TRANSCRIPTION_PROVIDER_ID)
|
||||
await store.initializeProvider('vision-official-provider')
|
||||
await store.forceProviderConfigured('vision-official-provider')
|
||||
|
||||
expect(store.moduleChatProvidersMetadata.map(provider => provider.id)).not.toContain('official-provider')
|
||||
expect(store.moduleSpeechProvidersMetadata.map(provider => provider.id)).not.toContain(OFFICIAL_SPEECH_PROVIDER_ID)
|
||||
expect(store.moduleTranscriptionProvidersMetadata.map(provider => provider.id)).not.toContain(OFFICIAL_TRANSCRIPTION_PROVIDER_ID)
|
||||
expect(store.moduleVisionProvidersMetadata.map(provider => provider.id)).not.toContain('vision-official-provider')
|
||||
|
||||
const user: User = {
|
||||
id: 'user-1',
|
||||
name: 'AIRI User',
|
||||
email: 'user@example.com',
|
||||
emailVerified: true,
|
||||
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
}
|
||||
const session: Session = {
|
||||
id: 'session-1',
|
||||
token: 'server-session-token',
|
||||
userId: user.id,
|
||||
expiresAt: new Date('2026-12-01T00:00:00.000Z'),
|
||||
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
}
|
||||
useAuthStore().$patch({ user, session })
|
||||
|
||||
expect(store.moduleChatProvidersMetadata.map(provider => provider.id)).toContain('official-provider')
|
||||
expect(store.moduleSpeechProvidersMetadata.map(provider => provider.id)).toContain(OFFICIAL_SPEECH_PROVIDER_ID)
|
||||
expect(store.moduleSpeechProvidersMetadata.map(provider => provider.id)).not.toContain(OFFICIAL_SPEECH_STREAMING_PROVIDER_ID)
|
||||
expect(store.moduleTranscriptionProvidersMetadata.map(provider => provider.id)).toContain(OFFICIAL_TRANSCRIPTION_PROVIDER_ID)
|
||||
expect(store.moduleVisionProvidersMetadata.map(provider => provider.id)).toContain('vision-official-provider')
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// getModelsForProvider created a new empty array for every cache miss.
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
validateProvider as runProviderValidation,
|
||||
} from '../../libs/providers'
|
||||
import { selectProviderMetadata, selectProvidersMetadata } from '../../libs/providers/metadata'
|
||||
import { useAuthStore } from '../auth'
|
||||
import { useProviderConfigStore } from './config'
|
||||
import { normalizeProviderConfigDefaults } from './config-defaults'
|
||||
|
||||
@@ -86,6 +87,7 @@ const useProviderStateStore = defineStore('provider-state', () => {
|
||||
* configuration remains in {@link useProviderConfigStore}.
|
||||
*/
|
||||
export const useProviderStore = defineStore('provider', () => {
|
||||
const authStore = useAuthStore()
|
||||
const providerConfigStore = useProviderConfigStore()
|
||||
const providerStateStore = useProviderStateStore()
|
||||
const providerCredentials = computed(() => providerConfigStore.configs)
|
||||
@@ -714,10 +716,17 @@ export const useProviderStore = defineStore('provider', () => {
|
||||
.filter(d => providerMetadata[d.id])
|
||||
.map(d => projectProvider(d.id))
|
||||
.filter(metadata => metadata !== undefined)
|
||||
// Vision providers reuse chat definitions under separate instance ids.
|
||||
// Include these generated definitions before configured custom instances.
|
||||
const visionDefinitions = definedProviders
|
||||
.filter(definition => providerMetadata[definition.id]?.category === 'chat')
|
||||
.map(definition => projectProvider(`${VISION_PROVIDER_ID_PREFIX}${definition.id}`))
|
||||
.filter(metadata => metadata !== undefined)
|
||||
const definitionIds = new Set([...definitions, ...visionDefinitions].map(metadata => metadata.id))
|
||||
|
||||
const configuredInstances: ProviderMetadata[] = []
|
||||
for (const providerId of Object.keys(providerConfigStore.providers)) {
|
||||
if (providerMetadata[providerId])
|
||||
if (definitionIds.has(providerId))
|
||||
continue
|
||||
|
||||
const metadata = projectProvider(providerId)
|
||||
@@ -725,7 +734,7 @@ export const useProviderStore = defineStore('provider', () => {
|
||||
configuredInstances.push(metadata)
|
||||
}
|
||||
|
||||
return [...definitions, ...configuredInstances]
|
||||
return [...definitions, ...visionDefinitions, ...configuredInstances]
|
||||
})
|
||||
|
||||
function getTranscriptionFeatures(providerId: string) {
|
||||
@@ -875,6 +884,57 @@ export const useProviderStore = defineStore('provider', () => {
|
||||
return !!addedProviders.value[providerId] || isProviderConfigDirty(providerId)
|
||||
}
|
||||
|
||||
function isProviderAvailableWithoutConfiguration(providerId: string) {
|
||||
return providerConfiguredBy(providerId) !== 'authentication'
|
||||
&& getProviderDefinition(providerId).requiresCredentials === false
|
||||
}
|
||||
|
||||
function providerConfiguredBy(providerId: string) {
|
||||
const configuredProvider = providerConfigStore.providers[providerId]
|
||||
if (configuredProvider)
|
||||
return configuredProvider.configuredBy
|
||||
|
||||
return getProviderDefinition(providerId).configuredBy ?? 'user'
|
||||
}
|
||||
|
||||
function isProviderConfiguredForModule(providerId: string) {
|
||||
return providerConfigStore.configuredProviders[providerId]
|
||||
&& (providerConfiguredBy(providerId) !== 'authentication' || authStore.isAuthenticated)
|
||||
}
|
||||
|
||||
// Authentication-owned providers do not require a user-supplied API key,
|
||||
// but they do require an authenticated session. Browser and local providers
|
||||
// remain available without a persisted configuration record.
|
||||
const moduleChatProvidersMetadata = computed(() => {
|
||||
return allChatProvidersMetadata.value.filter(metadata =>
|
||||
isProviderConfiguredForModule(metadata.id)
|
||||
|| (providerConfiguredBy(metadata.id) !== 'authentication' && shouldListProvider(metadata.id))
|
||||
|| isProviderAvailableWithoutConfiguration(metadata.id),
|
||||
)
|
||||
})
|
||||
|
||||
const moduleSpeechProvidersMetadata = computed(() => {
|
||||
return allAudioSpeechProvidersMetadata.value.filter(metadata =>
|
||||
isProviderConfiguredForModule(metadata.id)
|
||||
|| isProviderAvailableWithoutConfiguration(metadata.id),
|
||||
)
|
||||
})
|
||||
|
||||
const moduleTranscriptionProvidersMetadata = computed(() => {
|
||||
return allAudioTranscriptionProvidersMetadata.value.filter(metadata =>
|
||||
isProviderConfiguredForModule(metadata.id)
|
||||
|| isProviderAvailableWithoutConfiguration(metadata.id),
|
||||
)
|
||||
})
|
||||
|
||||
const moduleVisionProvidersMetadata = computed(() => {
|
||||
return allVisionProvidersMetadata.value.filter(metadata =>
|
||||
isProviderConfiguredForModule(metadata.id)
|
||||
|| (providerConfiguredBy(metadata.id) !== 'authentication' && shouldListProvider(metadata.id))
|
||||
|| isProviderAvailableWithoutConfiguration(metadata.id),
|
||||
)
|
||||
})
|
||||
|
||||
const persistedProvidersMetadata = computed(() => {
|
||||
return availableProvidersMetadata.value.filter(metadata => shouldListProvider(metadata.id))
|
||||
})
|
||||
@@ -925,6 +985,10 @@ export const useProviderStore = defineStore('provider', () => {
|
||||
configuredSpeechProvidersMetadata,
|
||||
configuredTranscriptionProvidersMetadata,
|
||||
configuredVisionProvidersMetadata,
|
||||
moduleChatProvidersMetadata,
|
||||
moduleSpeechProvidersMetadata,
|
||||
moduleTranscriptionProvidersMetadata,
|
||||
moduleVisionProvidersMetadata,
|
||||
persistedChatProvidersMetadata,
|
||||
persistedVisionProvidersMetadata,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user