mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-09 06:20:12 +08:00
Compare commits
4 Commits
fix/8929
...
codex/prov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d455e37f92 | ||
|
|
6627fd53c2 | ||
|
|
25cbd41e08 | ||
|
|
3b8caf37e6 |
@@ -145,6 +145,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
REPEATED_TOOL_NOTICE_L1_THRESHOLD = 3
|
||||
REPEATED_TOOL_NOTICE_L2_THRESHOLD = 4
|
||||
REPEATED_TOOL_NOTICE_L3_THRESHOLD = 5
|
||||
MALFORMED_TOOL_NAME_PLACEHOLDER = "__malformed_tool_name__"
|
||||
REPEATED_TOOL_NOTICE_L1_TEMPLATE = (
|
||||
"\n\n[SYSTEM NOTICE] By the way, you have executed the same tool "
|
||||
"`{tool_name}` {streak} times consecutively. Double-check whether another "
|
||||
@@ -532,6 +533,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
)
|
||||
break
|
||||
|
||||
self._sanitize_malformed_tool_calls(resp)
|
||||
yield resp
|
||||
return
|
||||
|
||||
@@ -689,6 +691,22 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
streak=streak,
|
||||
)
|
||||
|
||||
def _sanitize_malformed_tool_calls(
|
||||
self,
|
||||
llm_resp: LLMResponse,
|
||||
) -> None:
|
||||
"""Normalize malformed tool call names.
|
||||
|
||||
Args:
|
||||
llm_resp: The LLM response whose tool call lists should be sanitized.
|
||||
"""
|
||||
llm_resp.tools_call_name = [
|
||||
self.MALFORMED_TOOL_NAME_PLACEHOLDER
|
||||
if tool_name is None or tool_name.strip() == ""
|
||||
else tool_name
|
||||
for tool_name in llm_resp.tools_call_name
|
||||
]
|
||||
|
||||
@override
|
||||
async def step(self):
|
||||
"""Process a single step of the agent.
|
||||
@@ -1312,6 +1330,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
)
|
||||
if requery_resp:
|
||||
llm_resp = requery_resp
|
||||
self._sanitize_malformed_tool_calls(llm_resp)
|
||||
|
||||
# If the re-query still returns no tool calls, and also does not have a meaningful assistant reply,
|
||||
# we consider it as a failure of the LLM to follow the tool-use instruction,
|
||||
@@ -1339,6 +1358,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
)
|
||||
if repair_resp:
|
||||
llm_resp = repair_resp
|
||||
self._sanitize_malformed_tool_calls(llm_resp)
|
||||
|
||||
return llm_resp, subset
|
||||
|
||||
|
||||
@@ -1297,10 +1297,19 @@ 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", []))
|
||||
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:
|
||||
model_metadata[model_id] = LLM_METADATAS[model_id]
|
||||
return {
|
||||
"config_schema": config_schema,
|
||||
"providers": self.config.get("provider", []),
|
||||
"providers": providers,
|
||||
"provider_sources": self.config.get("provider_sources", []),
|
||||
"model_metadata": model_metadata,
|
||||
}
|
||||
|
||||
def list_provider_sources(self) -> dict:
|
||||
@@ -1543,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
|
||||
@@ -1562,12 +1574,16 @@ 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))
|
||||
return {"providers": providers}
|
||||
provider_response = copy.deepcopy(provider)
|
||||
model_id = provider_response.get("model")
|
||||
if isinstance(model_id, str) and model_id in LLM_METADATAS:
|
||||
model_metadata[model_id] = LLM_METADATAS[model_id]
|
||||
providers.append(provider_response)
|
||||
return {"providers": providers, "model_metadata": model_metadata}
|
||||
|
||||
def list_providers_for_dashboard_types(
|
||||
self, provider_type: str | None
|
||||
@@ -1597,7 +1613,14 @@ class ProviderConfigService:
|
||||
)
|
||||
if provider is None:
|
||||
raise ValueError(f"Provider {provider_id} not found")
|
||||
return {"provider": provider}
|
||||
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:
|
||||
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)
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
|
||||
@@ -54,28 +54,73 @@
|
||||
<v-list-item-subtitle class="provider-subtitle">
|
||||
<span class="model-name">{{ provider.model }}</span>
|
||||
<span class="meta-icons">
|
||||
<v-icon v-if="supportsImageInput(provider)" size="13">
|
||||
mdi-eye-outline
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsAudioInput(provider)" size="13">
|
||||
mdi-music-note-outline
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsToolCall(provider)" size="13">
|
||||
mdi-wrench
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsReasoning(provider)" size="13">
|
||||
mdi-brain
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
v-for="item in capabilityBadges(provider)"
|
||||
:key="item.key"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
<template #activator="{ props: badgeTooltipProps }">
|
||||
<span
|
||||
v-bind="badgeTooltipProps"
|
||||
class="meta-icon-badge"
|
||||
:class="{ 'meta-icon-badge--disabled': !item.enabled }"
|
||||
@click.stop
|
||||
>
|
||||
<v-icon size="13">{{ item.icon }}</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ item.tooltip }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
v-if="formatContextLimit(provider, metadataForProvider(provider))"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
<template #activator="{ props: contextTooltipProps }">
|
||||
<span
|
||||
v-bind="contextTooltipProps"
|
||||
class="meta-context-badge"
|
||||
@click.stop
|
||||
>
|
||||
{{ formatContextLimit(provider, metadataForProvider(provider)) }}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{
|
||||
tm("models.metadata.context", {
|
||||
tokens: formatContextLimit(
|
||||
provider,
|
||||
metadataForProvider(provider),
|
||||
),
|
||||
})
|
||||
}}</span>
|
||||
</v-tooltip>
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-if="selectedProviderId === provider.id"
|
||||
class="provider-selected-icon"
|
||||
size="18"
|
||||
>
|
||||
mdi-check
|
||||
</v-icon>
|
||||
<div class="provider-menu-actions" @click.stop>
|
||||
<v-tooltip location="top">
|
||||
<template #activator="{ props: testTooltipProps }">
|
||||
<v-btn
|
||||
v-bind="testTooltipProps"
|
||||
icon="mdi-connection"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
:loading="testingProviderIds.includes(provider.id)"
|
||||
:disabled="testingProviderIds.includes(provider.id)"
|
||||
@click.stop="testProvider(provider)"
|
||||
/>
|
||||
</template>
|
||||
<span>{{ tm("models.testButton") }}</span>
|
||||
</v-tooltip>
|
||||
<v-icon
|
||||
v-if="selectedProviderId === provider.id"
|
||||
class="provider-selected-icon"
|
||||
size="18"
|
||||
>
|
||||
mdi-check
|
||||
</v-icon>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -94,18 +139,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { providerApi } from "@/api/v1";
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import { useToast } from "@/utils/toast";
|
||||
import {
|
||||
formatContextLimit,
|
||||
providerCapabilityBadges,
|
||||
type ProviderModelMetadata,
|
||||
type ProviderMetadataSource,
|
||||
} from "@/utils/providerMetadata";
|
||||
|
||||
interface ModelMetadata {
|
||||
modalities?: { input?: string[] };
|
||||
tool_call?: boolean;
|
||||
reasoning?: boolean;
|
||||
}
|
||||
|
||||
interface ProviderConfig {
|
||||
interface ProviderConfig extends ProviderMetadataSource {
|
||||
id: string;
|
||||
model: string;
|
||||
api_base?: string;
|
||||
model_metadata?: ModelMetadata;
|
||||
enable?: boolean;
|
||||
}
|
||||
|
||||
@@ -127,6 +173,10 @@ const searchQuery = ref("");
|
||||
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();
|
||||
|
||||
const variant = computed(() => props.variant);
|
||||
const menuLocation = computed(() =>
|
||||
@@ -134,7 +184,9 @@ const menuLocation = computed(() =>
|
||||
);
|
||||
|
||||
const selectedProvider = computed(() =>
|
||||
providerConfigs.value.find((provider) => provider.id === selectedProviderId.value),
|
||||
providerConfigs.value.find(
|
||||
(provider) => provider.id === selectedProviderId.value,
|
||||
),
|
||||
);
|
||||
|
||||
const triggerTitle = computed(() => {
|
||||
@@ -183,9 +235,12 @@ async function loadProviderConfigs(force = false) {
|
||||
try {
|
||||
const response = await providerApi.listByProviderType("chat_completion");
|
||||
if (response.data.status === "ok") {
|
||||
providerConfigs.value = ((response.data.data || []) as unknown as ProviderConfig[]).filter(
|
||||
(provider: ProviderConfig) => provider.enable !== false,
|
||||
);
|
||||
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);
|
||||
providersLoaded.value = true;
|
||||
const selected = selectedProvider.value;
|
||||
if (selected) {
|
||||
@@ -207,22 +262,40 @@ function selectProvider(provider: ProviderConfig) {
|
||||
menuOpen.value = false;
|
||||
}
|
||||
|
||||
function supportsImageInput(provider: ProviderConfig): boolean {
|
||||
const inputs = provider.model_metadata?.modalities?.input || [];
|
||||
return inputs.includes("image");
|
||||
function capabilityBadges(provider: ProviderConfig) {
|
||||
return providerCapabilityBadges(provider, metadataForProvider(provider), tm);
|
||||
}
|
||||
|
||||
function supportsAudioInput(provider: ProviderConfig): boolean {
|
||||
const inputs = provider.model_metadata?.modalities?.input || [];
|
||||
return inputs.includes("audio");
|
||||
function metadataForProvider(provider: ProviderConfig) {
|
||||
return provider.model ? modelMetadata.value[provider.model] || null : null;
|
||||
}
|
||||
|
||||
function supportsToolCall(provider: ProviderConfig): boolean {
|
||||
return Boolean(provider.model_metadata?.tool_call);
|
||||
}
|
||||
|
||||
function supportsReasoning(provider: ProviderConfig): boolean {
|
||||
return Boolean(provider.model_metadata?.reasoning);
|
||||
async function testProvider(provider: ProviderConfig) {
|
||||
if (testingProviderIds.value.includes(provider.id)) return;
|
||||
testingProviderIds.value.push(provider.id);
|
||||
try {
|
||||
const startTime = performance.now();
|
||||
const response = await providerApi.test(provider.id);
|
||||
if (response.data.status === "ok" && response.data.data.error === null) {
|
||||
const latency = Math.max(0, Math.round(performance.now() - startTime));
|
||||
toastSuccess(
|
||||
tm("models.testSuccessWithLatency", {
|
||||
id: provider.id,
|
||||
latency,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
throw new Error(response.data.data.error || tm("models.testError"));
|
||||
}
|
||||
} catch (error: any) {
|
||||
toastError(
|
||||
error.response?.data?.message || error.message || tm("models.testError"),
|
||||
);
|
||||
} finally {
|
||||
testingProviderIds.value = testingProviderIds.value.filter(
|
||||
(id) => id !== provider.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentSelection() {
|
||||
@@ -404,6 +477,35 @@ defineExpose({
|
||||
color: rgba(var(--v-theme-on-surface), 0.5);
|
||||
}
|
||||
|
||||
.meta-icon-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: rgba(var(--v-theme-on-surface), 0.72);
|
||||
}
|
||||
|
||||
.meta-icon-badge--disabled {
|
||||
color: rgba(var(--v-theme-on-surface), 0.34);
|
||||
}
|
||||
|
||||
.meta-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;
|
||||
}
|
||||
|
||||
.provider-menu-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.provider-selected-icon {
|
||||
color: rgba(var(--v-theme-on-surface), 0.72);
|
||||
}
|
||||
|
||||
@@ -76,18 +76,49 @@
|
||||
<v-list-item-subtitle class="regenerate-model-subtitle">
|
||||
<span class="regenerate-model-name">{{ provider.model }}</span>
|
||||
<span class="regenerate-model-icons">
|
||||
<v-icon v-if="supportsImageInput(provider)" size="12">
|
||||
mdi-eye-outline
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsAudioInput(provider)" size="12">
|
||||
mdi-music-note-outline
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsToolCall(provider)" size="12">
|
||||
mdi-wrench
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsReasoning(provider)" size="12">
|
||||
mdi-brain
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
v-for="item in capabilityBadges(provider)"
|
||||
:key="item.key"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
<template #activator="{ props: badgeTooltipProps }">
|
||||
<span
|
||||
v-bind="badgeTooltipProps"
|
||||
class="regenerate-model-icon-badge"
|
||||
:class="{
|
||||
'regenerate-model-icon-badge--disabled': !item.enabled,
|
||||
}"
|
||||
@click.stop
|
||||
>
|
||||
<v-icon size="12">{{ item.icon }}</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ item.tooltip }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
v-if="formatContextLimit(provider, metadataForProvider(provider))"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
<template #activator="{ props: contextTooltipProps }">
|
||||
<span
|
||||
v-bind="contextTooltipProps"
|
||||
class="regenerate-model-context-badge"
|
||||
@click.stop
|
||||
>
|
||||
{{ formatContextLimit(provider, metadataForProvider(provider)) }}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{
|
||||
providerTm("models.metadata.context", {
|
||||
tokens: formatContextLimit(
|
||||
provider,
|
||||
metadataForProvider(provider),
|
||||
),
|
||||
})
|
||||
}}</span>
|
||||
</v-tooltip>
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
@@ -106,17 +137,16 @@ import { ref } from "vue";
|
||||
import { providerApi } from "@/api/v1";
|
||||
import StyledMenu from "@/components/shared/StyledMenu.vue";
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import {
|
||||
formatContextLimit,
|
||||
providerCapabilityBadges,
|
||||
type ProviderModelMetadata,
|
||||
type ProviderMetadataSource,
|
||||
} from "@/utils/providerMetadata";
|
||||
|
||||
interface ModelMetadata {
|
||||
modalities?: { input?: string[] };
|
||||
tool_call?: boolean;
|
||||
reasoning?: boolean;
|
||||
}
|
||||
|
||||
interface ProviderConfig {
|
||||
interface ProviderConfig extends ProviderMetadataSource {
|
||||
id: string;
|
||||
model: string;
|
||||
model_metadata?: ModelMetadata;
|
||||
enable?: boolean;
|
||||
}
|
||||
|
||||
@@ -131,9 +161,11 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { tm } = useModuleI18n("features/chat");
|
||||
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;
|
||||
@@ -141,9 +173,12 @@ async function loadProviderConfigs(force = false) {
|
||||
try {
|
||||
const response = await providerApi.listByProviderType("chat_completion");
|
||||
if (response.data.status === "ok") {
|
||||
providerConfigs.value = ((response.data.data || []) as unknown as ProviderConfig[]).filter(
|
||||
(provider: ProviderConfig) => provider.enable !== false,
|
||||
);
|
||||
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);
|
||||
providersLoaded.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -172,20 +207,16 @@ function retryWithModel(provider: ProviderConfig) {
|
||||
});
|
||||
}
|
||||
|
||||
function supportsImageInput(provider: ProviderConfig): boolean {
|
||||
return Boolean(provider.model_metadata?.modalities?.input?.includes("image"));
|
||||
function capabilityBadges(provider: ProviderConfig) {
|
||||
return providerCapabilityBadges(
|
||||
provider,
|
||||
metadataForProvider(provider),
|
||||
providerTm,
|
||||
);
|
||||
}
|
||||
|
||||
function supportsAudioInput(provider: ProviderConfig): boolean {
|
||||
return Boolean(provider.model_metadata?.modalities?.input?.includes("audio"));
|
||||
}
|
||||
|
||||
function supportsToolCall(provider: ProviderConfig): boolean {
|
||||
return Boolean(provider.model_metadata?.tool_call);
|
||||
}
|
||||
|
||||
function supportsReasoning(provider: ProviderConfig): boolean {
|
||||
return Boolean(provider.model_metadata?.reasoning);
|
||||
function metadataForProvider(provider: ProviderConfig) {
|
||||
return provider.model ? modelMetadata.value[provider.model] || null : null;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -226,6 +257,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));
|
||||
|
||||
@@ -66,19 +66,47 @@
|
||||
<div class="provider-model-row__title">{{ entry.provider.id }}</div>
|
||||
<div class="provider-model-row__subtitle">{{ entry.provider.model }}</div>
|
||||
<div class="provider-model-row__meta">
|
||||
<span
|
||||
v-for="item in capabilityIcons(entry.metadata)"
|
||||
:key="item.icon"
|
||||
class="provider-model-row__badge"
|
||||
<v-tooltip
|
||||
v-for="item in capabilityBadges(entry)"
|
||||
:key="item.key"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
<v-icon size="14">{{ item.icon }}</v-icon>
|
||||
</span>
|
||||
<span
|
||||
<template #activator="{ props: badgeTooltipProps }">
|
||||
<span
|
||||
v-bind="badgeTooltipProps"
|
||||
class="provider-model-row__badge"
|
||||
:class="{
|
||||
'provider-model-row__badge--enabled': item.enabled,
|
||||
'provider-model-row__badge--disabled': !item.enabled
|
||||
}"
|
||||
@click.stop
|
||||
>
|
||||
<v-icon size="14">{{ item.icon }}</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ item.tooltip }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
v-if="formatContextLimit(entry.metadata)"
|
||||
class="provider-model-row__badge provider-model-row__badge--text"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
{{ formatContextLimit(entry.metadata) }}
|
||||
</span>
|
||||
<template #activator="{ props: contextTooltipProps }">
|
||||
<span
|
||||
v-bind="contextTooltipProps"
|
||||
class="provider-model-row__badge provider-model-row__badge--text provider-model-row__badge--enabled"
|
||||
@click.stop
|
||||
>
|
||||
{{ formatContextLimit(entry.metadata) }}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{
|
||||
tm('models.metadata.context', {
|
||||
tokens: formatContextLimit(entry.metadata)
|
||||
})
|
||||
}}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -94,14 +122,20 @@
|
||||
@update:modelValue="emit('toggle-provider-enable', entry.provider, $event)"
|
||||
></v-switch>
|
||||
|
||||
<v-btn
|
||||
icon="mdi-connection"
|
||||
size="small"
|
||||
variant="text"
|
||||
:disabled="!entry.provider.enable || isProviderSaving(entry.provider.id)"
|
||||
:loading="isProviderTesting(entry.provider.id)"
|
||||
@click.stop="emit('test-provider', entry.provider)"
|
||||
></v-btn>
|
||||
<v-tooltip location="top">
|
||||
<template #activator="{ props: testTooltipProps }">
|
||||
<v-btn
|
||||
v-bind="testTooltipProps"
|
||||
icon="mdi-connection"
|
||||
size="small"
|
||||
variant="text"
|
||||
:disabled="!entry.provider.enable || isProviderSaving(entry.provider.id)"
|
||||
:loading="isProviderTesting(entry.provider.id)"
|
||||
@click.stop="emit('test-provider', entry.provider)"
|
||||
></v-btn>
|
||||
</template>
|
||||
<span>{{ tm('models.testButton') }}</span>
|
||||
</v-tooltip>
|
||||
<v-btn
|
||||
icon="mdi-cog-outline"
|
||||
size="small"
|
||||
@@ -117,8 +151,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div><strong>{{ tm('models.tooltips.providerId') }}:</strong> {{ entry.provider.id }}</div>
|
||||
<div><strong>{{ tm('models.tooltips.modelId') }}:</strong> {{ entry.provider.model }}</div>
|
||||
<div>
|
||||
<strong>{{ tm('models.tooltips.providerId') }}:</strong>
|
||||
{{ entry.provider.id }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ tm('models.tooltips.modelId') }}:</strong>
|
||||
{{ entry.provider.model }}
|
||||
</div>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
@@ -152,19 +192,47 @@
|
||||
>
|
||||
<div class="provider-model-row__title provider-model-row__title--mono">{{ entry.model }}</div>
|
||||
<div class="provider-model-row__meta">
|
||||
<span
|
||||
v-for="item in capabilityIcons(entry.metadata)"
|
||||
:key="item.icon"
|
||||
class="provider-model-row__badge"
|
||||
<v-tooltip
|
||||
v-for="item in capabilityBadges(entry)"
|
||||
:key="item.key"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
<v-icon size="14">{{ item.icon }}</v-icon>
|
||||
</span>
|
||||
<span
|
||||
<template #activator="{ props: badgeTooltipProps }">
|
||||
<span
|
||||
v-bind="badgeTooltipProps"
|
||||
class="provider-model-row__badge"
|
||||
:class="{
|
||||
'provider-model-row__badge--enabled': item.enabled,
|
||||
'provider-model-row__badge--disabled': !item.enabled
|
||||
}"
|
||||
@click.stop
|
||||
>
|
||||
<v-icon size="14">{{ item.icon }}</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ item.tooltip }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
v-if="formatContextLimit(entry.metadata)"
|
||||
class="provider-model-row__badge provider-model-row__badge--text"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
{{ formatContextLimit(entry.metadata) }}
|
||||
</span>
|
||||
<template #activator="{ props: contextTooltipProps }">
|
||||
<span
|
||||
v-bind="contextTooltipProps"
|
||||
class="provider-model-row__badge provider-model-row__badge--text provider-model-row__badge--enabled"
|
||||
@click.stop
|
||||
>
|
||||
{{ formatContextLimit(entry.metadata) }}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{
|
||||
tm('models.metadata.context', {
|
||||
tokens: formatContextLimit(entry.metadata)
|
||||
})
|
||||
}}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -179,7 +247,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div><strong>{{ tm('models.tooltips.modelId') }}:</strong> {{ entry.model }}</div>
|
||||
<div>
|
||||
<strong>{{ tm('models.tooltips.modelId') }}:</strong>
|
||||
{{ entry.model }}
|
||||
</div>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<v-list-item-subtitle>{{ tm('providerSelector.clearSelectionSubtitle') }}</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-icon v-if="selectedProvider === ''" color="primary">mdi-check-circle</v-icon>
|
||||
<v-icon v-if="selectedProvider === ''">mdi-check-circle</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
@@ -124,10 +124,69 @@
|
||||
<v-list-item-subtitle>
|
||||
{{ provider.type || provider.provider_type || tm('providerSelector.unknownType') }}
|
||||
<span v-if="provider.model">- {{ provider.model }}</span>
|
||||
<span
|
||||
v-if="capabilityBadges(provider).length || formatContextLimit(provider, metadataForProvider(provider))"
|
||||
class="provider-selector-meta"
|
||||
>
|
||||
<v-tooltip
|
||||
v-for="item in capabilityBadges(provider)"
|
||||
:key="item.key"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
<template #activator="{ props: badgeTooltipProps }">
|
||||
<span
|
||||
v-bind="badgeTooltipProps"
|
||||
class="provider-selector-meta__badge"
|
||||
:class="{ 'provider-selector-meta__badge--disabled': !item.enabled }"
|
||||
@click.stop
|
||||
>
|
||||
<v-icon size="13">{{ item.icon }}</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ item.tooltip }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
v-if="formatContextLimit(provider, metadataForProvider(provider))"
|
||||
location="top"
|
||||
max-width="320"
|
||||
>
|
||||
<template #activator="{ props: contextTooltipProps }">
|
||||
<span
|
||||
v-bind="contextTooltipProps"
|
||||
class="provider-selector-meta__context"
|
||||
@click.stop
|
||||
>
|
||||
{{ formatContextLimit(provider, metadataForProvider(provider)) }}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{
|
||||
providerTm('models.metadata.context', {
|
||||
tokens: formatContextLimit(provider, metadataForProvider(provider))
|
||||
})
|
||||
}}</span>
|
||||
</v-tooltip>
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-icon v-if="isProviderSelected(provider.id)" color="primary">mdi-check-circle</v-icon>
|
||||
<div class="provider-selector-actions" @click.stop>
|
||||
<v-tooltip location="top">
|
||||
<template #activator="{ props: testTooltipProps }">
|
||||
<v-btn
|
||||
v-bind="testTooltipProps"
|
||||
icon="mdi-connection"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
:loading="isProviderTesting(provider.id)"
|
||||
:disabled="!isProviderTestable(provider)"
|
||||
@click.stop="testProvider(provider)"
|
||||
/>
|
||||
</template>
|
||||
<span>{{ providerTm('models.testButton') }}</span>
|
||||
</v-tooltip>
|
||||
<v-icon v-if="isProviderSelected(provider.id)">mdi-check-circle</v-icon>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -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,8 @@ const loading = ref(false)
|
||||
const selectedProvider = ref('')
|
||||
const selectedProviders = ref([])
|
||||
const providerDrawer = ref(false)
|
||||
const testingProviders = ref([])
|
||||
const modelMetadata = ref({})
|
||||
|
||||
const hasSelection = computed(() => {
|
||||
if (props.multiple) {
|
||||
@@ -265,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))
|
||||
@@ -333,6 +399,46 @@ function isProviderSelected(providerId) {
|
||||
return selectedProvider.value === providerId
|
||||
}
|
||||
|
||||
function capabilityBadges(provider) {
|
||||
return providerCapabilityBadges(provider, metadataForProvider(provider), providerTm)
|
||||
}
|
||||
|
||||
function metadataForProvider(provider) {
|
||||
return provider?.model ? modelMetadata.value[provider.model] || null : null
|
||||
}
|
||||
|
||||
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 +490,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;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,8 @@ export function useProviderModelConfigDialog(options: UseProviderModelConfigDial
|
||||
})
|
||||
|
||||
function openProviderEdit(provider: any) {
|
||||
providerEditData.value = JSON.parse(JSON.stringify(provider))
|
||||
const editableProvider = JSON.parse(JSON.stringify(provider))
|
||||
providerEditData.value = editableProvider
|
||||
providerEditOriginalId.value = provider.id
|
||||
providerEditMode.value = 'edit'
|
||||
showProviderEditDialog.value = true
|
||||
|
||||
@@ -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)
|
||||
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))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -686,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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 модели"
|
||||
|
||||
@@ -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"
|
||||
|
||||
83
dashboard/src/utils/providerMetadata.ts
Normal file
83
dashboard/src/utils/providerMetadata.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
export interface ProviderModelMetadata {
|
||||
modalities?: { input?: string[] }
|
||||
tool_call?: boolean
|
||||
reasoning?: boolean
|
||||
limit?: { context?: number }
|
||||
}
|
||||
|
||||
export interface ProviderMetadataSource {
|
||||
model?: string
|
||||
modalities?: string[]
|
||||
max_context_tokens?: number
|
||||
reasoning?: boolean
|
||||
}
|
||||
|
||||
export interface ProviderCapabilityBadge {
|
||||
key: string
|
||||
icon: string
|
||||
enabled: boolean
|
||||
tooltip: string
|
||||
}
|
||||
|
||||
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`
|
||||
return `${context}`
|
||||
}
|
||||
|
||||
export function providerCapabilityBadges(
|
||||
provider: ProviderMetadataSource | null | undefined,
|
||||
metadata: ProviderModelMetadata | null | undefined,
|
||||
tm: (key: string, params?: Record<string, string>) => string
|
||||
): ProviderCapabilityBadge[] {
|
||||
const inputs = 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(metadata?.tool_call),
|
||||
enabled: modalities.includes('tool_use'),
|
||||
label: tm('models.metadata.toolUse')
|
||||
},
|
||||
{
|
||||
key: 'reasoning',
|
||||
icon: 'mdi-brain',
|
||||
supported: Boolean(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: !metadata || item.enabled,
|
||||
tooltip:
|
||||
metadata && !item.enabled
|
||||
? tm('models.metadata.supportedDisabled', { capability: item.label })
|
||||
: tm('models.metadata.enabled', { capability: item.label })
|
||||
}))
|
||||
}
|
||||
@@ -592,6 +592,13 @@ def test_file_uri_to_path_supports_standard_and_legacy_posix_file_uris(tmp_path)
|
||||
assert media_utils.file_uri_to_path(legacy_file_uri) == str(media_path)
|
||||
|
||||
|
||||
def test_file_uri_to_path_preserves_posix_root_for_container_paths():
|
||||
if os.name != "nt":
|
||||
assert media_utils.file_uri_to_path("file:///AstrBot/data/cache/image.png") == (
|
||||
"/AstrBot/data/cache/image.png"
|
||||
)
|
||||
|
||||
|
||||
def test_from_file_system_uses_pathlib_file_uri(tmp_path):
|
||||
media_path = tmp_path / "media file.bin"
|
||||
media_path.write_bytes(b"media")
|
||||
|
||||
Reference in New Issue
Block a user