Download dashboard/src/composables/useProviderSources.ts from qa1145/astrbbbb: direct link, hf CLI and curl.
- Browser
- Download file 22.1 kB
-
https://huggingface.co/spaces/qa1145/astrbbbb/resolve/main/dashboard/src/composables/useProviderSources.ts
- Command line
-
hf download hf://spaces/qa1145/astrbbbb/dashboard/src/composables/useProviderSources.ts
-
curl -L -o useProviderSources.ts https://huggingface.co/spaces/qa1145/astrbbbb/resolve/main/dashboard/src/composables/useProviderSources.ts
22.1 kB
| import { ref, computed, onMounted, nextTick, watch } from 'vue' | |
| import axios from 'axios' | |
| import { getProviderIcon } from '@/utils/providerUtils' | |
| import { askForConfirmation as askForConfirmationDialog, useConfirmDialog } from '@/utils/confirmDialog' | |
| import { normalizeTextInput } from '@/utils/inputValue' | |
| export interface UseProviderSourcesOptions { | |
| defaultTab?: string | |
| tm: (key: string, params?: Record<string, unknown>) => string | |
| showMessage: (message: string, color?: string) => void | |
| } | |
| export function resolveDefaultTab(value?: string) { | |
| const normalized = (value || '').toLowerCase() | |
| if (normalized.startsWith('select_agent_runner_provider') || normalized === 'agent_runner') { | |
| return 'agent_runner' | |
| } | |
| if (normalized === 'select_provider_stt' || normalized === 'speech_to_text' || normalized.includes('stt')) { | |
| return 'speech_to_text' | |
| } | |
| if (normalized === 'select_provider_tts' || normalized === 'text_to_speech' || normalized.includes('tts')) { | |
| return 'text_to_speech' | |
| } | |
| if (normalized.includes('embedding')) { | |
| return 'embedding' | |
| } | |
| if (normalized.includes('rerank')) { | |
| return 'rerank' | |
| } | |
| return 'chat_completion' | |
| } | |
| export function useProviderSources(options: UseProviderSourcesOptions) { | |
| const { tm, showMessage } = options | |
| const confirmDialog = useConfirmDialog() | |
| async function askForConfirmation(message: string) { | |
| return askForConfirmationDialog(message, confirmDialog) | |
| } | |
| // ===== State ===== | |
| const config = ref<Record<string, any>>({}) | |
| const metadata = ref<Record<string, any>>({}) | |
| const providerSources = ref<any[]>([]) | |
| const providers = ref<any[]>([]) | |
| const selectedProviderType = ref<string>(resolveDefaultTab(options.defaultTab)) | |
| const selectedProviderSource = ref<any | null>(null) | |
| const selectedProviderSourceOriginalId = ref<string | null>(null) | |
| const editableProviderSource = ref<any | null>(null) | |
| const availableModels = ref<any[]>([]) | |
| const modelMetadata = ref<Record<string, any>>({}) | |
| const loadingModels = ref(false) | |
| const savingSource = ref(false) | |
| const testingProviders = ref<string[]>([]) | |
| const isSourceModified = ref(false) | |
| const configSchema = ref<Record<string, any>>({}) | |
| const providerTemplates = ref<Record<string, any>>({}) | |
| const manualModelId = ref('') | |
| const modelSearch = ref('') | |
| let suppressSourceWatch = false | |
| const providerTypes = computed(() => [ | |
| { value: 'chat_completion', label: tm('providers.tabs.chatCompletion'), icon: 'mdi-message-text' }, | |
| { value: 'agent_runner', label: tm('providers.tabs.agentRunner'), icon: 'mdi-robot' }, | |
| { value: 'speech_to_text', label: tm('providers.tabs.speechToText'), icon: 'mdi-microphone-message' }, | |
| { value: 'text_to_speech', label: tm('providers.tabs.textToSpeech'), icon: 'mdi-volume-high' }, | |
| { value: 'embedding', label: tm('providers.tabs.embedding'), icon: 'mdi-code-json' }, | |
| { value: 'rerank', label: tm('providers.tabs.rerank'), icon: 'mdi-compare-vertical' } | |
| ]) | |
| // ===== Computed ===== | |
| const availableSourceTypes = computed(() => { | |
| if (!providerTemplates.value || Object.keys(providerTemplates.value).length === 0) { | |
| return [] | |
| } | |
| const types: Array<{ value: string; label: string; icon: string }> = [] | |
| for (const [templateName, template] of Object.entries(providerTemplates.value)) { | |
| if (template.provider_type === selectedProviderType.value) { | |
| types.push({ | |
| value: templateName, | |
| label: templateName, | |
| icon: getProviderIcon(template.provider) | |
| }) | |
| } | |
| } | |
| return types | |
| }) | |
| const filteredProviderSources = computed(() => { | |
| if (!providerSources.value) return [] | |
| return providerSources.value.filter((source) => | |
| source.provider_type === selectedProviderType.value || | |
| (source.type && isTypeMatchingProviderType(source.type, selectedProviderType.value)) | |
| ) | |
| }) | |
| const displayedProviderSources = computed(() => { | |
| return filteredProviderSources.value || [] | |
| }) | |
| const sourceProviders = computed(() => { | |
| if (!selectedProviderSource.value || !providers.value) return [] | |
| return providers.value.filter((p) => p.provider_source_id === selectedProviderSource.value.id) | |
| }) | |
| const existingModelsForSelectedSource = computed(() => { | |
| if (!selectedProviderSource.value) return new Set<string>() | |
| return new Set(sourceProviders.value.map((p: any) => p.model)) | |
| }) | |
| const sortedAvailableModels = computed(() => { | |
| const existing = existingModelsForSelectedSource.value | |
| return [...(availableModels.value || [])].sort((a, b) => { | |
| const aName = typeof a === 'string' ? a : a?.name | |
| const bName = typeof b === 'string' ? b : b?.name | |
| const aExists = existing.has(aName) | |
| const bExists = existing.has(bName) | |
| if (aExists && !bExists) return -1 | |
| if (!aExists && bExists) return 1 | |
| return 0 | |
| }) | |
| }) | |
| const mergedModelEntries = computed(() => { | |
| const configuredEntries = (sourceProviders.value || []).map((provider: any) => ({ | |
| type: 'configured', | |
| provider, | |
| metadata: getModelMetadata(provider.model) | |
| })) | |
| const availableEntries = (sortedAvailableModels.value || []) | |
| .filter((item: any) => { | |
| const name = typeof item === 'string' ? item : item?.name | |
| return !existingModelsForSelectedSource.value.has(name) | |
| }) | |
| .map((item: any) => { | |
| const name = typeof item === 'string' ? item : item?.name | |
| return { | |
| type: 'available', | |
| model: name, | |
| metadata: typeof item === 'object' ? item?.metadata : getModelMetadata(name) | |
| } | |
| }) | |
| return [...configuredEntries, ...availableEntries] | |
| }) | |
| const filteredMergedModelEntries = computed(() => { | |
| const term = normalizeTextInput(modelSearch.value).trim().toLowerCase() | |
| if (!term) return mergedModelEntries.value | |
| return mergedModelEntries.value.filter((entry: any) => { | |
| if (entry.type === 'configured') { | |
| const id = entry.provider.id?.toLowerCase() || '' | |
| const model = entry.provider.model?.toLowerCase() || '' | |
| return id.includes(term) || model.includes(term) | |
| } | |
| const model = entry.model?.toLowerCase() || '' | |
| return model.includes(term) | |
| }) | |
| }) | |
| const manualProviderId = computed(() => { | |
| if (!selectedProviderSource.value) return '' | |
| const modelId = manualModelId.value.trim() | |
| if (!modelId) return '' | |
| return `${selectedProviderSource.value.id}/${modelId}` | |
| }) | |
| const basicSourceConfig = computed(() => { | |
| if (!editableProviderSource.value) return null | |
| const fields = ['id', 'key', 'api_base'] | |
| const basic: Record<string, any> = {} | |
| fields.forEach((field) => { | |
| Object.defineProperty(basic, field, { | |
| get() { | |
| return editableProviderSource.value![field] | |
| }, | |
| set(val) { | |
| editableProviderSource.value![field] = val | |
| }, | |
| enumerable: true | |
| }) | |
| }) | |
| return basic | |
| }) | |
| const advancedSourceConfig = computed(() => { | |
| if (!editableProviderSource.value) return null | |
| const excluded = ['id', 'key', 'api_base', 'enable', 'type', 'provider_type', 'provider'] | |
| const advanced: Record<string, any> = {} | |
| for (const key of Object.keys(editableProviderSource.value)) { | |
| if (excluded.includes(key)) continue | |
| Object.defineProperty(advanced, key, { | |
| get() { | |
| return editableProviderSource.value![key] | |
| }, | |
| set(val) { | |
| editableProviderSource.value![key] = val | |
| }, | |
| enumerable: true | |
| }) | |
| } | |
| return advanced | |
| }) | |
| const filteredProviders = computed(() => { | |
| if (!providers.value || selectedProviderType.value === 'chat_completion') { | |
| return [] | |
| } | |
| return providers.value.filter((provider: any) => getProviderType(provider) === selectedProviderType.value) | |
| }) | |
| const providerSourceSchema = computed(() => { | |
| if (!configSchema.value || !configSchema.value.provider) { | |
| return configSchema.value | |
| } | |
| // 创建一个深拷贝以避免修改原始 schema | |
| const customSchema = JSON.parse(JSON.stringify(configSchema.value)) | |
| // 为 provider source 的 id 字段添加自定义 hint | |
| if (customSchema.provider?.items?.id) { | |
| customSchema.provider.items.id.hint = tm('providerSources.hints.id') | |
| customSchema.provider.items.key.hint = tm('providerSources.hints.key') | |
| customSchema.provider.items.api_base.hint = tm('providerSources.hints.apiBase') | |
| } | |
| // 为 proxy 字段添加描述和提示 | |
| if (customSchema.provider?.items?.proxy) { | |
| customSchema.provider.items.proxy.description = tm('providerSources.labels.proxy') | |
| customSchema.provider.items.proxy.hint = tm('providerSources.hints.proxy') | |
| } | |
| return customSchema | |
| }) | |
| // ===== Watches ===== | |
| watch(editableProviderSource, () => { | |
| if (suppressSourceWatch) return | |
| if (!editableProviderSource.value) return | |
| isSourceModified.value = true | |
| }, { deep: true }) | |
| // ===== Helper Functions ===== | |
| function isTypeMatchingProviderType(type?: string, providerType?: string) { | |
| if (!type || !providerType) return false | |
| if (providerType === 'chat_completion') { | |
| return type.includes('chat_completion') | |
| } | |
| return type.includes(providerType) | |
| } | |
| function resolveSourceIcon(source: any) { | |
| if (!source) return '' | |
| return getProviderIcon(source.provider) || '' | |
| } | |
| function getSourceDisplayName(source: any) { | |
| if (!source) return '' | |
| if (source.isPlaceholder) return source.templateKey || source.id || '' | |
| return source.id | |
| } | |
| function getModelMetadata(modelName?: string) { | |
| if (!modelName) return null | |
| return modelMetadata.value?.[modelName] || null | |
| } | |
| function supportsImageInput(meta: any) { | |
| const inputs = meta?.modalities?.input || [] | |
| return inputs.includes('image') | |
| } | |
| function supportsToolCall(meta: any) { | |
| return Boolean(meta?.tool_call) | |
| } | |
| function supportsReasoning(meta: any) { | |
| return Boolean(meta?.reasoning) | |
| } | |
| function formatContextLimit(meta: any) { | |
| const ctx = meta?.limit?.context | |
| if (!ctx || typeof ctx !== 'number') return '' | |
| if (ctx >= 1_000_000) return `${Math.round(ctx / 1_000_000)}M` | |
| if (ctx >= 1_000) return `${Math.round(ctx / 1_000)}K` | |
| return `${ctx}` | |
| } | |
| function getProviderType(provider: any) { | |
| if (!provider) return undefined | |
| if (provider.provider_type) { | |
| return provider.provider_type | |
| } | |
| const oldVersionProviderTypeMapping: Record<string, string> = { | |
| openai_chat_completion: 'chat_completion', | |
| anthropic_chat_completion: 'chat_completion', | |
| googlegenai_chat_completion: 'chat_completion', | |
| zhipu_chat_completion: 'chat_completion', | |
| dify: 'agent_runner', | |
| coze: 'agent_runner', | |
| dashscope: 'chat_completion', | |
| openai_whisper_api: 'speech_to_text', | |
| openai_whisper_selfhost: 'speech_to_text', | |
| sensevoice_stt_selfhost: 'speech_to_text', | |
| openai_tts_api: 'text_to_speech', | |
| edge_tts: 'text_to_speech', | |
| gsvi_tts_api: 'text_to_speech', | |
| fishaudio_tts_api: 'text_to_speech', | |
| dashscope_tts: 'text_to_speech', | |
| azure_tts: 'text_to_speech', | |
| minimax_tts_api: 'text_to_speech', | |
| volcengine_tts: 'text_to_speech' | |
| } | |
| return oldVersionProviderTypeMapping[provider.type] | |
| } | |
| function selectProviderSource(source: any) { | |
| if (source?.isPlaceholder && source.templateKey) { | |
| addProviderSource(source.templateKey) | |
| return | |
| } | |
| selectedProviderSource.value = source | |
| selectedProviderSourceOriginalId.value = source?.id || null | |
| suppressSourceWatch = true | |
| editableProviderSource.value = source ? JSON.parse(JSON.stringify(source)) : null | |
| nextTick(() => { | |
| suppressSourceWatch = false | |
| }) | |
| availableModels.value = [] | |
| modelMetadata.value = {} | |
| isSourceModified.value = false | |
| } | |
| function extractSourceFieldsFromTemplate(template: Record<string, any>) { | |
| const sourceFields: Record<string, any> = {} | |
| const excludeKeys = ['id', 'enable', 'model', 'provider_source_id', 'modalities', 'custom_extra_body'] | |
| for (const [key, value] of Object.entries(template)) { | |
| if (!excludeKeys.includes(key)) { | |
| sourceFields[key] = value | |
| } | |
| } | |
| return sourceFields | |
| } | |
| function generateUniqueSourceId(baseId: string) { | |
| const existingIds = new Set(providerSources.value.map((s: any) => s.id)) | |
| if (!existingIds.has(baseId)) return baseId | |
| let counter = 1 | |
| let candidate = `${baseId}_${counter}` | |
| while (existingIds.has(candidate)) { | |
| counter += 1 | |
| candidate = `${baseId}_${counter}` | |
| } | |
| return candidate | |
| } | |
| function addProviderSource(templateKey: string) { | |
| const template = providerTemplates.value[templateKey] | |
| if (!template) { | |
| showMessage('未找到对应的模板配置', 'error') | |
| return | |
| } | |
| const newId = generateUniqueSourceId(template.id) | |
| const newSource = { | |
| ...extractSourceFieldsFromTemplate(template), | |
| id: newId, | |
| type: template.type, | |
| provider_type: template.provider_type, | |
| provider: template.provider, | |
| enable: true | |
| } | |
| providerSources.value.push(newSource) | |
| selectedProviderSource.value = newSource | |
| selectedProviderSourceOriginalId.value = newId | |
| editableProviderSource.value = JSON.parse(JSON.stringify(newSource)) | |
| availableModels.value = [] | |
| modelMetadata.value = {} | |
| isSourceModified.value = true | |
| } | |
| async function deleteProviderSource(source: any) { | |
| const confirmed = await askForConfirmation( | |
| tm('providerSources.deleteConfirm', { id: source.id }) | |
| ) | |
| if (!confirmed) return | |
| try { | |
| await axios.post('/api/config/provider_sources/delete', { id: source.id }) | |
| providers.value = providers.value.filter((p) => p.provider_source_id !== source.id) | |
| providerSources.value = providerSources.value.filter((s) => s.id !== source.id) | |
| if (selectedProviderSource.value?.id === source.id) { | |
| selectedProviderSource.value = null | |
| selectedProviderSourceOriginalId.value = null | |
| editableProviderSource.value = null | |
| } | |
| showMessage(tm('providerSources.deleteSuccess')) | |
| } catch (error: any) { | |
| showMessage(error.message || tm('providerSources.deleteError'), 'error') | |
| } finally { | |
| await loadConfig() | |
| } | |
| } | |
| async function saveProviderSource() { | |
| if (!selectedProviderSource.value) return | |
| savingSource.value = true | |
| const originalId = selectedProviderSourceOriginalId.value || selectedProviderSource.value.id | |
| try { | |
| const response = await axios.post('/api/config/provider_sources/update', { | |
| config: editableProviderSource.value, | |
| original_id: originalId | |
| }) | |
| if (response.data.status !== 'ok') { | |
| throw new Error(response.data.message) | |
| } | |
| if (editableProviderSource.value!.id !== originalId) { | |
| providers.value = providers.value.map((p) => | |
| p.provider_source_id === originalId | |
| ? { ...p, provider_source_id: editableProviderSource.value!.id } | |
| : p | |
| ) | |
| selectedProviderSourceOriginalId.value = editableProviderSource.value!.id | |
| } | |
| const idx = providerSources.value.findIndex((ps) => ps.id === originalId) | |
| if (idx !== -1) { | |
| providerSources.value[idx] = JSON.parse(JSON.stringify(editableProviderSource.value)) | |
| selectedProviderSource.value = providerSources.value[idx] | |
| } | |
| suppressSourceWatch = true | |
| editableProviderSource.value = selectedProviderSource.value | |
| nextTick(() => { | |
| suppressSourceWatch = false | |
| }) | |
| isSourceModified.value = false | |
| showMessage(response.data.message || tm('providerSources.saveSuccess')) | |
| return true | |
| } catch (error: any) { | |
| showMessage(error.response?.data?.message || error.message || tm('providerSources.saveError'), 'error') | |
| return false | |
| } finally { | |
| savingSource.value = false | |
| loadConfig() | |
| } | |
| } | |
| async function fetchAvailableModels() { | |
| if (!selectedProviderSource.value) return | |
| if (isSourceModified.value) { | |
| const saved = await saveProviderSource() | |
| if (!saved) { | |
| return | |
| } | |
| } | |
| loadingModels.value = true | |
| try { | |
| const sourceId = editableProviderSource.value?.id || selectedProviderSource.value.id | |
| const response = await axios.get('/api/config/provider_sources/models', { | |
| params: { source_id: sourceId } | |
| }) | |
| if (response.data.status === 'ok') { | |
| const metadataMap = response.data.data.model_metadata || {} | |
| modelMetadata.value = metadataMap | |
| availableModels.value = (response.data.data.models || []).map((model: string) => ({ | |
| name: model, | |
| metadata: metadataMap?.[model] || null | |
| })) | |
| if (availableModels.value.length === 0) { | |
| showMessage(tm('models.noModelsFound'), 'info') | |
| } | |
| } else { | |
| throw new Error(response.data.message) | |
| } | |
| } catch (error: any) { | |
| modelMetadata.value = {} | |
| showMessage(error.response?.data?.message || error.message || tm('models.fetchError'), 'error') | |
| } finally { | |
| loadingModels.value = false | |
| } | |
| } | |
| async function addModelProvider(modelName: string) { | |
| if (!selectedProviderSource.value) return | |
| const sourceId = editableProviderSource.value?.id || selectedProviderSource.value.id | |
| const newId = `${sourceId}/${modelName}` | |
| const metadata = getModelMetadata(modelName) | |
| let modalities: string[] | |
| if (!metadata) { | |
| modalities = ['text', 'image', 'tool_use'] | |
| } else { | |
| modalities = ['text'] | |
| if (supportsImageInput(metadata)) { | |
| modalities.push('image') | |
| } | |
| if (supportsToolCall(metadata)) { | |
| modalities.push('tool_use') | |
| } | |
| } | |
| let max_context_tokens = 0 | |
| if (metadata?.limit?.context && typeof metadata.limit.context === 'number') { | |
| max_context_tokens = metadata.limit.context | |
| } | |
| const newProvider = { | |
| id: newId, | |
| enable: false, | |
| provider_source_id: sourceId, | |
| model: modelName, | |
| modalities, | |
| custom_extra_body: {}, | |
| max_context_tokens: max_context_tokens | |
| } | |
| try { | |
| const res = await axios.post('/api/config/provider/new', newProvider) | |
| if (res.data.status === 'error') { | |
| throw new Error(res.data.message) | |
| } | |
| providers.value.push(newProvider) | |
| showMessage(res.data.message || tm('models.addSuccess', { model: modelName })) | |
| } catch (error: any) { | |
| showMessage(error.response?.data?.message || error.message || tm('providerSources.saveError'), 'error') | |
| } finally { | |
| await loadConfig() | |
| } | |
| } | |
| function modelAlreadyConfigured(modelName: string) { | |
| return existingModelsForSelectedSource.value.has(modelName) | |
| } | |
| async function deleteProvider(provider: any) { | |
| const confirmed = await askForConfirmation(tm('models.deleteConfirm', { id: provider.id })) | |
| if (!confirmed) return | |
| try { | |
| await axios.post('/api/config/provider/delete', { id: provider.id }) | |
| providers.value = providers.value.filter((p) => p.id !== provider.id) | |
| showMessage(tm('models.deleteSuccess')) | |
| } catch (error: any) { | |
| showMessage(error.message || tm('models.deleteError'), 'error') | |
| } finally { | |
| await loadConfig() | |
| } | |
| } | |
| async function testProvider(provider: any) { | |
| testingProviders.value.push(provider.id) | |
| try { | |
| const response = await axios.get('/api/config/provider/check_one', { params: { id: provider.id } }) | |
| if (response.data.status === 'ok' && response.data.data.error === null) { | |
| showMessage(tm('models.testSuccess', { id: provider.id })) | |
| } else { | |
| throw new Error(response.data.data.error || tm('models.testError')) | |
| } | |
| } catch (error: any) { | |
| showMessage(error.response?.data?.message || error.message || tm('models.testError'), 'error') | |
| } finally { | |
| testingProviders.value = testingProviders.value.filter((id) => id !== provider.id) | |
| } | |
| } | |
| async function loadConfig() { | |
| loadProviderTemplate() | |
| } | |
| async function loadProviderTemplate() { | |
| try { | |
| const response = await axios.get('/api/config/provider/template') | |
| if (response.data.status === 'ok') { | |
| configSchema.value = response.data.data.config_schema || {} | |
| if (configSchema.value.provider?.config_template) { | |
| providerTemplates.value = configSchema.value.provider.config_template | |
| } | |
| providerSources.value = response.data.data.provider_sources || [] | |
| providers.value = response.data.data.providers || [] | |
| } | |
| } catch (error) { | |
| console.error('Failed to load provider template:', error) | |
| } | |
| } | |
| function updateDefaultTab(value: string) { | |
| selectedProviderType.value = resolveDefaultTab(value) | |
| } | |
| onMounted(async () => { | |
| await loadProviderTemplate() | |
| }) | |
| return { | |
| // state | |
| config, | |
| metadata, | |
| providerSources, | |
| providers, | |
| selectedProviderType, | |
| selectedProviderSource, | |
| selectedProviderSourceOriginalId, | |
| editableProviderSource, | |
| availableModels, | |
| modelMetadata, | |
| loadingModels, | |
| savingSource, | |
| testingProviders, | |
| isSourceModified, | |
| configSchema, | |
| providerTemplates, | |
| manualModelId, | |
| modelSearch, | |
| // computed | |
| providerTypes, | |
| availableSourceTypes, | |
| displayedProviderSources, | |
| sourceProviders, | |
| mergedModelEntries, | |
| filteredMergedModelEntries, | |
| filteredProviders, | |
| basicSourceConfig, | |
| advancedSourceConfig, | |
| manualProviderId, | |
| providerSourceSchema, | |
| // helpers | |
| resolveSourceIcon, | |
| getSourceDisplayName, | |
| getModelMetadata, | |
| supportsImageInput, | |
| supportsToolCall, | |
| supportsReasoning, | |
| formatContextLimit, | |
| getProviderType, | |
| // methods | |
| updateDefaultTab, | |
| selectProviderSource, | |
| addProviderSource, | |
| deleteProviderSource, | |
| saveProviderSource, | |
| fetchAvailableModels, | |
| addModelProvider, | |
| deleteProvider, | |
| modelAlreadyConfigured, | |
| testProvider, | |
| loadConfig, | |
| loadProviderTemplate | |
| } | |
| } | |