mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(admin,pool,billing): 端点级模型测试、Provider 自动置顶、缓存 TTL 分级计费展示与账号状态增强
- 模型测试支持指定端点:新增 ModelTestDialog 组件,多端点时弹窗选择,单端点直接测试; 后端 test-model-failover 接口新增 endpoint_id 参数,支持 global/direct 模式下按端点过滤候选 - 创建 Provider 时优先级自动置顶(provider_priority 默认 None,后端取 min-1), 显式指定优先级时 shift 已有行;前端创建时不发送 priority,更新时保留 - 缓存计费 UI 增强:RequestDetailDrawer 支持 5min/1h 缓存创建 token 分级展示, 含按 TTL 匹配单价和分行成本计算;ModelDetailDrawer/ModelsTab 标签区分 5min/1h 缓存创建 - Pool 批量操作额度筛选拆分为「无5H限额」和「无周限额」,按 | 分隔 segment 匹配 - KeyFormDialog 优化非 vertex_ai 时布局,API 密钥输入内联到 grid 右列 - Codex refresher 结构化错误标记:401/402/403 使用 [OAUTH_EXPIRED]/[ACCOUNT_BLOCK] 前缀, 新增 deactivated_workspace 识别与分类 - 前后端 accountBlock 关键词同步:新增 token invalidated、deactivated_workspace 识别, OAuth 失效提示清理 block 前缀后展示 - PoolConfig 新增 batch_concurrency 配置(默认 8,上限 32) - 预设模型新增 gpt-5.4;TestResultDialog 响应式布局与 key 脱敏优化
This commit is contained in:
@@ -193,7 +193,7 @@
|
||||
<!-- 阶梯标题 -->
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-2 flex-wrap">
|
||||
<span class="font-medium text-foreground">Token 计费</span>
|
||||
<span class="text-muted-foreground/60">(输入 {{ formatNumber(detail.tokens?.input || detail.input_tokens || 0) }} + 缓存创建 {{ formatNumber(detail.cache_creation_input_tokens || 0) }} + 缓存读取 {{ formatNumber(detail.cache_read_input_tokens || 0) }})</span>
|
||||
<span class="text-muted-foreground/60">(输入 {{ formatNumber(detail.tokens?.input || detail.input_tokens || 0) }} + 缓存创建 {{ cacheCreationSummaryText }} + 缓存读取 {{ formatNumber(detail.cache_read_input_tokens || 0) }})</span>
|
||||
<Badge
|
||||
v-if="displayTiers.length > 1"
|
||||
variant="outline"
|
||||
@@ -236,7 +236,15 @@
|
||||
<div class="text-muted-foreground flex items-center gap-2 flex-wrap">
|
||||
<span>输入 ${{ formatPrice(tier.input_price_per_1m) }}/M</span>
|
||||
<span>输出 ${{ formatPrice(tier.output_price_per_1m) }}/M</span>
|
||||
<span v-if="tier.cache_creation_price_per_1m">
|
||||
<template v-if="hasTierCacheCreationSplitPricing(tier)">
|
||||
<span v-if="getTierCachePriceForTTL(tier, 5, 'cache_creation_price_per_1m') !== null">
|
||||
缓存创建(5min) ${{ formatPrice(getTierCachePriceForTTL(tier, 5, 'cache_creation_price_per_1m') || 0) }}/M
|
||||
</span>
|
||||
<span v-if="getTierCachePriceForTTL(tier, 60, 'cache_creation_price_per_1m') !== null">
|
||||
缓存创建(1h) ${{ formatPrice(getTierCachePriceForTTL(tier, 60, 'cache_creation_price_per_1m') || 0) }}/M
|
||||
</span>
|
||||
</template>
|
||||
<span v-else-if="tier.cache_creation_price_per_1m">
|
||||
缓存创建 ${{ formatPrice(tier.cache_creation_price_per_1m) }}/M
|
||||
</span>
|
||||
<span v-if="tier.cache_read_price_per_1m">
|
||||
@@ -267,7 +275,7 @@
|
||||
<!-- 缓存创建 缓存读取 -->
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">缓存创建</span>
|
||||
<span class="text-xs text-muted-foreground w-[56px]">{{ cacheCreationSplitRows.length > 0 ? '创建合计' : '缓存创建' }}</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.cache_creation_input_tokens || 0 }}</span>
|
||||
<span class="text-xs font-mono">${{ (detail.cache_creation_cost || 0).toFixed(6) }}</span>
|
||||
</div>
|
||||
@@ -283,11 +291,22 @@
|
||||
</div>
|
||||
<!-- 缓存创建 5m/1h 细分 -->
|
||||
<div
|
||||
v-if="(detail.cache_creation_input_tokens_5m || 0) > 0 || (detail.cache_creation_input_tokens_1h || 0) > 0"
|
||||
class="flex items-center pl-[56px]"
|
||||
v-if="cacheCreationSplitRows.length > 0"
|
||||
class="space-y-1 pl-[56px]"
|
||||
>
|
||||
<span class="text-xs text-muted-foreground/50">5min: {{ detail.cache_creation_input_tokens_5m || 0 }}</span>
|
||||
<span class="text-xs text-muted-foreground/50 ml-4">1h: {{ detail.cache_creation_input_tokens_1h || 0 }}</span>
|
||||
<div
|
||||
v-for="row in cacheCreationSplitRows"
|
||||
:key="row.key"
|
||||
class="flex items-center gap-4 text-xs text-muted-foreground/70"
|
||||
>
|
||||
<span class="w-[72px]">{{ row.label }}</span>
|
||||
<span class="font-mono text-foreground/90">{{ formatNumber(row.tokens) }}</span>
|
||||
<span v-if="row.pricePer1M !== null">${{ formatPrice(row.pricePer1M) }}/M</span>
|
||||
<span
|
||||
v-if="row.cost !== null"
|
||||
class="font-mono"
|
||||
>${{ row.cost.toFixed(6) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -714,6 +733,18 @@ const historicalPricing = ref<{
|
||||
cache_read_price: string
|
||||
request_price: string
|
||||
} | null>(null)
|
||||
|
||||
type CacheTTLPriceEntry = {
|
||||
ttl_minutes?: number | null
|
||||
cache_creation_price_per_1m?: number | null
|
||||
cache_read_price_per_1m?: number | null
|
||||
}
|
||||
|
||||
type PricingTierLike = {
|
||||
cache_creation_price_per_1m?: number | null
|
||||
cache_read_price_per_1m?: number | null
|
||||
cache_ttl_pricing?: CacheTTLPriceEntry[] | null
|
||||
}
|
||||
const autoRefreshTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const autoRefreshing = ref(false)
|
||||
const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hidden)
|
||||
@@ -943,6 +974,70 @@ const currentTierIndex = computed(() => {
|
||||
return 0
|
||||
})
|
||||
|
||||
const currentTier = computed<PricingTierLike | null>(() => {
|
||||
const tier = displayTiers.value[currentTierIndex.value]
|
||||
if (!tier || typeof tier !== 'object') return null
|
||||
return tier as PricingTierLike
|
||||
})
|
||||
|
||||
const cacheCreationSummaryText = computed(() => {
|
||||
if (!detail.value) return '0'
|
||||
|
||||
const total = detail.value.cache_creation_input_tokens || 0
|
||||
const cache5m = detail.value.cache_creation_input_tokens_5m || 0
|
||||
const cache1h = detail.value.cache_creation_input_tokens_1h || 0
|
||||
|
||||
if (cache5m <= 0 && cache1h <= 0) {
|
||||
return formatNumber(total)
|
||||
}
|
||||
|
||||
const parts: string[] = []
|
||||
if (cache5m > 0) parts.push(`5min ${formatNumber(cache5m)}`)
|
||||
if (cache1h > 0) parts.push(`1h ${formatNumber(cache1h)}`)
|
||||
const remaining = Math.max(0, total - cache5m - cache1h)
|
||||
if (remaining > 0) parts.push(`其他 ${formatNumber(remaining)}`)
|
||||
return parts.join(' + ')
|
||||
})
|
||||
|
||||
const cacheCreationSplitRows = computed(() => {
|
||||
if (!detail.value) return []
|
||||
|
||||
const rows: Array<{
|
||||
key: string
|
||||
label: string
|
||||
tokens: number
|
||||
pricePer1M: number | null
|
||||
cost: number | null
|
||||
}> = []
|
||||
|
||||
const cache5m = detail.value.cache_creation_input_tokens_5m || 0
|
||||
const cache1h = detail.value.cache_creation_input_tokens_1h || 0
|
||||
|
||||
if (cache5m > 0) {
|
||||
const pricePer1M = getActiveCachePriceForTTL(5, 'cache_creation_price_per_1m')
|
||||
rows.push({
|
||||
key: '5m',
|
||||
label: '5min 创建',
|
||||
tokens: cache5m,
|
||||
pricePer1M,
|
||||
cost: pricePer1M !== null ? (cache5m * pricePer1M) / 1_000_000 : null,
|
||||
})
|
||||
}
|
||||
|
||||
if (cache1h > 0) {
|
||||
const pricePer1M = getActiveCachePriceForTTL(60, 'cache_creation_price_per_1m')
|
||||
rows.push({
|
||||
key: '1h',
|
||||
label: '1h 创建',
|
||||
tokens: cache1h,
|
||||
pricePer1M,
|
||||
cost: pricePer1M !== null ? (cache1h * pricePer1M) / 1_000_000 : null,
|
||||
})
|
||||
}
|
||||
|
||||
return rows
|
||||
})
|
||||
|
||||
// 总输入上下文(输入 + 缓存创建 + 缓存读取)
|
||||
const _totalInputContext = computed(() => {
|
||||
if (!detail.value) return 0
|
||||
@@ -1013,6 +1108,52 @@ function hasContent(data: unknown): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
function toFiniteNumber(value: unknown): number | null {
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
function getTierCachePriceForTTL(
|
||||
tier: PricingTierLike | null | undefined,
|
||||
ttlMinutes: number,
|
||||
priceKey: 'cache_creation_price_per_1m' | 'cache_read_price_per_1m',
|
||||
): number | null {
|
||||
const fallback = toFiniteNumber(tier?.[priceKey])
|
||||
const ttlPricing = Array.isArray(tier?.cache_ttl_pricing)
|
||||
? tier.cache_ttl_pricing
|
||||
.filter((entry): entry is CacheTTLPriceEntry => !!entry && typeof entry === 'object')
|
||||
.sort((a, b) => Number(a.ttl_minutes || 0) - Number(b.ttl_minutes || 0))
|
||||
: []
|
||||
|
||||
if (ttlPricing.length === 0) return fallback
|
||||
|
||||
const matched = ttlPricing.find((entry) => Number(entry.ttl_minutes || 0) >= ttlMinutes)
|
||||
|| ttlPricing[ttlPricing.length - 1]
|
||||
const price = toFiniteNumber(matched?.[priceKey])
|
||||
return price ?? fallback
|
||||
}
|
||||
|
||||
function hasTierCacheCreationSplitPricing(tier: PricingTierLike | null | undefined): boolean {
|
||||
const ttlPricing = Array.isArray(tier?.cache_ttl_pricing) ? tier.cache_ttl_pricing : []
|
||||
return ttlPricing.some((entry) =>
|
||||
Number(entry?.ttl_minutes || 0) >= 60
|
||||
&& toFiniteNumber(entry?.cache_creation_price_per_1m) !== null,
|
||||
)
|
||||
}
|
||||
|
||||
function getActiveCachePriceForTTL(
|
||||
ttlMinutes: number,
|
||||
priceKey: 'cache_creation_price_per_1m' | 'cache_read_price_per_1m',
|
||||
): number | null {
|
||||
const tierPrice = getTierCachePriceForTTL(currentTier.value, ttlMinutes, priceKey)
|
||||
if (tierPrice !== null) return tierPrice
|
||||
|
||||
if (priceKey === 'cache_creation_price_per_1m') {
|
||||
return toFiniteNumber(detail.value?.cache_creation_price_per_1m)
|
||||
}
|
||||
return toFiniteNumber(detail.value?.cache_read_price_per_1m)
|
||||
}
|
||||
|
||||
function getDefaultDataSourceForTab(tab: string): 'client' | 'provider' {
|
||||
if (!detail.value) {
|
||||
if (['request-headers', 'request-body'].includes(tab)) return 'provider'
|
||||
|
||||
Reference in New Issue
Block a user