diff --git a/astrbot/dashboard/services/config_service.py b/astrbot/dashboard/services/config_service.py index da6e01ef2..a35dda861 100644 --- a/astrbot/dashboard/services/config_service.py +++ b/astrbot/dashboard/services/config_service.py @@ -24,7 +24,6 @@ from astrbot.core.platform.register import platform_cls_map, platform_registry from astrbot.core.provider.register import provider_registry from astrbot.core.star.star import star_registry from astrbot.core.utils.astrbot_path import get_astrbot_plugin_data_path -from astrbot.core.utils.llm_metadata import LLM_METADATAS from astrbot.core.utils.totp import ( is_totp_enabled, revoke_user_trusted_devices, @@ -1299,14 +1298,18 @@ class ProviderConfigService: if provider.default_config_tmpl: provider_default_tmpl[provider.type] = provider.default_config_tmpl providers = copy.deepcopy(self.config.get("provider", [])) + from astrbot.core.utils.llm_metadata import LLM_METADATAS + + model_metadata = {} for provider in providers: model_id = provider.get("model") if isinstance(model_id, str) and model_id in LLM_METADATAS: - provider["model_metadata"] = LLM_METADATAS[model_id] + model_metadata[model_id] = LLM_METADATAS[model_id] return { "config_schema": config_schema, "providers": providers, "provider_sources": self.config.get("provider_sources", []), + "model_metadata": model_metadata, } def list_provider_sources(self) -> dict: @@ -1549,8 +1552,11 @@ class ProviderConfigService: source_id: str | None = None, enabled: bool | None = None, ) -> dict: + from astrbot.core.utils.llm_metadata import LLM_METADATAS + provider_type = self._resolve_provider_type(capability) providers = [] + model_metadata = {} source_provider_type = { source["id"]: source.get("provider_type", "chat_completion") for source in self.provider_manager.provider_sources_config @@ -1575,9 +1581,9 @@ class ProviderConfigService: provider_response = copy.deepcopy(provider) model_id = provider_response.get("model") if isinstance(model_id, str) and model_id in LLM_METADATAS: - provider_response["model_metadata"] = LLM_METADATAS[model_id] + model_metadata[model_id] = LLM_METADATAS[model_id] providers.append(provider_response) - return {"providers": providers} + return {"providers": providers, "model_metadata": model_metadata} def list_providers_for_dashboard_types( self, provider_type: str | None @@ -1607,10 +1613,14 @@ class ProviderConfigService: ) if provider is None: raise ValueError(f"Provider {provider_id} not found") - model_id = provider.get("model") + provider_response = copy.deepcopy(provider) + from astrbot.core.utils.llm_metadata import LLM_METADATAS + + model_id = provider_response.get("model") + model_metadata = {} if isinstance(model_id, str) and model_id in LLM_METADATAS: - provider["model_metadata"] = LLM_METADATAS[model_id] - return {"provider": provider} + model_metadata[model_id] = LLM_METADATAS[model_id] + return {"provider": provider_response, "model_metadata": model_metadata} async def create_provider(self, config: dict, source_id: str | None = None) -> None: config = copy.deepcopy(config) diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index d10f532c2..37022d332 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -78,6 +78,21 @@ export interface ProviderSchemaData { config_schema?: OpenConfig; providers?: OpenConfig[]; provider_sources?: OpenConfig[]; + model_metadata?: Record; +} + +export interface ProviderListData { + providers?: OpenConfig[]; + model_metadata?: Record; +} + +export interface ProviderByTypeEnvelope extends ApiEnvelope { + model_metadata?: Record; +} + +export interface ProviderByIdData { + provider?: OpenConfig; + model_metadata?: Record; } export interface ProviderSourceModelsData { @@ -496,11 +511,13 @@ export const providerApi = { ); }, list(params?: ProviderListParams) { - return typed<{ providers: OpenConfig[] }>( + return typed( openApiV1.listProviders({ query: generatedQuery(params) }), ); }, - async listByProviderType(providerType: string): Promise>> { + async listByProviderType( + providerType: string, + ): Promise> { const capabilities = providerTypeToCapabilities(providerType); if (capabilities.length === 0) { const response = await providerApi.list(); @@ -509,6 +526,7 @@ export const providerApi = { data: { ...response.data, data: response.data.data.providers || [], + model_metadata: response.data.data.model_metadata || {}, }, }; } @@ -517,11 +535,21 @@ export const providerApi = { capabilities.map((capability) => providerApi.list({ capability })), ); const first = responses[0]; + const modelMetadata = responses.reduce>( + (acc, response) => ({ + ...acc, + ...(response.data.data.model_metadata || {}), + }), + {}, + ); return { ...first, data: { ...first.data, - data: responses.flatMap((response) => response.data.data.providers || []), + data: responses.flatMap( + (response) => response.data.data.providers || [], + ), + model_metadata: modelMetadata, }, }; }, @@ -545,7 +573,7 @@ export const providerApi = { ); }, get(providerId: string, merged = false) { - return typed<{ provider: OpenConfig }>( + return typed( openApiV1.getProviderById({ query: { provider_id: providerId, merged }, }), diff --git a/dashboard/src/components/chat/ProviderModelMenu.vue b/dashboard/src/components/chat/ProviderModelMenu.vue index 0e29bed5d..30d4b8d25 100644 --- a/dashboard/src/components/chat/ProviderModelMenu.vue +++ b/dashboard/src/components/chat/ProviderModelMenu.vue @@ -73,7 +73,7 @@ {{ item.tooltip }} @@ -83,12 +83,15 @@ class="meta-context-badge" @click.stop > - {{ formatContextLimit(provider) }} + {{ formatContextLimit(provider, metadataForProvider(provider)) }} {{ tm("models.metadata.context", { - tokens: formatContextLimit(provider), + tokens: formatContextLimit( + provider, + metadataForProvider(provider), + ), }) }} @@ -141,6 +144,7 @@ import { useToast } from "@/utils/toast"; import { formatContextLimit, providerCapabilityBadges, + type ProviderModelMetadata, type ProviderMetadataSource, } from "@/utils/providerMetadata"; @@ -170,6 +174,7 @@ const menuOpen = ref(false); const loadingProviders = ref(false); const providersLoaded = ref(false); const testingProviderIds = ref([]); +const modelMetadata = ref>({}); const { tm } = useModuleI18n("features/provider"); const { success: toastSuccess, error: toastError } = useToast(); @@ -230,6 +235,9 @@ async function loadProviderConfigs(force = false) { try { const response = await providerApi.listByProviderType("chat_completion"); if (response.data.status === "ok") { + modelMetadata.value = ( + response.data.model_metadata || {} + ) as Record; providerConfigs.value = ( (response.data.data || []) as unknown as ProviderConfig[] ).filter((provider: ProviderConfig) => provider.enable !== false); @@ -255,7 +263,11 @@ function selectProvider(provider: ProviderConfig) { } function capabilityBadges(provider: ProviderConfig) { - return providerCapabilityBadges(provider, tm); + return providerCapabilityBadges(provider, metadataForProvider(provider), tm); +} + +function metadataForProvider(provider: ProviderConfig) { + return provider.model ? modelMetadata.value[provider.model] || null : null; } async function testProvider(provider: ProviderConfig) { diff --git a/dashboard/src/components/chat/RegenerateMenu.vue b/dashboard/src/components/chat/RegenerateMenu.vue index 6d532f654..fd2811be5 100644 --- a/dashboard/src/components/chat/RegenerateMenu.vue +++ b/dashboard/src/components/chat/RegenerateMenu.vue @@ -97,7 +97,7 @@ {{ item.tooltip }} @@ -107,12 +107,15 @@ class="regenerate-model-context-badge" @click.stop > - {{ formatContextLimit(provider) }} + {{ formatContextLimit(provider, metadataForProvider(provider)) }} {{ providerTm("models.metadata.context", { - tokens: formatContextLimit(provider), + tokens: formatContextLimit( + provider, + metadataForProvider(provider), + ), }) }} @@ -137,6 +140,7 @@ import { useModuleI18n } from "@/i18n/composables"; import { formatContextLimit, providerCapabilityBadges, + type ProviderModelMetadata, type ProviderMetadataSource, } from "@/utils/providerMetadata"; @@ -161,6 +165,7 @@ const { tm: providerTm } = useModuleI18n("features/provider"); const providerConfigs = ref([]); const loadingProviders = ref(false); const providersLoaded = ref(false); +const modelMetadata = ref>({}); async function loadProviderConfigs(force = false) { if (loadingProviders.value || (providersLoaded.value && !force)) return; @@ -168,6 +173,9 @@ async function loadProviderConfigs(force = false) { try { const response = await providerApi.listByProviderType("chat_completion"); if (response.data.status === "ok") { + modelMetadata.value = ( + response.data.model_metadata || {} + ) as Record; providerConfigs.value = ( (response.data.data || []) as unknown as ProviderConfig[] ).filter((provider: ProviderConfig) => provider.enable !== false); @@ -200,7 +208,15 @@ function retryWithModel(provider: ProviderConfig) { } function capabilityBadges(provider: ProviderConfig) { - return providerCapabilityBadges(provider, providerTm); + return providerCapabilityBadges( + provider, + metadataForProvider(provider), + providerTm, + ); +} + +function metadataForProvider(provider: ProviderConfig) { + return provider.model ? modelMetadata.value[provider.model] || null : null; } diff --git a/dashboard/src/components/shared/ProviderSelector.vue b/dashboard/src/components/shared/ProviderSelector.vue index 50e3040b8..6ce9864eb 100644 --- a/dashboard/src/components/shared/ProviderSelector.vue +++ b/dashboard/src/components/shared/ProviderSelector.vue @@ -125,7 +125,7 @@ {{ provider.type || provider.provider_type || tm('providerSelector.unknownType') }} - {{ provider.model }} {{ item.tooltip }} @@ -157,12 +157,12 @@ class="provider-selector-meta__context" @click.stop > - {{ formatContextLimit(provider) }} + {{ formatContextLimit(provider, metadataForProvider(provider)) }} {{ providerTm('models.metadata.context', { - tokens: formatContextLimit(provider) + tokens: formatContextLimit(provider, metadataForProvider(provider)) }) }} @@ -280,6 +280,7 @@ const selectedProvider = ref('') const selectedProviders = ref([]) const providerDrawer = ref(false) const testingProviders = ref([]) +const modelMetadata = ref({}) const hasSelection = computed(() => { if (props.multiple) { @@ -329,6 +330,7 @@ async function loadProviders() { try { const response = await providerApi.listByProviderType(props.providerType) if (response.data.status === 'ok') { + modelMetadata.value = response.data.model_metadata || {} const providers = response.data.data || [] providerList.value = props.providerSubtype ? providers.filter((provider) => matchesProviderSubtype(provider, props.providerSubtype)) @@ -398,7 +400,11 @@ function isProviderSelected(providerId) { } function capabilityBadges(provider) { - return providerCapabilityBadges(provider, providerTm) + return providerCapabilityBadges(provider, metadataForProvider(provider), providerTm) +} + +function metadataForProvider(provider) { + return provider?.model ? modelMetadata.value[provider.model] || null : null } function isProviderTesting(providerId) { diff --git a/dashboard/src/composables/useProviderModelConfigDialog.ts b/dashboard/src/composables/useProviderModelConfigDialog.ts index f1ad3caec..c2c49d35c 100644 --- a/dashboard/src/composables/useProviderModelConfigDialog.ts +++ b/dashboard/src/composables/useProviderModelConfigDialog.ts @@ -59,7 +59,6 @@ export function useProviderModelConfigDialog(options: UseProviderModelConfigDial function openProviderEdit(provider: any) { const editableProvider = JSON.parse(JSON.stringify(provider)) - delete editableProvider.model_metadata providerEditData.value = editableProvider providerEditOriginalId.value = provider.id providerEditMode.value = 'edit' @@ -91,7 +90,6 @@ export function useProviderModelConfigDialog(options: UseProviderModelConfigDial savingProviders.value.push(providerEditData.value.id) try { - delete providerEditData.value.model_metadata const isAdding = providerEditMode.value === 'add' const sourceId = providerEditData.value.provider_source_id || selectedProviderSource.value?.id if (isAdding && !sourceId) { diff --git a/dashboard/src/composables/useProviderSources.ts b/dashboard/src/composables/useProviderSources.ts index f24da5e2c..d0616f3b5 100644 --- a/dashboard/src/composables/useProviderSources.ts +++ b/dashboard/src/composables/useProviderSources.ts @@ -151,7 +151,7 @@ export function useProviderSources(options: UseProviderSourcesOptions) { const mergedModelEntries = computed(() => { const configuredEntries = (sourceProviders.value || []).map((provider: any) => { - const metadata = getModelMetadata(provider.model) || provider.model_metadata || null + const metadata = getModelMetadata(provider.model) return { type: 'configured', provider, @@ -691,6 +691,7 @@ export function useProviderSources(options: UseProviderSourcesOptions) { providerTemplates.value = configSchema.value.provider.config_template } providerSources.value = response.data.data.provider_sources || [] + modelMetadata.value = (response.data.data.model_metadata || {}) as Record providers.value = response.data.data.providers || [] } } catch (error) { diff --git a/dashboard/src/utils/providerMetadata.ts b/dashboard/src/utils/providerMetadata.ts index 988c6de47..845cf669b 100644 --- a/dashboard/src/utils/providerMetadata.ts +++ b/dashboard/src/utils/providerMetadata.ts @@ -6,8 +6,8 @@ export interface ProviderModelMetadata { } export interface ProviderMetadataSource { + model?: string modalities?: string[] - model_metadata?: ProviderModelMetadata | null max_context_tokens?: number reasoning?: boolean } @@ -19,8 +19,11 @@ export interface ProviderCapabilityBadge { tooltip: string } -export function formatContextLimit(provider: ProviderMetadataSource | null | undefined): string { - const context = provider?.model_metadata?.limit?.context || provider?.max_context_tokens +export function formatContextLimit( + provider: ProviderMetadataSource | null | undefined, + metadata?: ProviderModelMetadata | null +): string { + const context = metadata?.limit?.context || provider?.max_context_tokens if (!context || typeof context !== 'number') return '' if (context >= 1_000_000) return `${Math.round(context / 1_000_000)}M` if (context >= 1_000) return `${Math.round(context / 1_000)}K` @@ -29,9 +32,10 @@ export function formatContextLimit(provider: ProviderMetadataSource | null | und export function providerCapabilityBadges( provider: ProviderMetadataSource | null | undefined, + metadata: ProviderModelMetadata | null | undefined, tm: (key: string, params?: Record) => string ): ProviderCapabilityBadge[] { - const inputs = provider?.model_metadata?.modalities?.input || [] + const inputs = metadata?.modalities?.input || [] const providerModalities = provider?.modalities const modalities = Array.isArray(providerModalities) ? providerModalities : [] const definitions = [ @@ -52,14 +56,14 @@ export function providerCapabilityBadges( { key: 'tool_use', icon: 'mdi-wrench-outline', - supported: Boolean(provider?.model_metadata?.tool_call), + supported: Boolean(metadata?.tool_call), enabled: modalities.includes('tool_use'), label: tm('models.metadata.toolUse') }, { key: 'reasoning', icon: 'mdi-brain', - supported: Boolean(provider?.model_metadata?.reasoning), + supported: Boolean(metadata?.reasoning), enabled: Boolean(provider?.reasoning), label: tm('models.metadata.reasoning') } @@ -70,9 +74,9 @@ export function providerCapabilityBadges( .map((item) => ({ key: item.key, icon: item.icon, - enabled: !provider?.model_metadata || item.enabled, + enabled: !metadata || item.enabled, tooltip: - provider?.model_metadata && !item.enabled + metadata && !item.enabled ? tm('models.metadata.supportedDisabled', { capability: item.label }) : tm('models.metadata.enabled', { capability: item.label }) }))