Compare commits

...

1 Commits

Author SHA1 Message Date
Soulter
d455e37f92 fix: keep model metadata separate from providers 2026-07-06 21:07:32 +08:00
8 changed files with 110 additions and 35 deletions

View File

@@ -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)

View File

@@ -78,6 +78,21 @@ export interface ProviderSchemaData {
config_schema?: OpenConfig;
providers?: OpenConfig[];
provider_sources?: OpenConfig[];
model_metadata?: Record<string, unknown>;
}
export interface ProviderListData {
providers?: OpenConfig[];
model_metadata?: Record<string, unknown>;
}
export interface ProviderByTypeEnvelope extends ApiEnvelope<OpenConfig[]> {
model_metadata?: Record<string, unknown>;
}
export interface ProviderByIdData {
provider?: OpenConfig;
model_metadata?: Record<string, unknown>;
}
export interface ProviderSourceModelsData {
@@ -496,11 +511,13 @@ export const providerApi = {
);
},
list(params?: ProviderListParams) {
return typed<{ providers: OpenConfig[] }>(
return typed<ProviderListData>(
openApiV1.listProviders({ query: generatedQuery(params) }),
);
},
async listByProviderType(providerType: string): Promise<AxiosResponse<ApiEnvelope<OpenConfig[]>>> {
async listByProviderType(
providerType: string,
): Promise<AxiosResponse<ProviderByTypeEnvelope>> {
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<Record<string, unknown>>(
(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<ProviderByIdData>(
openApiV1.getProviderById({
query: { provider_id: providerId, merged },
}),

View File

@@ -73,7 +73,7 @@
<span>{{ item.tooltip }}</span>
</v-tooltip>
<v-tooltip
v-if="formatContextLimit(provider)"
v-if="formatContextLimit(provider, metadataForProvider(provider))"
location="top"
max-width="320"
>
@@ -83,12 +83,15 @@
class="meta-context-badge"
@click.stop
>
{{ formatContextLimit(provider) }}
{{ formatContextLimit(provider, metadataForProvider(provider)) }}
</span>
</template>
<span>{{
tm("models.metadata.context", {
tokens: formatContextLimit(provider),
tokens: formatContextLimit(
provider,
metadataForProvider(provider),
),
})
}}</span>
</v-tooltip>
@@ -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<string[]>([]);
const modelMetadata = ref<Record<string, ProviderModelMetadata>>({});
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<string, ProviderModelMetadata>;
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) {

View File

@@ -97,7 +97,7 @@
<span>{{ item.tooltip }}</span>
</v-tooltip>
<v-tooltip
v-if="formatContextLimit(provider)"
v-if="formatContextLimit(provider, metadataForProvider(provider))"
location="top"
max-width="320"
>
@@ -107,12 +107,15 @@
class="regenerate-model-context-badge"
@click.stop
>
{{ formatContextLimit(provider) }}
{{ formatContextLimit(provider, metadataForProvider(provider)) }}
</span>
</template>
<span>{{
providerTm("models.metadata.context", {
tokens: formatContextLimit(provider),
tokens: formatContextLimit(
provider,
metadataForProvider(provider),
),
})
}}</span>
</v-tooltip>
@@ -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<ProviderConfig[]>([]);
const loadingProviders = ref(false);
const providersLoaded = ref(false);
const modelMetadata = ref<Record<string, ProviderModelMetadata>>({});
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<string, ProviderModelMetadata>;
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;
}
</script>

View File

@@ -125,7 +125,7 @@
{{ provider.type || provider.provider_type || tm('providerSelector.unknownType') }}
<span v-if="provider.model">- {{ provider.model }}</span>
<span
v-if="capabilityBadges(provider).length || formatContextLimit(provider)"
v-if="capabilityBadges(provider).length || formatContextLimit(provider, metadataForProvider(provider))"
class="provider-selector-meta"
>
<v-tooltip
@@ -147,7 +147,7 @@
<span>{{ item.tooltip }}</span>
</v-tooltip>
<v-tooltip
v-if="formatContextLimit(provider)"
v-if="formatContextLimit(provider, metadataForProvider(provider))"
location="top"
max-width="320"
>
@@ -157,12 +157,12 @@
class="provider-selector-meta__context"
@click.stop
>
{{ formatContextLimit(provider) }}
{{ formatContextLimit(provider, metadataForProvider(provider)) }}
</span>
</template>
<span>{{
providerTm('models.metadata.context', {
tokens: formatContextLimit(provider)
tokens: formatContextLimit(provider, metadataForProvider(provider))
})
}}</span>
</v-tooltip>
@@ -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) {

View File

@@ -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) {

View File

@@ -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<string, any>
providers.value = response.data.data.providers || []
}
} catch (error) {

View File

@@ -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, string>) => 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 })
}))