fix(stage-ui): correct provider source filter categories (#1929)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: LemonNeko <self@lemonneko.moe>
This commit is contained in:
Doji
2026-06-02 16:02:00 +00:00
committed by GitHub
co-authored by autofix-ci[bot] LemonNeko
parent 4d496b18d7
commit 88fac74702
7 changed files with 435 additions and 39 deletions
@@ -1,4 +1,7 @@
<script setup lang="ts">
import type { ProviderSourceDeployment, ProviderSourcePricing } from '@proj-airi/stage-ui/libs/providers/source-metadata'
import type { Ref } from 'vue'
import { IconStatusItem, RippleGrid } from '@proj-airi/stage-ui/components'
import { useAnalytics } from '@proj-airi/stage-ui/composables'
import { useRippleGridState } from '@proj-airi/stage-ui/composables/use-ripple-grid-state'
@@ -9,6 +12,31 @@ import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
interface ProviderSourceCard {
id: string
category: string
to?: string
icon?: string
iconColor?: string
iconImage?: string
name?: string
description?: string
localizedName?: string
localizedDescription?: string
configured?: boolean
pricing?: ProviderSourcePricing
deployment?: ProviderSourceDeployment
beginnerRecommended?: boolean
}
interface ProviderBlockConfig {
id: string
icon: string
title: string
description: string
providersRef: Readonly<Ref<ProviderSourceCard[]>>
}
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
@@ -23,7 +51,7 @@ const {
allAudioTranscriptionProvidersMetadata,
} = storeToRefs(providersStore)
const allArtistryProvidersMetadata = computed(() => {
const allArtistryProvidersMetadata = computed<ProviderSourceCard[]>(() => {
return [
{
id: 'comfyui',
@@ -54,7 +82,6 @@ const allArtistryProvidersMetadata = computed(() => {
to: '/settings/providers/artistry/replicate',
pricing: 'paid',
deployment: 'cloud',
beginnerRecommended: false,
iconImage: undefined,
},
{
@@ -70,13 +97,12 @@ const allArtistryProvidersMetadata = computed(() => {
to: '/settings/providers/artistry/nanobanana',
pricing: 'free',
deployment: 'cloud',
beginnerRecommended: false,
iconImage: undefined,
},
]
})
const providerBlocksConfig = [
const providerBlocksConfig: ProviderBlockConfig[] = [
{
id: 'chat',
icon: 'i-solar:chat-square-like-bold-duotone',
@@ -133,7 +159,7 @@ const providerBlocks = computed(() => {
.filter(block => block.id === activeTabId.value)
.map((block) => {
const filteredProviders = block.providersRef.value
.filter((p: any) => {
.filter((p) => {
if (filterPricing.value !== 'all' && p.pricing !== filterPricing.value)
return false
if (filterDeployment.value !== 'all' && p.deployment !== filterDeployment.value)
@@ -251,10 +277,10 @@ const providerBlocks = computed(() => {
:icon="provider.icon"
:icon-color="provider.iconColor"
:icon-image="provider.iconImage"
:to="`/settings/providers/${provider.category}/${provider.id}`"
:to="provider.to ?? `/settings/providers/${provider.category}/${provider.id}`"
:configured="provider.configured"
:pricing="provider.pricing as any"
:deployment="provider.deployment as any"
:pricing="provider.pricing"
:deployment="provider.deployment"
:beginner-recommended="provider.beginnerRecommended"
@click="trackProviderClick(provider.id, provider.category)"
/>
@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { ProviderSourceDeployment, ProviderSourcePricing } from '../../libs/providers/source-metadata'
const props = defineProps<{
title: string
description?: string
@@ -7,8 +9,8 @@ const props = defineProps<{
iconImage?: string
to: string
configured?: boolean
pricing?: 'free' | 'paid' | 'internal'
deployment?: 'local' | 'cloud'
pricing?: ProviderSourcePricing
deployment?: ProviderSourceDeployment
beginnerRecommended?: boolean
}>()
</script>
@@ -1,3 +1,4 @@
export * from './providers'
export * from './source-metadata'
export * from './types'
export * from './validators'
@@ -0,0 +1,144 @@
export type ProviderSourcePricing = 'free' | 'paid'
export type ProviderSourceDeployment = 'local' | 'cloud'
/**
* Represents source catalogue tags used by provider filtering UI.
*/
export interface ProviderSourceMetadata {
/** Price bucket shown by the provider source filter. */
pricing?: ProviderSourcePricing
/** Runtime/deployment bucket shown by the provider source filter. */
deployment?: ProviderSourceDeployment
/** Whether the provider should receive the existing recommended tag. */
beginnerRecommended?: boolean
}
export interface ProviderSourceMetadataInput {
id?: string
}
const paidCloud = {
pricing: 'paid',
deployment: 'cloud',
} satisfies ProviderSourceMetadata
const freeLocal = {
pricing: 'free',
deployment: 'local',
} satisfies ProviderSourceMetadata
const recommendedPaidCloud = {
...paidCloud,
beginnerRecommended: true,
} satisfies ProviderSourceMetadata
const providerSourceMetadataById = {
'302-ai': paidCloud,
'aihubmix': paidCloud,
'alibaba-cloud-model-studio': paidCloud,
'aliyun-nls-transcription': paidCloud,
'amazon-bedrock': paidCloud,
'anthropic': paidCloud,
'app-local-audio-speech': freeLocal,
'app-local-audio-transcription': freeLocal,
'azure-ai-foundry': paidCloud,
'azure-openai': paidCloud,
'browser-local-audio-speech': freeLocal,
'browser-local-audio-transcription': freeLocal,
'browser-web-speech-api': freeLocal,
'byteplus': paidCloud,
'byteplus-coding-plan': paidCloud,
'cerebras-ai': paidCloud,
'cloudflare-workers-ai': paidCloud,
'comet-api': paidCloud,
'comet-api-speech': paidCloud,
'comet-api-transcription': paidCloud,
'deepgram-tts': paidCloud,
'deepseek': paidCloud,
'elevenlabs': paidCloud,
'featherless-ai': paidCloud,
'fireworks-ai': paidCloud,
'google-generative-ai': paidCloud,
'groq': paidCloud,
'index-tts-vllm': freeLocal,
'kokoro-local': freeLocal,
'lm-studio': freeLocal,
'microsoft-speech': paidCloud,
'mimo': paidCloud,
'mimo-audio-speech': paidCloud,
'mimo-audio-transcription': paidCloud,
'minimax': paidCloud,
'minimax-global': paidCloud,
'minimax-speech': paidCloud,
'mistral-ai': paidCloud,
'modelscope': paidCloud,
'moonshot-ai': paidCloud,
'n1n': paidCloud,
'novita-ai': paidCloud,
'nvidia': paidCloud,
'official-provider': recommendedPaidCloud,
'official-provider-speech': recommendedPaidCloud,
'official-provider-speech-streaming': recommendedPaidCloud,
'ollama': freeLocal,
'openai': paidCloud,
'openai-audio-speech': paidCloud,
'openai-audio-transcription': paidCloud,
'openai-compatible': false,
'openai-compatible-audio-speech': false,
'openai-compatible-audio-transcription': false,
'openrouter-ai': paidCloud,
'openrouter-audio-speech': paidCloud,
'perplexity-ai': paidCloud,
'player2-speech': freeLocal,
'speech-noop': false,
'together-ai': paidCloud,
'volcengine': paidCloud,
'volcengine-coding-plan': paidCloud,
'xai': paidCloud,
'zai': paidCloud,
} satisfies Record<string, ProviderSourceMetadata | false>
/**
* Normalizes provider source metadata by dropping undefined fields.
*
* Before:
* - `{ pricing: "paid", deployment: undefined }`
*
* After:
* - `{ pricing: "paid" }`
*/
function compactProviderSourceMetadata(metadata: ProviderSourceMetadata): ProviderSourceMetadata {
return {
...(metadata.pricing ? { pricing: metadata.pricing } : {}),
...(metadata.deployment ? { deployment: metadata.deployment } : {}),
...(metadata.beginnerRecommended !== undefined ? { beginnerRecommended: metadata.beginnerRecommended } : {}),
}
}
/**
* Resolves the provider source tags used by settings/provider filtering.
*
* Use when:
* - Rendering provider source cards.
* - Converting defineProvider() catalogue entries to legacy ProviderMetadata.
*
* Expects:
* - `metadata.id` may identify a provider with catalogue metadata.
*
* Returns:
* - Compact metadata with only meaningful tag fields.
*/
export function resolveProviderSourceMetadata(
metadata: ProviderSourceMetadataInput = {},
): ProviderSourceMetadata {
if (!metadata.id)
return {}
const sourceMetadata = providerSourceMetadataById[metadata.id as keyof typeof providerSourceMetadataById]
if (sourceMetadata === false)
return {}
if (sourceMetadata)
return compactProviderSourceMetadata(sourceMetadata)
return {}
}
+13 -5
View File
@@ -19,9 +19,11 @@ import type {
VoiceProviderWithExtraOptions,
} from 'unspeech'
import type { ProviderSourceDeployment, ProviderSourcePricing } from '../libs/providers/source-metadata'
import type { ProviderOnboardingField } from '../libs/providers/types'
import type { AliyunRealtimeSpeechExtraOptions } from './providers/aliyun/stream-transcription'
import { errorMessageFrom } from '@moeru/std'
import { isStageTamagotchi, isUrl } from '@proj-airi/stage-shared'
import { getCachedWebGPUCapabilities, isWebGPUSupported } from '@proj-airi/stage-shared/webgpu'
import { computedAsync, useIntervalFn, useLocalStorage } from '@vueuse/core'
@@ -51,6 +53,7 @@ import { useI18n } from 'vue-i18n'
import { getKokoroAdapter } from '../libs/inference/adapters/kokoro'
import { getProviderValidationIntervalMs, listProviders as listDefinedProviders, ProviderValidationCheck } from '../libs/providers'
import { resolveProviderSourceMetadata } from '../libs/providers/source-metadata'
import { getDefaultKokoroModel, KOKORO_MODELS, kokoroModelsToModelInfo } from '../workers/kokoro/constants'
import { useAuthStore } from './auth'
import { createAliyunNLSProvider as createAliyunNlsStreamProvider } from './providers/aliyun/stream-transcription'
@@ -181,8 +184,8 @@ export interface ProviderMetadata {
supportsStreamOutput: boolean
supportsStreamInput: boolean
}
pricing?: 'free' | 'paid' | 'internal'
deployment?: 'local' | 'cloud'
pricing?: ProviderSourcePricing
deployment?: ProviderSourceDeployment
beginnerRecommended?: boolean
}
@@ -2238,6 +2241,7 @@ export const useProvidersStore = defineStore('providers', () => {
// translate unified provider definitions from libs/providers to legacy store metadata.
// Existing metadata remains as fallback for providers not yet migrated.
const definedProviders = listDefinedProviders()
const definedProviderIds = new Set(definedProviders.map(d => d.id))
const translatedProviderMetadata = convertProviderDefinitionsToMetadata(
definedProviders,
@@ -2266,6 +2270,12 @@ export const useProvidersStore = defineStore('providers', () => {
providerMetadata[providerId] = translated
}
for (const metadata of Object.values(providerMetadata)) {
if (definedProviderIds.has(metadata.id))
continue
Object.assign(metadata, resolveProviderSourceMetadata(metadata))
}
// const validatedCredentials = ref<Record<string, string>>({})
const providerRuntimeState = ref<Record<string, ProviderRuntimeState>>({})
const providerValidationInFlight = new Map<string, Promise<boolean>>()
@@ -2536,7 +2546,7 @@ export const useProvidersStore = defineStore('providers', () => {
catch (error) {
console.error(`Error fetching models for ${providerId}:`, error)
if (runtimeState) {
runtimeState.modelLoadError = error instanceof Error ? error.message : 'Unknown error'
runtimeState.modelLoadError = errorMessageFrom(error) ?? 'Unknown error'
}
return []
}
@@ -2613,8 +2623,6 @@ export const useProvidersStore = defineStore('providers', () => {
// Get all providers metadata (for settings page).
// Order: defined providers first (already sorted by order in registry), then legacy-only providers.
const definedProviderIds = new Set(definedProviders.map(d => d.id))
const allProvidersMetadata = computed(() => {
const localize = (metadata: ProviderMetadata) => ({
...metadata,
@@ -5,6 +5,7 @@ import type { ProviderMetadata } from '../providers'
import { listModels } from '@xsai/model'
import { resolveProviderSourceMetadata } from '../../libs/providers/source-metadata'
import { CHAT_COMPLETIONS_VALIDATOR_ID, isModelProvider } from '../../libs/providers/types'
import { getValidatorsOfProvider, validateProvider } from '../../libs/providers/validators/run'
@@ -99,6 +100,7 @@ export function convertProviderDefinitionToMetadata(
const keyExtractor = (input: string): string => input
const category = getCategoryFromTasks(definition.tasks)
const schemaDefaults = extractSchemaDefaults(definition, t)
const providerSourceMetadata = resolveProviderSourceMetadata(definition)
return {
id: definition.id,
order: definition.order,
@@ -111,6 +113,7 @@ export function convertProviderDefinitionToMetadata(
icon: definition.icon,
iconColor: definition.iconColor,
iconImage: definition.iconImage,
...providerSourceMetadata,
isAvailableBy: definition.isAvailableBy,
requiresCredentials: definition.requiresCredentials,
onboardingFields: definition.onboardingFields?.({ t }),