mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat: 视频计费增强与影子计费系统
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -26,58 +26,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 能力筛选 -->
|
||||
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.streaming ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="流式输出"
|
||||
@click="capabilityFilters.streaming = !capabilityFilters.streaming"
|
||||
>
|
||||
<Zap class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.imageGeneration ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="图像生成"
|
||||
@click="capabilityFilters.imageGeneration = !capabilityFilters.imageGeneration"
|
||||
>
|
||||
<Image class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.vision ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="视觉理解"
|
||||
@click="capabilityFilters.vision = !capabilityFilters.vision"
|
||||
>
|
||||
<Eye class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.toolUse ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="工具调用"
|
||||
@click="capabilityFilters.toolUse = !capabilityFilters.toolUse"
|
||||
>
|
||||
<Wrench class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.extendedThinking ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="深度思考"
|
||||
@click="capabilityFilters.extendedThinking = !capabilityFilters.extendedThinking"
|
||||
>
|
||||
<Brain class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -103,7 +51,7 @@
|
||||
模型名称
|
||||
</TableHead>
|
||||
<TableHead class="w-[140px]">
|
||||
能力/偏好
|
||||
模型偏好
|
||||
</TableHead>
|
||||
<TableHead class="w-[160px] text-center">
|
||||
价格 ($/M)
|
||||
@@ -165,45 +113,19 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="space-y-1 w-fit">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Zap
|
||||
v-if="model.config?.streaming !== false"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="流式输出"
|
||||
/>
|
||||
<Image
|
||||
v-if="model.config?.image_generation === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="图像生成"
|
||||
/>
|
||||
<Eye
|
||||
v-if="model.config?.vision === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="视觉理解"
|
||||
/>
|
||||
<Wrench
|
||||
v-if="model.config?.function_calling === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="工具调用"
|
||||
/>
|
||||
<Brain
|
||||
v-if="model.config?.extended_thinking === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="深度思考"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-0.5">
|
||||
<template v-if="model.supported_capabilities?.length">
|
||||
<div class="border-t border-border/50" />
|
||||
<div class="flex flex-wrap gap-0.5">
|
||||
<span
|
||||
v-for="capName in model.supported_capabilities"
|
||||
:key="capName"
|
||||
class="text-[11px] px-1 py-0.5 rounded bg-muted/60 text-muted-foreground"
|
||||
:title="getCapabilityDisplayName(capName)"
|
||||
>{{ getCapabilityShortName(capName) }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-for="capName in model.supported_capabilities"
|
||||
:key="capName"
|
||||
class="text-[11px] px-1 py-0.5 rounded bg-muted/60 text-muted-foreground"
|
||||
:title="getCapabilityDisplayName(capName)"
|
||||
>{{ getCapabilityShortName(capName) }}</span>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted-foreground text-xs"
|
||||
>-</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
@@ -227,9 +149,17 @@
|
||||
<span class="text-muted-foreground">按次:</span>
|
||||
<span class="font-mono ml-1">${{ model.default_price_per_request.toFixed(3) }}/次</span>
|
||||
</div>
|
||||
<!-- 视频费用计费 -->
|
||||
<div v-if="hasVideoPricing(model)">
|
||||
<span class="text-muted-foreground">视频:</span>
|
||||
<span
|
||||
class="font-mono ml-1"
|
||||
:title="getVideoPricingTooltip(model)"
|
||||
>{{ getVideoPricingDisplay(model) }}</span>
|
||||
</div>
|
||||
<!-- 无计费配置 -->
|
||||
<div
|
||||
v-if="!getFirstTierPrice(model, 'input') && !getFirstTierPrice(model, 'output') && !model.default_price_per_request"
|
||||
v-if="!getFirstTierPrice(model, 'input') && !getFirstTierPrice(model, 'output') && !model.default_price_per_request && !hasVideoPricing(model)"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
-
|
||||
@@ -358,28 +288,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:能力图标 -->
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<Zap
|
||||
v-if="model.config?.streaming !== false"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
/>
|
||||
<Image
|
||||
v-if="model.config?.image_generation === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
/>
|
||||
<Eye
|
||||
v-if="model.config?.vision === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
/>
|
||||
<Wrench
|
||||
v-if="model.config?.function_calling === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
/>
|
||||
<Brain
|
||||
v-if="model.config?.extended_thinking === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
/>
|
||||
<!-- 第二行:模型偏好 -->
|
||||
<div
|
||||
v-if="model.supported_capabilities?.length"
|
||||
class="flex flex-wrap gap-0.5"
|
||||
>
|
||||
<span
|
||||
v-for="capName in model.supported_capabilities"
|
||||
:key="capName"
|
||||
class="text-[11px] px-1 py-0.5 rounded bg-muted/60 text-muted-foreground"
|
||||
>{{ getCapabilityShortName(capName) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 第三行:统计信息 -->
|
||||
@@ -601,6 +519,7 @@ import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useRowClick } from '@/composables/useRowClick'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { sortResolutionEntries } from '@/utils/form'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -722,6 +641,43 @@ function hasTieredPricing(model: GlobalModelResponse): boolean {
|
||||
return (tiered?.tiers?.length || 0) > 1
|
||||
}
|
||||
|
||||
// 检测是否有视频分辨率计费配置
|
||||
function hasVideoPricing(model: GlobalModelResponse): boolean {
|
||||
const priceByResolution = model.config?.billing?.video?.price_per_second_by_resolution
|
||||
return priceByResolution && typeof priceByResolution === 'object' && Object.keys(priceByResolution).length > 0
|
||||
}
|
||||
|
||||
// 获取视频分辨率计费的数量
|
||||
function getVideoPricingCount(model: GlobalModelResponse): number {
|
||||
const priceByResolution = model.config?.billing?.video?.price_per_second_by_resolution
|
||||
if (!priceByResolution || typeof priceByResolution !== 'object') return 0
|
||||
return Object.keys(priceByResolution).length
|
||||
}
|
||||
|
||||
// 获取视频计费的显示文本(如:720p $0.1/s [多分辨率])
|
||||
function getVideoPricingDisplay(model: GlobalModelResponse): string {
|
||||
const priceByResolution = model.config?.billing?.video?.price_per_second_by_resolution
|
||||
if (!priceByResolution || typeof priceByResolution !== 'object') return ''
|
||||
const entries = sortResolutionEntries(Object.entries(priceByResolution))
|
||||
if (entries.length === 0) return ''
|
||||
// 获取最低分辨率和价格
|
||||
const [firstRes, firstPrice] = entries[0]
|
||||
const priceStr = `${firstRes} $${(firstPrice as number).toFixed(2)}/s`
|
||||
// 如果有多个分辨率,添加标记
|
||||
if (entries.length > 1) {
|
||||
return `${priceStr} [${entries.length}种]`
|
||||
}
|
||||
return priceStr
|
||||
}
|
||||
|
||||
// 获取视频计费详情的 tooltip
|
||||
function getVideoPricingTooltip(model: GlobalModelResponse): string {
|
||||
const priceByResolution = model.config?.billing?.video?.price_per_second_by_resolution
|
||||
if (!priceByResolution || typeof priceByResolution !== 'object') return ''
|
||||
const entries = sortResolutionEntries(Object.entries(priceByResolution))
|
||||
return entries.map(([res, price]) => `${res}: $${(price as number).toFixed(4)}/s`).join('\n')
|
||||
}
|
||||
|
||||
// 检测是否有对话框打开(防止误关闭抽屉)
|
||||
const hasBlockingDialogOpen = computed(() =>
|
||||
createModelDialogOpen.value ||
|
||||
|
||||
@@ -97,23 +97,23 @@
|
||||
>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/40 hover:bg-transparent">
|
||||
<TableHead class="w-[18%] min-w-[140px] h-11 font-medium text-foreground/80">
|
||||
<TableRow>
|
||||
<TableHead class="w-[18%] min-w-[140px]">
|
||||
提供商信息
|
||||
</TableHead>
|
||||
<TableHead class="w-[20%] min-w-[180px] h-11 font-medium text-foreground/80">
|
||||
<TableHead class="w-[20%] min-w-[180px]">
|
||||
余额监控
|
||||
</TableHead>
|
||||
<TableHead class="w-[12%] min-w-[100px] h-11 font-medium text-foreground/80 text-center">
|
||||
<TableHead class="w-[12%] min-w-[100px] text-center">
|
||||
资源统计
|
||||
</TableHead>
|
||||
<TableHead class="w-[24%] min-w-[260px] h-11 font-medium text-foreground/80">
|
||||
<TableHead class="w-[24%] min-w-[260px]">
|
||||
端点健康
|
||||
</TableHead>
|
||||
<TableHead class="w-[8%] min-w-[60px] h-11 font-medium text-foreground/80 text-center">
|
||||
<TableHead class="w-[8%] min-w-[60px] text-center">
|
||||
状态
|
||||
</TableHead>
|
||||
<TableHead class="w-[18%] min-w-[120px] h-11 font-medium text-foreground/80 text-center">
|
||||
<TableHead class="w-[18%] min-w-[120px] text-center">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
|
||||
@@ -210,10 +210,10 @@
|
||||
</div>
|
||||
</CardSection>
|
||||
|
||||
<!-- 日志记录配置 -->
|
||||
<!-- 请求记录配置 -->
|
||||
<CardSection
|
||||
title="日志记录"
|
||||
description="控制请求日志的记录方式和内容"
|
||||
title="请求记录"
|
||||
description="控制请求/响应详情的入库方式和内容"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
@@ -233,7 +233,7 @@
|
||||
记录详细程度
|
||||
</Label>
|
||||
<Select
|
||||
v-model="systemConfig.request_log_level"
|
||||
v-model="systemConfig.request_record_level"
|
||||
v-model:open="logLevelSelectOpen"
|
||||
>
|
||||
<SelectTrigger
|
||||
@@ -317,10 +317,10 @@
|
||||
</div>
|
||||
</CardSection>
|
||||
|
||||
<!-- 日志清理策略 -->
|
||||
<!-- 请求记录清理策略 -->
|
||||
<CardSection
|
||||
title="日志清理策略"
|
||||
description="配置日志的分级保留和自动清理"
|
||||
title="请求记录清理策略"
|
||||
description="配置请求记录的分级保留和自动清理"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -357,7 +357,7 @@
|
||||
for="detail-log-retention-days"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
详细日志保留天数
|
||||
详细记录保留天数
|
||||
</Label>
|
||||
<Input
|
||||
id="detail-log-retention-days"
|
||||
@@ -376,7 +376,7 @@
|
||||
for="compressed-log-retention-days"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
压缩日志保留天数
|
||||
压缩记录保留天数
|
||||
</Label>
|
||||
<Input
|
||||
id="compressed-log-retention-days"
|
||||
@@ -414,7 +414,7 @@
|
||||
for="log-retention-days"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
完整日志保留天数
|
||||
完整记录保留天数
|
||||
</Label>
|
||||
<Input
|
||||
id="log-retention-days"
|
||||
@@ -909,12 +909,12 @@ interface SystemConfig {
|
||||
auto_delete_expired_keys: boolean
|
||||
// 格式转换
|
||||
enable_format_conversion: boolean
|
||||
// 日志记录
|
||||
request_log_level: string
|
||||
// 请求记录
|
||||
request_record_level: string
|
||||
max_request_body_size: number
|
||||
max_response_body_size: number
|
||||
sensitive_headers: string[]
|
||||
// 日志清理
|
||||
// 请求记录清理
|
||||
enable_auto_cleanup: boolean
|
||||
detail_log_retention_days: number
|
||||
compressed_log_retention_days: number
|
||||
@@ -965,12 +965,12 @@ const systemConfig = ref<SystemConfig>({
|
||||
auto_delete_expired_keys: false,
|
||||
// 格式转换
|
||||
enable_format_conversion: false,
|
||||
// 日志记录
|
||||
request_log_level: 'basic',
|
||||
// 请求记录
|
||||
request_record_level: 'basic',
|
||||
max_request_body_size: 1048576,
|
||||
max_response_body_size: 1048576,
|
||||
sensitive_headers: ['authorization', 'x-api-key', 'api-key', 'cookie', 'set-cookie'],
|
||||
// 日志清理
|
||||
// 请求记录清理
|
||||
enable_auto_cleanup: true,
|
||||
detail_log_retention_days: 7,
|
||||
compressed_log_retention_days: 90,
|
||||
@@ -1000,7 +1000,7 @@ const hasBasicConfigChanges = computed(() => {
|
||||
const hasLogConfigChanges = computed(() => {
|
||||
if (!originalConfig.value) return false
|
||||
return (
|
||||
systemConfig.value.request_log_level !== originalConfig.value.request_log_level ||
|
||||
systemConfig.value.request_record_level !== originalConfig.value.request_record_level ||
|
||||
systemConfig.value.max_request_body_size !== originalConfig.value.max_request_body_size ||
|
||||
systemConfig.value.max_response_body_size !== originalConfig.value.max_response_body_size ||
|
||||
JSON.stringify(systemConfig.value.sensitive_headers) !== JSON.stringify(originalConfig.value.sensitive_headers)
|
||||
@@ -1072,12 +1072,12 @@ async function loadSystemConfig() {
|
||||
'auto_delete_expired_keys',
|
||||
// 格式转换
|
||||
'enable_format_conversion',
|
||||
// 日志记录
|
||||
'request_log_level',
|
||||
// 请求记录
|
||||
'request_record_level',
|
||||
'max_request_body_size',
|
||||
'max_response_body_size',
|
||||
'sensitive_headers',
|
||||
// 日志清理
|
||||
// 请求记录清理
|
||||
'enable_auto_cleanup',
|
||||
'detail_log_retention_days',
|
||||
'compressed_log_retention_days',
|
||||
@@ -1165,8 +1165,8 @@ async function saveLogConfig() {
|
||||
try {
|
||||
const configItems = [
|
||||
{
|
||||
key: 'request_log_level',
|
||||
value: systemConfig.value.request_log_level,
|
||||
key: 'request_record_level',
|
||||
value: systemConfig.value.request_record_level,
|
||||
description: '请求记录级别'
|
||||
},
|
||||
{
|
||||
@@ -1193,15 +1193,15 @@ async function saveLogConfig() {
|
||||
)
|
||||
// 更新原始值
|
||||
if (originalConfig.value) {
|
||||
originalConfig.value.request_log_level = systemConfig.value.request_log_level
|
||||
originalConfig.value.request_record_level = systemConfig.value.request_record_level
|
||||
originalConfig.value.max_request_body_size = systemConfig.value.max_request_body_size
|
||||
originalConfig.value.max_response_body_size = systemConfig.value.max_response_body_size
|
||||
originalConfig.value.sensitive_headers = [...systemConfig.value.sensitive_headers]
|
||||
}
|
||||
success('日志配置已保存')
|
||||
success('请求记录配置已保存')
|
||||
} catch (err) {
|
||||
error('保存配置失败')
|
||||
log.error('保存日志配置失败:', err)
|
||||
log.error('保存请求记录配置失败:', err)
|
||||
} finally {
|
||||
logConfigLoading.value = false
|
||||
}
|
||||
@@ -1250,12 +1250,12 @@ async function saveCleanupConfig() {
|
||||
{
|
||||
key: 'detail_log_retention_days',
|
||||
value: systemConfig.value.detail_log_retention_days,
|
||||
description: '详细日志保留天数'
|
||||
description: '详细记录保留天数'
|
||||
},
|
||||
{
|
||||
key: 'compressed_log_retention_days',
|
||||
value: systemConfig.value.compressed_log_retention_days,
|
||||
description: '压缩日志保留天数'
|
||||
description: '压缩记录保留天数'
|
||||
},
|
||||
{
|
||||
key: 'header_retention_days',
|
||||
@@ -1265,7 +1265,7 @@ async function saveCleanupConfig() {
|
||||
{
|
||||
key: 'log_retention_days',
|
||||
value: systemConfig.value.log_retention_days,
|
||||
description: '完整日志保留天数'
|
||||
description: '完整记录保留天数'
|
||||
},
|
||||
{
|
||||
key: 'cleanup_batch_size',
|
||||
@@ -1293,10 +1293,10 @@ async function saveCleanupConfig() {
|
||||
originalConfig.value.cleanup_batch_size = systemConfig.value.cleanup_batch_size
|
||||
originalConfig.value.audit_log_retention_days = systemConfig.value.audit_log_retention_days
|
||||
}
|
||||
success('日志清理配置已保存')
|
||||
success('请求记录清理配置已保存')
|
||||
} catch (err) {
|
||||
error('保存配置失败')
|
||||
log.error('保存日志清理配置失败:', err)
|
||||
log.error('保存请求记录清理配置失败:', err)
|
||||
} finally {
|
||||
cleanupConfigLoading.value = false
|
||||
}
|
||||
|
||||
@@ -29,38 +29,38 @@ const providerExamples = [
|
||||
{
|
||||
name: 'OpenAI',
|
||||
url: 'https://api.openai.com',
|
||||
format: 'OpenAI',
|
||||
format: 'OpenAI Chat',
|
||||
note: '官方 API,需要国际信用卡或通过代理访问'
|
||||
},
|
||||
{
|
||||
name: 'Anthropic',
|
||||
url: 'https://api.anthropic.com',
|
||||
format: 'Claude',
|
||||
format: 'Claude Chat',
|
||||
note: '官方 Claude API'
|
||||
},
|
||||
{
|
||||
name: 'Google AI',
|
||||
url: 'https://generativelanguage.googleapis.com',
|
||||
format: 'Gemini',
|
||||
format: 'Gemini Chat',
|
||||
note: '官方 Gemini API'
|
||||
},
|
||||
{
|
||||
name: 'Azure OpenAI',
|
||||
url: 'https://{resource}.openai.azure.com',
|
||||
format: 'OpenAI',
|
||||
format: 'OpenAI Chat',
|
||||
note: '需要替换 {resource} 为你的资源名'
|
||||
},
|
||||
{
|
||||
name: 'OpenRouter',
|
||||
url: 'https://openrouter.ai/api',
|
||||
format: 'OpenAI',
|
||||
format: 'OpenAI Chat',
|
||||
note: '聚合多家供应商的 API 代理'
|
||||
},
|
||||
{
|
||||
name: '自托管 / 其他',
|
||||
url: 'https://your-api.com',
|
||||
format: 'OpenAI',
|
||||
note: '大多数 OpenAI 兼容服务选择 OpenAI 格式'
|
||||
format: 'OpenAI Chat',
|
||||
note: '大多数 OpenAI 兼容服务选择 OpenAI Chat 格式'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -111,7 +111,7 @@ export const coreConcepts = [
|
||||
// API 格式说明
|
||||
export const apiFormats = [
|
||||
{
|
||||
name: 'OpenAI',
|
||||
name: 'OpenAI Chat',
|
||||
endpoint: '/v1/chat/completions',
|
||||
auth: 'Authorization: Bearer xxx',
|
||||
clients: ['OpenAI SDK', 'Cursor', 'LangChain', '大部分开源工具']
|
||||
@@ -123,7 +123,7 @@ export const apiFormats = [
|
||||
clients: ['Codex CLI']
|
||||
},
|
||||
{
|
||||
name: 'Claude',
|
||||
name: 'Claude Chat',
|
||||
endpoint: '/v1/messages',
|
||||
auth: 'x-api-key: xxx',
|
||||
clients: ['Anthropic SDK']
|
||||
@@ -135,7 +135,7 @@ export const apiFormats = [
|
||||
clients: ['Claude Code']
|
||||
},
|
||||
{
|
||||
name: 'Gemini',
|
||||
name: 'Gemini Chat',
|
||||
endpoint: '/v1beta/models/{model}:generateContent',
|
||||
auth: 'x-goog-api-key: xxx',
|
||||
clients: ['Gemini SDK', 'Gemini CLI']
|
||||
|
||||
@@ -24,40 +24,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 能力筛选 -->
|
||||
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.vision ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="Vision"
|
||||
@click="capabilityFilters.vision = !capabilityFilters.vision"
|
||||
>
|
||||
<Eye class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.toolUse ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="Tool Use"
|
||||
@click="capabilityFilters.toolUse = !capabilityFilters.toolUse"
|
||||
>
|
||||
<Wrench class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.extendedThinking ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="Extended Thinking"
|
||||
@click="capabilityFilters.extendedThinking = !capabilityFilters.extendedThinking"
|
||||
>
|
||||
<Brain class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<RefreshButton
|
||||
:loading="loading"
|
||||
@@ -77,9 +43,6 @@
|
||||
<TableHead class="w-[120px] h-12 font-semibold">
|
||||
模型偏好
|
||||
</TableHead>
|
||||
<TableHead class="w-[100px] h-12 font-semibold">
|
||||
能力
|
||||
</TableHead>
|
||||
<TableHead class="w-[140px] h-12 font-semibold text-center">
|
||||
价格 ($/M)
|
||||
</TableHead>
|
||||
@@ -91,7 +54,7 @@
|
||||
<TableBody>
|
||||
<TableRow v-if="loading">
|
||||
<TableCell
|
||||
colspan="5"
|
||||
colspan="4"
|
||||
class="text-center py-12"
|
||||
>
|
||||
<Loader2 class="w-6 h-6 animate-spin mx-auto" />
|
||||
@@ -99,7 +62,7 @@
|
||||
</TableRow>
|
||||
<TableRow v-else-if="filteredModels.length === 0">
|
||||
<TableCell
|
||||
colspan="5"
|
||||
colspan="4"
|
||||
class="text-center py-12 text-muted-foreground"
|
||||
>
|
||||
没有找到匹配的模型
|
||||
@@ -162,25 +125,6 @@
|
||||
>-</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div class="flex gap-1.5">
|
||||
<Eye
|
||||
v-if="model.config?.vision === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="Vision"
|
||||
/>
|
||||
<Wrench
|
||||
v-if="model.config?.function_calling === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="Tool Use"
|
||||
/>
|
||||
<Brain
|
||||
v-if="model.config?.extended_thinking === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="Extended Thinking"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-center">
|
||||
<div class="text-xs space-y-0.5">
|
||||
<!-- 按 Token 计费 -->
|
||||
@@ -250,23 +194,7 @@
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:能力图标 -->
|
||||
<div class="flex gap-1.5">
|
||||
<Eye
|
||||
v-if="model.config?.vision === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
/>
|
||||
<Wrench
|
||||
v-if="model.config?.function_calling === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
/>
|
||||
<Brain
|
||||
v-if="model.config?.extended_thinking === true"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 第三行:价格 -->
|
||||
<!-- 第二行:价格 -->
|
||||
<div
|
||||
v-if="getFirstTierPrice(model, 'input') || getFirstTierPrice(model, 'output')"
|
||||
class="text-xs text-muted-foreground font-mono"
|
||||
|
||||
Reference in New Issue
Block a user