fix(monitor): 精简缓存率展示
This commit is contained in:
5 files changed
+47
-202
No files matched your search
@@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { CircleHelp } from '@lucide/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { UsageAggregateDto } from '@/app/resources/usage'
|
||||
import AppTooltip from '@/components/ui/AppTooltip.vue'
|
||||
import { formatEstimatedCost, formatInteger, formatPercent, formatTokens } from '@/lib/format'
|
||||
|
||||
import { formatCacheHitRate } from '@/lib/cache-rate'
|
||||
@@ -42,29 +40,23 @@ function inputTokens(): number {
|
||||
<article class="usage-kpi usage-kpi--accent">
|
||||
<span>{{ t('monitor.usage.kpi.requests') }}</span>
|
||||
<strong>{{ formatInteger(summary.request_count, locale) }}</strong>
|
||||
<small>
|
||||
{{
|
||||
t('monitor.usage.kpi.outcomeSummary', {
|
||||
success: formatInteger(summary.success_count, locale),
|
||||
failure: formatInteger(summary.failure_count, locale),
|
||||
rate: formatPercent(summary.failure_count, summary.request_count, locale),
|
||||
})
|
||||
}}
|
||||
<small class="usage-kpi__outcomes">
|
||||
<span class="usage-kpi__success">
|
||||
{{ t('monitor.usage.columns.success') }}
|
||||
{{ formatInteger(summary.success_count, locale) }}
|
||||
</span>
|
||||
<span class="usage-kpi__separator">/</span>
|
||||
<span class="usage-kpi__failure">
|
||||
{{ t('monitor.usage.columns.failure') }}
|
||||
{{ formatInteger(summary.failure_count, locale) }}
|
||||
</span>
|
||||
<span class="usage-kpi__rate">
|
||||
({{ formatPercent(summary.failure_count, summary.request_count, locale) }})
|
||||
</span>
|
||||
</small>
|
||||
</article>
|
||||
<article class="usage-kpi usage-kpi--cache">
|
||||
<span>
|
||||
{{ t('monitor.usage.kpi.cacheHitRate') }}
|
||||
<AppTooltip :content="t('monitor.usage.kpi.cacheHitRateHelp')" side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="usage-kpi__help"
|
||||
:aria-label="t('monitor.usage.kpi.cacheHitRateHelp')"
|
||||
>
|
||||
<CircleHelp :size="13" aria-hidden="true" />
|
||||
</button>
|
||||
</AppTooltip>
|
||||
</span>
|
||||
<span>{{ t('monitor.usage.kpi.cacheHitRate') }}</span>
|
||||
<strong>
|
||||
{{ formatCacheHitRate(summary.cache_read_tokens, inputTokens(), locale) }}
|
||||
</strong>
|
||||
@@ -173,16 +165,24 @@ function inputTokens(): number {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.usage-kpi__help {
|
||||
display: inline-grid;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
.usage-kpi__outcomes {
|
||||
display: flex !important;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.usage-kpi__success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.usage-kpi__failure {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.usage-kpi__separator,
|
||||
.usage-kpi__rate {
|
||||
color: var(--color-text-faint);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.usage-kpi__quality {
|
||||
|
||||
@@ -24,9 +24,8 @@ import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import InlineFeedback from '@/components/ui/InlineFeedback.vue'
|
||||
import QueryFeedback from '@/components/ui/QueryFeedback.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import { formatEstimatedCost, formatInteger, formatPercent, formatTokens } from '@/lib/format'
|
||||
import { formatEstimatedCost, formatInteger, formatTokens } from '@/lib/format'
|
||||
|
||||
import { cacheHitRate, formatCacheHitRate } from '@/lib/cache-rate'
|
||||
import MonitorSectionHeading from './MonitorSectionHeading.vue'
|
||||
import {
|
||||
applyUsageFilterDraft,
|
||||
@@ -85,41 +84,6 @@ function formatCost(aggregate: UsageAggregateDto): string {
|
||||
return formatEstimatedCost(aggregate.estimated_cost_nano_usd, locale.value)
|
||||
}
|
||||
|
||||
function inputTokens(aggregate: UsageAggregateDto): number {
|
||||
return (
|
||||
aggregate.uncached_input_tokens +
|
||||
aggregate.cache_read_tokens +
|
||||
aggregate.cache_write_5m_tokens +
|
||||
aggregate.cache_write_1h_tokens +
|
||||
aggregate.cache_write_unknown_tokens
|
||||
)
|
||||
}
|
||||
|
||||
function nonHitInputTokens(aggregate: UsageAggregateDto): number {
|
||||
return inputTokens(aggregate) - aggregate.cache_read_tokens
|
||||
}
|
||||
|
||||
function cacheRate(aggregate: UsageAggregateDto): number | null {
|
||||
return cacheHitRate(aggregate.cache_read_tokens, inputTokens(aggregate))
|
||||
}
|
||||
|
||||
function cacheRateLabel(aggregate: UsageAggregateDto): string {
|
||||
return formatCacheHitRate(aggregate.cache_read_tokens, inputTokens(aggregate), locale.value)
|
||||
}
|
||||
|
||||
function nonHitRateLabel(aggregate: UsageAggregateDto): string {
|
||||
const rate = cacheRate(aggregate)
|
||||
if (rate === null) return '—'
|
||||
return new Intl.NumberFormat(locale.value, {
|
||||
style: 'percent',
|
||||
maximumFractionDigits: 1,
|
||||
}).format(1 - rate)
|
||||
}
|
||||
|
||||
function cacheShareWidth(aggregate: UsageAggregateDto): string {
|
||||
return `${(cacheRate(aggregate) ?? 0) * 100}%`
|
||||
}
|
||||
|
||||
function groupName(groupID: number): string {
|
||||
if (groupID === 0) return t('monitor.usage.breakdown.unattributedGroup')
|
||||
return (
|
||||
@@ -262,34 +226,6 @@ defineExpose({ openFilters })
|
||||
:title="t('monitor.usage.tokens.title')"
|
||||
:description="t('monitor.usage.tokens.description')"
|
||||
/>
|
||||
<div class="usage-token-composition">
|
||||
<div class="usage-token-composition__legend">
|
||||
<span>
|
||||
<i class="usage-token-composition__dot usage-token-composition__dot--hit" />
|
||||
{{ t('monitor.usage.tokens.cacheRead') }}
|
||||
<strong>{{ formatTokens(report.summary.cache_read_tokens, locale) }}</strong>
|
||||
<small>{{ cacheRateLabel(report.summary) }}</small>
|
||||
</span>
|
||||
<span>
|
||||
<i class="usage-token-composition__dot" />
|
||||
{{ t('monitor.usage.tokens.nonHitInput') }}
|
||||
<strong>{{ formatTokens(nonHitInputTokens(report.summary), locale) }}</strong>
|
||||
<small>{{ nonHitRateLabel(report.summary) }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="usage-token-composition__track"
|
||||
role="img"
|
||||
:aria-label="
|
||||
t('monitor.usage.tokens.cacheCompositionLabel', {
|
||||
hit: cacheRateLabel(report.summary),
|
||||
other: nonHitRateLabel(report.summary),
|
||||
})
|
||||
"
|
||||
>
|
||||
<span :style="{ width: cacheShareWidth(report.summary) }" />
|
||||
</div>
|
||||
</div>
|
||||
<dl class="usage-token-grid">
|
||||
<div>
|
||||
<dt>{{ t('monitor.usage.tokens.uncachedInput') }}</dt>
|
||||
@@ -433,15 +369,17 @@ defineExpose({ openFilters })
|
||||
<span class="usage-cell-label">{{ t('monitor.usage.columns.quality') }}</span>
|
||||
<span>
|
||||
{{
|
||||
t('monitor.usage.columns.qualitySuccess', {
|
||||
rate: formatPercent(row.success_count, row.request_count, locale),
|
||||
t('monitor.usage.columns.qualityUsage', {
|
||||
missing: formatInteger(row.usage_missing_count, locale),
|
||||
partial: formatInteger(row.partial_count, locale),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<small>
|
||||
{{
|
||||
t('monitor.usage.columns.qualityCache', {
|
||||
rate: cacheRateLabel(row),
|
||||
t('monitor.usage.columns.qualityPricing', {
|
||||
unpriced: formatInteger(row.unpriced_request_count, locale),
|
||||
partial: formatInteger(row.pricing_partial_count, locale),
|
||||
})
|
||||
}}
|
||||
</small>
|
||||
@@ -614,70 +552,6 @@ defineExpose({ openFilters })
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.usage-token-composition {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
padding-inline: 2px;
|
||||
}
|
||||
|
||||
.usage-token-composition__legend {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-label-xs);
|
||||
}
|
||||
|
||||
.usage-token-composition__legend > span {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.usage-token-composition__legend strong,
|
||||
.usage-token-composition__legend small {
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.usage-token-composition__legend strong {
|
||||
color: var(--color-text);
|
||||
font-weight: 560;
|
||||
}
|
||||
|
||||
.usage-token-composition__legend small {
|
||||
color: var(--color-text-faint);
|
||||
}
|
||||
|
||||
.usage-token-composition__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
flex: 0 0 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-faint);
|
||||
}
|
||||
|
||||
.usage-token-composition__dot--hit {
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.usage-token-composition__track {
|
||||
height: 5px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: var(--color-surface-sunken);
|
||||
}
|
||||
|
||||
.usage-token-composition__track > span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.usage-quality-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -734,17 +608,17 @@ defineExpose({ openFilters })
|
||||
}
|
||||
|
||||
.usage-breakdown-record {
|
||||
display: grid;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
.usage-breakdown-record__summary {
|
||||
--ledger-record-list-record-min-height: 72px;
|
||||
--ledger-record-list-record-padding: 12px 0;
|
||||
|
||||
grid-template-columns: subgrid;
|
||||
grid-template-columns: var(--ledger-record-list-grid);
|
||||
column-gap: var(--ledger-record-list-column-gap);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
@@ -842,18 +716,15 @@ defineExpose({ openFilters })
|
||||
|
||||
.usage-breakdown-record__detail {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin: 0 -14px;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
margin: 0;
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
background: var(--color-border-subtle);
|
||||
gap: 1px;
|
||||
gap: 14px 22px;
|
||||
padding: 14px 0 16px;
|
||||
}
|
||||
|
||||
.usage-breakdown-record__detail > div {
|
||||
min-width: 0;
|
||||
background: var(--color-surface-raised);
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.usage-breakdown-record__detail dt {
|
||||
@@ -863,7 +734,7 @@ defineExpose({ openFilters })
|
||||
|
||||
.usage-breakdown-record__detail dd {
|
||||
margin: 4px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -929,6 +800,7 @@ defineExpose({ openFilters })
|
||||
.usage-breakdown-record__summary {
|
||||
--ledger-record-list-card-grid: repeat(2, minmax(0, 1fr));
|
||||
|
||||
grid-template-columns: var(--ledger-record-list-card-grid);
|
||||
position: relative;
|
||||
padding-right: 52px;
|
||||
}
|
||||
@@ -965,12 +837,6 @@ defineExpose({ openFilters })
|
||||
.usage-trend-panel__chart {
|
||||
padding: 14px 12px 10px;
|
||||
}
|
||||
|
||||
.usage-token-composition__legend {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
|
||||
@@ -240,11 +240,8 @@ export default {
|
||||
requests: 'Requests',
|
||||
outcomes: 'Success and failure',
|
||||
outcomeCounts: 'Success {success} · Failure {failure}',
|
||||
outcomeSummary: 'Success {success} / failure {failure} ({rate})',
|
||||
successRate: 'Success rate {rate}',
|
||||
cacheHitRate: 'Cache hit rate',
|
||||
cacheHitRateHelp:
|
||||
'Cache hit rate = cache-read tokens / input tokens (uncached input + cache reads + all cache writes). Output tokens are excluded; only reported usage is measured.',
|
||||
cacheTokenSummary: 'Cache read {read} / input {input}',
|
||||
totalTokens: 'Total reported tokens',
|
||||
persistedWindow: 'Current reporting window',
|
||||
@@ -258,8 +255,6 @@ export default {
|
||||
cacheWrite5m: 'Cache write (5m)',
|
||||
cacheWrite1h: 'Cache write (1h)',
|
||||
cacheWriteUnknown: 'Cache write',
|
||||
nonHitInput: 'Other input',
|
||||
cacheCompositionLabel: 'Cache reads {hit}; other input {other}',
|
||||
output: 'Output',
|
||||
},
|
||||
cost: {
|
||||
@@ -340,8 +335,6 @@ export default {
|
||||
estimatedCost: 'Estimated cost',
|
||||
quality: 'Quality',
|
||||
actions: 'Actions',
|
||||
qualitySuccess: 'Success rate {rate}',
|
||||
qualityCache: 'Cache hit rate {rate}',
|
||||
qualityUsage: 'Missing {missing} · Partial {partial}',
|
||||
qualityPricing: 'Unpriced {unpriced} · Partial {partial}',
|
||||
qualityCompact:
|
||||
|
||||
@@ -241,11 +241,8 @@ export default {
|
||||
requests: 'リクエスト数',
|
||||
outcomes: '成功と失敗',
|
||||
outcomeCounts: '成功 {success} · 失敗 {failure}',
|
||||
outcomeSummary: '成功 {success} / 失敗 {failure}({rate})',
|
||||
successRate: '成功率 {rate}',
|
||||
cacheHitRate: 'キャッシュ率',
|
||||
cacheHitRateHelp:
|
||||
'キャッシュ率 = キャッシュ読み取り ÷ 入力 Token(キャッシュなし入力 + キャッシュ読み取り + すべてのキャッシュ書き込み)。出力 Token は含まず、報告済み使用量のみを対象とします。',
|
||||
cacheTokenSummary: 'キャッシュ読み取り {read} / 入力 {input}',
|
||||
totalTokens: '報告済みトークン合計',
|
||||
persistedWindow: '現在の集計期間',
|
||||
@@ -259,8 +256,6 @@ export default {
|
||||
cacheWrite5m: 'キャッシュ書き込み(5 分)',
|
||||
cacheWrite1h: 'キャッシュ書き込み(1 時間)',
|
||||
cacheWriteUnknown: 'キャッシュ書き込み',
|
||||
nonHitInput: 'その他の入力',
|
||||
cacheCompositionLabel: 'キャッシュ読み取り {hit}、その他の入力 {other}',
|
||||
output: '出力',
|
||||
},
|
||||
cost: {
|
||||
@@ -341,8 +336,6 @@ export default {
|
||||
estimatedCost: '推定コスト',
|
||||
quality: '品質',
|
||||
actions: '操作',
|
||||
qualitySuccess: '成功率 {rate}',
|
||||
qualityCache: 'キャッシュ率 {rate}',
|
||||
qualityUsage: 'なし {missing} · 部分 {partial}',
|
||||
qualityPricing: '未設定 {unpriced} · 一部 {partial}',
|
||||
qualityCompact:
|
||||
|
||||
@@ -234,11 +234,8 @@ export default {
|
||||
requests: '请求数',
|
||||
outcomes: '成功与失败',
|
||||
outcomeCounts: '成功 {success} · 失败 {failure}',
|
||||
outcomeSummary: '成功 {success} / 失败 {failure}({rate})',
|
||||
successRate: '成功率 {rate}',
|
||||
cacheHitRate: '缓存率',
|
||||
cacheHitRateHelp:
|
||||
'缓存率 = 缓存读取 ÷ 输入 Token(非缓存输入 + 缓存读取 + 各类缓存写入),不含输出 Token;仅基于已报告用量。',
|
||||
cacheTokenSummary: '缓存读取 {read} / 输入 {input}',
|
||||
totalTokens: '已报告 Token 总数',
|
||||
persistedWindow: '当前统计窗口',
|
||||
@@ -252,8 +249,6 @@ export default {
|
||||
cacheWrite5m: '缓存写入(5 分钟)',
|
||||
cacheWrite1h: '缓存写入(1 小时)',
|
||||
cacheWriteUnknown: '缓存写入',
|
||||
nonHitInput: '其他输入',
|
||||
cacheCompositionLabel: '缓存读取占 {hit},其他输入占 {other}',
|
||||
output: '输出',
|
||||
},
|
||||
cost: {
|
||||
@@ -328,8 +323,6 @@ export default {
|
||||
estimatedCost: '预估成本',
|
||||
quality: '质量',
|
||||
actions: '操作',
|
||||
qualitySuccess: '成功率 {rate}',
|
||||
qualityCache: '缓存率 {rate}',
|
||||
qualityUsage: '缺失 {missing} · 部分 {partial}',
|
||||
qualityPricing: '未定价 {unpriced} · 部分定价 {partial}',
|
||||
qualityCompact:
|
||||
|
||||
Reference in new issue
Block a user