From 6627fd53c21b3ded7d8003722ed22eea6f5586e3 Mon Sep 17 00:00:00 2001
From: Weilong Liao <37870767+Soulter@users.noreply.github.com>
Date: Mon, 6 Jul 2026 20:38:30 +0800
Subject: [PATCH] feat: show model metadata in selectors
Surface cached model metadata in model/provider selectors, show neutral capability/context badges, and add provider test actions.
---
astrbot/dashboard/services/config_service.py | 21 +-
.../src/components/chat/ProviderModelMenu.vue | 180 +++++++++++----
.../src/components/chat/RegenerateMenu.vue | 112 ++++++---
.../provider/ProviderModelsPanel.vue | 218 ++++++++++++++----
.../components/shared/ProviderSelector.vue | 142 +++++++++++-
.../useProviderModelConfigDialog.ts | 5 +-
.../src/composables/useProviderSources.ts | 17 +-
.../i18n/locales/en-US/features/provider.json | 11 +
.../i18n/locales/ru-RU/features/provider.json | 12 +
.../i18n/locales/zh-CN/features/provider.json | 11 +
dashboard/src/utils/providerMetadata.ts | 79 +++++++
11 files changed, 665 insertions(+), 143 deletions(-)
create mode 100644 dashboard/src/utils/providerMetadata.ts
diff --git a/astrbot/dashboard/services/config_service.py b/astrbot/dashboard/services/config_service.py
index f7c06ce25..da6e01ef2 100644
--- a/astrbot/dashboard/services/config_service.py
+++ b/astrbot/dashboard/services/config_service.py
@@ -24,6 +24,7 @@ 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,
@@ -1297,9 +1298,14 @@ class ProviderConfigService:
for provider in provider_registry:
if provider.default_config_tmpl:
provider_default_tmpl[provider.type] = provider.default_config_tmpl
+ providers = copy.deepcopy(self.config.get("provider", []))
+ 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]
return {
"config_schema": config_schema,
- "providers": self.config.get("provider", []),
+ "providers": providers,
"provider_sources": self.config.get("provider_sources", []),
}
@@ -1562,11 +1568,15 @@ class ProviderConfigService:
if provider_type and effective_type != provider_type:
continue
if provider.get("provider_source_id"):
- providers.append(
- self.provider_manager.get_merged_provider_config(provider)
+ provider_response = self.provider_manager.get_merged_provider_config(
+ provider
)
else:
- providers.append(copy.deepcopy(provider))
+ 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]
+ providers.append(provider_response)
return {"providers": providers}
def list_providers_for_dashboard_types(
@@ -1597,6 +1607,9 @@ class ProviderConfigService:
)
if provider is None:
raise ValueError(f"Provider {provider_id} not found")
+ model_id = provider.get("model")
+ if isinstance(model_id, str) and model_id in LLM_METADATAS:
+ provider["model_metadata"] = LLM_METADATAS[model_id]
return {"provider": provider}
async def create_provider(self, config: dict, source_id: str | None = None) -> None:
diff --git a/dashboard/src/components/chat/ProviderModelMenu.vue b/dashboard/src/components/chat/ProviderModelMenu.vue
index 956155aea..0e29bed5d 100644
--- a/dashboard/src/components/chat/ProviderModelMenu.vue
+++ b/dashboard/src/components/chat/ProviderModelMenu.vue
@@ -54,28 +54,70 @@
{{ provider.model }}
-
- mdi-eye-outline
-
-
- mdi-music-note-outline
-
-
- mdi-wrench
-
-
- mdi-brain
-
+
+
+
+ {{ item.icon }}
+
+
+ {{ item.tooltip }}
+
+
+
+
+ {{ formatContextLimit(provider) }}
+
+
+ {{
+ tm("models.metadata.context", {
+ tokens: formatContextLimit(provider),
+ })
+ }}
+
-
- mdi-check
-
+
@@ -94,18 +136,18 @@
@@ -226,6 +241,29 @@ function supportsReasoning(provider: ProviderConfig): boolean {
color: var(--chat-muted, rgba(var(--v-theme-on-surface), 0.62));
}
+.regenerate-model-icon-badge {
+ display: inline-flex;
+ align-items: center;
+ color: rgba(var(--v-theme-on-surface), 0.72);
+}
+
+.regenerate-model-icon-badge--disabled {
+ color: rgba(var(--v-theme-on-surface), 0.34);
+}
+
+.regenerate-model-context-badge {
+ display: inline-flex;
+ align-items: center;
+ height: 16px;
+ padding: 0 5px;
+ border-radius: 999px;
+ background: rgba(var(--v-theme-on-surface), 0.06);
+ color: rgba(var(--v-theme-on-surface), 0.72);
+ font-size: 10px;
+ font-weight: 650;
+ line-height: 16px;
+}
+
.regenerate-empty {
padding: 14px 16px;
color: var(--chat-muted, rgba(var(--v-theme-on-surface), 0.62));
diff --git a/dashboard/src/components/provider/ProviderModelsPanel.vue b/dashboard/src/components/provider/ProviderModelsPanel.vue
index 5816962ef..0420e93bd 100644
--- a/dashboard/src/components/provider/ProviderModelsPanel.vue
+++ b/dashboard/src/components/provider/ProviderModelsPanel.vue
@@ -66,19 +66,47 @@
{{ entry.provider.id }}
{{ entry.provider.model }}
-
- {{ item.icon }}
-
-
+
+ {{ item.icon }}
+
+
+ {{ item.tooltip }}
+
+
- {{ formatContextLimit(entry.metadata) }}
-
+
+
+ {{ formatContextLimit(entry.metadata) }}
+
+
+ {{
+ tm('models.metadata.context', {
+ tokens: formatContextLimit(entry.metadata)
+ })
+ }}
+
@@ -94,14 +122,20 @@
@update:modelValue="emit('toggle-provider-enable', entry.provider, $event)"
>
-
+
+
+
+
+ {{ tm('models.testButton') }}
+
- {{ tm('models.tooltips.providerId') }}: {{ entry.provider.id }}
- {{ tm('models.tooltips.modelId') }}: {{ entry.provider.model }}
+
+ {{ tm('models.tooltips.providerId') }}:
+ {{ entry.provider.id }}
+
+
+ {{ tm('models.tooltips.modelId') }}:
+ {{ entry.provider.model }}
+
@@ -152,19 +192,47 @@
>
{{ entry.model }}
-
- {{ item.icon }}
-
-
+
+ {{ item.icon }}
+
+
+ {{ item.tooltip }}
+
+
- {{ formatContextLimit(entry.metadata) }}
-
+
+
+ {{ formatContextLimit(entry.metadata) }}
+
+
+ {{
+ tm('models.metadata.context', {
+ tokens: formatContextLimit(entry.metadata)
+ })
+ }}
+
@@ -179,7 +247,10 @@
- {{ tm('models.tooltips.modelId') }}: {{ entry.model }}
+
+ {{ tm('models.tooltips.modelId') }}:
+ {{ entry.model }}
+
@@ -275,21 +346,64 @@ const availableEntries = computed(() =>
(props.entries || []).filter((entry) => entry.type === 'available')
)
-const capabilityIcons = (metadata) => {
- const icons = []
- if (props.supportsImageInput(metadata)) {
- icons.push({ icon: 'mdi-image-outline' })
- }
- if (props.supportsAudioInput(metadata)) {
- icons.push({ icon: 'mdi-music-note-outline' })
- }
- if (props.supportsToolCall(metadata)) {
- icons.push({ icon: 'mdi-wrench-outline' })
- }
- if (props.supportsReasoning(metadata)) {
- icons.push({ icon: 'mdi-brain' })
- }
- return icons
+const capabilityBadges = (entry) => {
+ const metadata = entry?.metadata
+ const provider = entry?.provider
+ const modalities = Array.isArray(provider?.modalities) ? provider.modalities : []
+ const isConfigured = entry?.type === 'configured'
+ const hasModelMetadata = Boolean(entry?.hasModelMetadata)
+ const definitions = [
+ {
+ key: 'image',
+ icon: 'mdi-image-outline',
+ supported: props.supportsImageInput(metadata),
+ enabled: !isConfigured || modalities.includes('image'),
+ label: props.tm('models.metadata.image')
+ },
+ {
+ key: 'audio',
+ icon: 'mdi-music-note-outline',
+ supported: props.supportsAudioInput(metadata),
+ enabled: !isConfigured || modalities.includes('audio'),
+ label: props.tm('models.metadata.audio')
+ },
+ {
+ key: 'tool_use',
+ icon: 'mdi-wrench-outline',
+ supported: props.supportsToolCall(metadata),
+ enabled: !isConfigured || modalities.includes('tool_use'),
+ label: props.tm('models.metadata.toolUse')
+ },
+ {
+ key: 'reasoning',
+ icon: 'mdi-brain',
+ supported: props.supportsReasoning(metadata),
+ enabled: !isConfigured || Boolean(provider?.reasoning),
+ label: props.tm('models.metadata.reasoning')
+ }
+ ]
+
+ return definitions
+ .filter((item) => item.supported || (isConfigured && item.enabled))
+ .map((item) => {
+ const enabled = !isConfigured || !hasModelMetadata || item.enabled
+ let tooltip = props.tm('models.metadata.available', {
+ capability: item.label
+ })
+ if (isConfigured) {
+ tooltip = enabled
+ ? props.tm('models.metadata.enabled', { capability: item.label })
+ : props.tm('models.metadata.supportedDisabled', {
+ capability: item.label
+ })
+ }
+ return {
+ key: item.key,
+ icon: item.icon,
+ enabled,
+ tooltip
+ }
+ })
}
const isProviderTesting = (providerId) => props.testingProviders.includes(providerId)
@@ -445,13 +559,21 @@ const isProviderSaving = (providerId) => props.savingProviders.includes(provider
width: 24px;
height: 24px;
border-radius: 999px;
- background: rgba(var(--v-theme-on-surface), 0.04);
- color: rgba(var(--v-theme-on-surface), 0.58);
display: inline-flex;
align-items: center;
justify-content: center;
}
+.provider-model-row__badge--enabled {
+ background: rgba(var(--v-theme-on-surface), 0.06);
+ color: rgba(var(--v-theme-on-surface), 0.72);
+}
+
+.provider-model-row__badge--disabled {
+ background: rgba(var(--v-theme-on-surface), 0.04);
+ color: rgba(var(--v-theme-on-surface), 0.34);
+}
+
.provider-model-row__badge--text {
width: auto;
padding: 0 8px;
diff --git a/dashboard/src/components/shared/ProviderSelector.vue b/dashboard/src/components/shared/ProviderSelector.vue
index 114bfd9e3..50e3040b8 100644
--- a/dashboard/src/components/shared/ProviderSelector.vue
+++ b/dashboard/src/components/shared/ProviderSelector.vue
@@ -106,7 +106,7 @@
{{ tm('providerSelector.clearSelectionSubtitle') }}
- mdi-check-circle
+ mdi-check-circle
@@ -124,10 +124,69 @@
{{ provider.type || provider.provider_type || tm('providerSelector.unknownType') }}
- {{ provider.model }}
+
+
+
+
+ {{ item.icon }}
+
+
+ {{ item.tooltip }}
+
+
+
+
+ {{ formatContextLimit(provider) }}
+
+
+ {{
+ providerTm('models.metadata.context', {
+ tokens: formatContextLimit(provider)
+ })
+ }}
+
+
- mdi-check-circle
+
+
+
+
+
+ {{ providerTm('models.testButton') }}
+
+ mdi-check-circle
+
@@ -181,6 +240,8 @@
import { computed, ref, watch } from 'vue'
import { providerApi } from '@/api/v1'
import { useModuleI18n } from '@/i18n/composables'
+import { useToast } from '@/utils/toast'
+import { formatContextLimit, providerCapabilityBadges } from '@/utils/providerMetadata'
import ProviderChatCompletionPanel from '@/components/provider/ProviderChatCompletionPanel.vue'
import ProviderPage from '@/views/ProviderPage.vue'
@@ -209,6 +270,8 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const { tm } = useModuleI18n('core.shared')
+const { tm: providerTm } = useModuleI18n('features/provider')
+const { success: toastSuccess, error: toastError } = useToast()
const dialog = ref(false)
const providerList = ref([])
@@ -216,6 +279,7 @@ const loading = ref(false)
const selectedProvider = ref('')
const selectedProviders = ref([])
const providerDrawer = ref(false)
+const testingProviders = ref([])
const hasSelection = computed(() => {
if (props.multiple) {
@@ -333,6 +397,42 @@ function isProviderSelected(providerId) {
return selectedProvider.value === providerId
}
+function capabilityBadges(provider) {
+ return providerCapabilityBadges(provider, providerTm)
+}
+
+function isProviderTesting(providerId) {
+ return testingProviders.value.includes(providerId)
+}
+
+function isProviderTestable(provider) {
+ return Boolean(provider?.id) && provider.enable !== false && !isProviderTesting(provider.id)
+}
+
+async function testProvider(provider) {
+ if (!isProviderTestable(provider)) {
+ return
+ }
+ testingProviders.value.push(provider.id)
+ try {
+ const startTime = performance.now()
+ const response = await providerApi.test(String(provider.id))
+ if (response.data.status === 'ok' && response.data.data.error === null) {
+ const latency = Math.max(0, Math.round(performance.now() - startTime))
+ toastSuccess(providerTm('models.testSuccessWithLatency', {
+ id: provider.id,
+ latency
+ }))
+ } else {
+ throw new Error(response.data.data.error || providerTm('models.testError'))
+ }
+ } catch (error) {
+ toastError(error.response?.data?.message || error.message || providerTm('models.testError'))
+ } finally {
+ testingProviders.value = testingProviders.value.filter((id) => id !== provider.id)
+ }
+}
+
function removeSelected(providerId) {
const idx = selectedProviders.value.indexOf(providerId)
if (idx >= 0) {
@@ -384,6 +484,44 @@ function closeProviderDrawer() {
border-radius: 10px;
}
+.provider-selector-meta {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ margin-left: 8px;
+ vertical-align: middle;
+}
+
+.provider-selector-meta__badge {
+ display: inline-flex;
+ align-items: center;
+ color: rgba(var(--v-theme-on-surface), 0.72);
+}
+
+.provider-selector-meta__badge--disabled {
+ color: rgba(var(--v-theme-on-surface), 0.34);
+}
+
+.provider-selector-meta__context {
+ display: inline-flex;
+ align-items: center;
+ height: 16px;
+ padding: 0 5px;
+ border-radius: 999px;
+ background: rgba(var(--v-theme-on-surface), 0.06);
+ color: rgba(var(--v-theme-on-surface), 0.72);
+ font-size: 10px;
+ font-weight: 650;
+ line-height: 16px;
+}
+
+.provider-selector-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ color: rgba(var(--v-theme-on-surface), 0.72);
+}
+
.v-list-item {
transition: all 0.2s ease;
}
diff --git a/dashboard/src/composables/useProviderModelConfigDialog.ts b/dashboard/src/composables/useProviderModelConfigDialog.ts
index ef07917d1..f1ad3caec 100644
--- a/dashboard/src/composables/useProviderModelConfigDialog.ts
+++ b/dashboard/src/composables/useProviderModelConfigDialog.ts
@@ -58,7 +58,9 @@ export function useProviderModelConfigDialog(options: UseProviderModelConfigDial
})
function openProviderEdit(provider: any) {
- providerEditData.value = JSON.parse(JSON.stringify(provider))
+ const editableProvider = JSON.parse(JSON.stringify(provider))
+ delete editableProvider.model_metadata
+ providerEditData.value = editableProvider
providerEditOriginalId.value = provider.id
providerEditMode.value = 'edit'
showProviderEditDialog.value = true
@@ -89,6 +91,7 @@ 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 b97c167e9..f24da5e2c 100644
--- a/dashboard/src/composables/useProviderSources.ts
+++ b/dashboard/src/composables/useProviderSources.ts
@@ -150,11 +150,15 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
}
const mergedModelEntries = computed(() => {
- const configuredEntries = (sourceProviders.value || []).map((provider: any) => ({
- type: 'configured',
- provider,
- metadata: getModelMetadata(provider.model) || buildMetadataFromProvider(provider)
- }))
+ const configuredEntries = (sourceProviders.value || []).map((provider: any) => {
+ const metadata = getModelMetadata(provider.model) || provider.model_metadata || null
+ return {
+ type: 'configured',
+ provider,
+ metadata: metadata || buildMetadataFromProvider(provider),
+ hasModelMetadata: Boolean(metadata)
+ }
+ })
const availableEntries = (sortedAvailableModels.value || [])
.filter((item: any) => {
@@ -166,7 +170,8 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
return {
type: 'available',
model: name,
- metadata: typeof item === 'object' ? item?.metadata : getModelMetadata(name)
+ metadata: typeof item === 'object' ? item?.metadata : getModelMetadata(name),
+ hasModelMetadata: Boolean(typeof item === 'object' ? item?.metadata : getModelMetadata(name))
}
})
diff --git a/dashboard/src/i18n/locales/en-US/features/provider.json b/dashboard/src/i18n/locales/en-US/features/provider.json
index 11832bfd8..4dd1a1fe6 100644
--- a/dashboard/src/i18n/locales/en-US/features/provider.json
+++ b/dashboard/src/i18n/locales/en-US/features/provider.json
@@ -146,6 +146,17 @@
"manualModelRequired": "Please enter a model ID",
"manualModelExists": "Model already exists",
"configure": "Configure",
+ "testButton": "Test model",
+ "metadata": {
+ "image": "Image input",
+ "audio": "Audio input",
+ "toolUse": "Tool use",
+ "reasoning": "Reasoning",
+ "context": "{tokens} context window",
+ "available": "Model metadata supports {capability}",
+ "enabled": "{capability} enabled",
+ "supportedDisabled": "Model metadata supports {capability}, but it is not enabled in this provider's modalities."
+ },
"tooltips": {
"providerId": "Provider ID",
"modelId": "Model ID"
diff --git a/dashboard/src/i18n/locales/ru-RU/features/provider.json b/dashboard/src/i18n/locales/ru-RU/features/provider.json
index 0ce704aea..7c597df4f 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/provider.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/provider.json
@@ -136,6 +136,7 @@
"deleteSuccess": "Модель удалена",
"deleteError": "Ошибка удаления модели",
"testSuccess": "Тест модели «{id}» пройден успешно",
+ "testSuccessWithLatency": "Тест модели «{id}» пройден успешно, задержка {latency} мс",
"testError": "Тест модели провален",
"searchPlaceholder": "Поиск по имени или ID",
"manualAddButton": "Добавить вручную",
@@ -146,6 +147,17 @@
"manualModelRequired": "Укажите ID модели",
"manualModelExists": "Эта модель уже добавлена",
"configure": "Настроить",
+ "testButton": "Тестировать модель",
+ "metadata": {
+ "image": "Ввод изображений",
+ "audio": "Ввод аудио",
+ "toolUse": "Использование инструментов",
+ "reasoning": "Рассуждение",
+ "context": "Контекстное окно {tokens}",
+ "available": "Metadata модели поддерживает {capability}",
+ "enabled": "{capability} включено",
+ "supportedDisabled": "Metadata модели поддерживает {capability}, но эта возможность не включена в modalities текущего провайдера."
+ },
"tooltips": {
"providerId": "ID провайдера",
"modelId": "ID модели"
diff --git a/dashboard/src/i18n/locales/zh-CN/features/provider.json b/dashboard/src/i18n/locales/zh-CN/features/provider.json
index d7bd1c631..63809fbc4 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/provider.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/provider.json
@@ -147,6 +147,17 @@
"manualModelRequired": "请输入模型 ID",
"manualModelExists": "该模型已存在",
"configure": "配置",
+ "testButton": "测试模型",
+ "metadata": {
+ "image": "图像输入",
+ "audio": "音频输入",
+ "toolUse": "工具使用",
+ "reasoning": "推理",
+ "context": "{tokens} 上下文窗口",
+ "available": "模型 metadata 支持{capability}",
+ "enabled": "已启用{capability}",
+ "supportedDisabled": "模型 metadata 支持{capability},但当前提供商的模型能力未启用。"
+ },
"tooltips": {
"providerId": "提供商 ID",
"modelId": "模型 ID"
diff --git a/dashboard/src/utils/providerMetadata.ts b/dashboard/src/utils/providerMetadata.ts
new file mode 100644
index 000000000..988c6de47
--- /dev/null
+++ b/dashboard/src/utils/providerMetadata.ts
@@ -0,0 +1,79 @@
+export interface ProviderModelMetadata {
+ modalities?: { input?: string[] }
+ tool_call?: boolean
+ reasoning?: boolean
+ limit?: { context?: number }
+}
+
+export interface ProviderMetadataSource {
+ modalities?: string[]
+ model_metadata?: ProviderModelMetadata | null
+ max_context_tokens?: number
+ reasoning?: boolean
+}
+
+export interface ProviderCapabilityBadge {
+ key: string
+ icon: string
+ enabled: boolean
+ tooltip: string
+}
+
+export function formatContextLimit(provider: ProviderMetadataSource | null | undefined): string {
+ const context = provider?.model_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`
+ return `${context}`
+}
+
+export function providerCapabilityBadges(
+ provider: ProviderMetadataSource | null | undefined,
+ tm: (key: string, params?: Record) => string
+): ProviderCapabilityBadge[] {
+ const inputs = provider?.model_metadata?.modalities?.input || []
+ const providerModalities = provider?.modalities
+ const modalities = Array.isArray(providerModalities) ? providerModalities : []
+ const definitions = [
+ {
+ key: 'image',
+ icon: 'mdi-image-outline',
+ supported: inputs.includes('image'),
+ enabled: modalities.includes('image'),
+ label: tm('models.metadata.image')
+ },
+ {
+ key: 'audio',
+ icon: 'mdi-music-note-outline',
+ supported: inputs.includes('audio'),
+ enabled: modalities.includes('audio'),
+ label: tm('models.metadata.audio')
+ },
+ {
+ key: 'tool_use',
+ icon: 'mdi-wrench-outline',
+ supported: Boolean(provider?.model_metadata?.tool_call),
+ enabled: modalities.includes('tool_use'),
+ label: tm('models.metadata.toolUse')
+ },
+ {
+ key: 'reasoning',
+ icon: 'mdi-brain',
+ supported: Boolean(provider?.model_metadata?.reasoning),
+ enabled: Boolean(provider?.reasoning),
+ label: tm('models.metadata.reasoning')
+ }
+ ]
+
+ return definitions
+ .filter((item) => item.supported || item.enabled)
+ .map((item) => ({
+ key: item.key,
+ icon: item.icon,
+ enabled: !provider?.model_metadata || item.enabled,
+ tooltip:
+ provider?.model_metadata && !item.enabled
+ ? tm('models.metadata.supportedDisabled', { capability: item.label })
+ : tm('models.metadata.enabled', { capability: item.label })
+ }))
+}