refactor(stats): drop the cache-hit cell from the Context Stats card
The token card's corner already carries the session's cache-hit share
(ef72a7b), so the Context Stats board duplicated it. The grid is now six
cells: turns, steps, human inputs, live tool calls, and the two cost cells.
The removal takes its dead plumbing with it — the card drops its `usage`
prop, and `cacheHitPercent` loses the `decimals` override that existed only
for this cell's one-decimal display. The README rows and the card's
screenshot move the figure to the token card.
This commit is contained in:
1 parent
ef72a7bbb2
commit
984f31c954
10 files changed
+63
-124
No files matched your search
@@ -73,8 +73,8 @@ Open any session and click the **Context / 上下文** tab:
|
||||
|
||||
| Card | The question it answers |
|
||||
| --- | --- |
|
||||
| **Context Stats** | Turns, steps, human inputs, live tool calls, the session's cache-hit rate — plus a cost estimated from the models.dev list prices (hover the `?` for per-1M rates; DeepSeek peak/off-peak aware). |
|
||||
| **Token Stats** | Where the billed tokens went — the same total as the chat stats line under the composer, split by composition (system, tools, messages…) with the provider-exact output closing the ring. |
|
||||
| **Context Stats** | Turns, steps, human inputs, live tool calls — plus a cost estimated from the models.dev list prices (hover the `?` for per-1M rates; DeepSeek peak/off-peak aware). |
|
||||
| **Token Stats** | Where the billed tokens went — the same total as the chat stats line under the composer, split by composition (system, tools, messages…) with the provider-exact output closing the ring, and the session's cache-hit rate in the corner. |
|
||||
| **Timing Stats** | How active time split across model calls, tool runs, and overhead. |
|
||||
| **Current Context** | What's in the window *right now*. |
|
||||
| **Context Trend** | Every request's size — and its story. |
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 19 KiB |
@@ -94,7 +94,7 @@ export function makeContextView(
|
||||
// no longer mirrors it. Absent → derived fallback.
|
||||
const pressure = projectionOf(props, 'contextPressure', contextPressureOf)
|
||||
// Official token-meter `tokenUsage` projection — the same data the chat stats line below the input box reads for its '缓存命中' figure, so
|
||||
// the stats board's cache-hit cell reuses it verbatim; absent → the cell drops to a dash instead of estimating.
|
||||
// the Token card's figures reuse it verbatim; absent → the card drops them instead of estimating.
|
||||
const usage = projectionOf(props, 'tokenUsage', tokenUsageOf)
|
||||
// Official token-meter `contextBreakdown` projection — the exact rows the chat ring's click-open panel shows, so the overview legend
|
||||
// reads identically by construction; absent → the fold's own same-estimator sums inside headlineOf.
|
||||
@@ -591,7 +591,7 @@ export function makeContextView(
|
||||
pair in a narrow pane at the cards' min-width floors. */}
|
||||
{inSidebar ? null : (
|
||||
<div className="lc-cols lc-head">
|
||||
<StatsContext counts={counts} humanInputs={data.humanInputs} toolCalls={data.toolCalls} usage={usage}
|
||||
<StatsContext counts={counts} humanInputs={data.humanInputs} toolCalls={data.toolCalls}
|
||||
cost={data.cost} locale={activeLocale} sessionId={typeof sessionId === 'string' ? sessionId : undefined} />
|
||||
<PluginInfo />
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
/**
|
||||
* The Context card: what the session's context IS and how it evolved — a
|
||||
* seven-cell grid of the session's shape (turns / steps / human inputs /
|
||||
* live tool calls), the whole-session cache-hit rate, and the cost estimate
|
||||
* at two scopes: the family total (the current agent plus every subagent
|
||||
* session) and the subagents' own share.
|
||||
* six-cell grid of the session's shape (turns / steps / human inputs /
|
||||
* live tool calls) and the cost estimate at two scopes: the family total
|
||||
* (the current agent plus every subagent session) and the subagents' own
|
||||
* share.
|
||||
* Count figures only: nothing here is part of a spendable whole, so no pie —
|
||||
* proportions live in the composition card, and the context-event tallies
|
||||
* live on the events card's kind filters (contextView.tsx). The cache-hit
|
||||
* cell reads the official `tokenUsage` projection — the same source and
|
||||
* formula as the harness chat stats line under the composer, shown with one
|
||||
* decimal — and dashes until a provider reports usage. The cost cells price
|
||||
* the host-folded cumulative billed totals (complete session logs, never
|
||||
* proportions live in the composition card, the context-event tallies live
|
||||
* on the events card's kind filters (contextView.tsx), and the token figures
|
||||
* (the session's cache-hit share among them) live on the Token card. The
|
||||
* cost cells price the host-folded cumulative billed totals (complete
|
||||
* session logs, never
|
||||
* trimmed; the subagents' usage folds out of the session-list snapshot,
|
||||
* `makeSubagentCost` below) from the models.dev price book (modelPrices.ts)
|
||||
* in the locale's currency; their hover bubbles (a '?' marker + styled DOM
|
||||
@@ -26,15 +25,14 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState, type ReactElement, type ReactNode } from 'react'
|
||||
import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage, TimelineCounts, TokenUsage } from '../../shared/types'
|
||||
import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage, TimelineCounts } from '../../shared/types'
|
||||
import { estimateSessionCost, formatCost, formatPriceRate, mergeCostUsage, priceFaceOf, toCurrency } from '../cost'
|
||||
import type { CostCurrency, ModelBook, PriceFace } from '../cost'
|
||||
import { sessionsFaceOf, subagentCostFoldOf } from '../agentTree'
|
||||
import type { AgentHeads } from '../agentHeads'
|
||||
import { useSessionsSnapshot } from '../agentHeads'
|
||||
import { cacheHitPercent } from '../format'
|
||||
import { useModelPrices } from '../modelPrices'
|
||||
import { asRecord, numOf, type ClientCtx } from '../services'
|
||||
import { asRecord, type ClientCtx } from '../services'
|
||||
import { isDeepSeekProvider } from '../../shared/providers'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
@@ -139,8 +137,6 @@ export function makeStatsContext(
|
||||
humanInputs?: number
|
||||
/** Tool calls with a result live in the current context (absent on older hosts). */
|
||||
toolCalls?: number
|
||||
/** The official tokenUsage projection — the cache-hit cell's source (null until a provider reports). */
|
||||
usage: TokenUsage | null
|
||||
cost?: SessionCostUsage
|
||||
locale: string
|
||||
/** The current session id, anchoring the subagent-cost fold (absent = nothing to fold). */
|
||||
@@ -151,7 +147,6 @@ export function makeStatsContext(
|
||||
counts: TimelineCounts
|
||||
humanInputs?: number
|
||||
toolCalls?: number
|
||||
usage: TokenUsage | null
|
||||
cost?: SessionCostUsage
|
||||
locale: string
|
||||
sessionId?: string
|
||||
@@ -223,14 +218,6 @@ export function makeStatsContext(
|
||||
...notes(subDeepseek),
|
||||
subUnpriced ? <span key="unavailable">{t('stats.costUnavailable')}</span> : null,
|
||||
]
|
||||
// The harness chat stats line's own formula, shown two decimals deep:
|
||||
// prompt-side cache reads over the whole billed input (output excluded),
|
||||
// dashed until reported.
|
||||
const hit = props.usage === null ? null
|
||||
: cacheHitPercent(
|
||||
numOf(props.usage.cacheReadTokens),
|
||||
numOf(props.usage.uncachedInputTokens) + numOf(props.usage.cacheReadTokens) + numOf(props.usage.cacheWriteTokens),
|
||||
)
|
||||
const cell = (label: string, value: string | number, tip?: ReactNode, href?: string): ReactElement => {
|
||||
// The framed cell body, as a div — or as an anchor opening the models.dev
|
||||
// provider listing in a new tab when the caller hands a destination (the
|
||||
@@ -269,7 +256,6 @@ export function makeStatsContext(
|
||||
{cell(t('stats.steps'), props.counts.steps)}
|
||||
{cell(t('stats.humanInputs'), props.humanInputs ?? 0, t('stats.humanInputsTip'))}
|
||||
{cell(t('stats.toolCalls'), props.toolCalls ?? 0)}
|
||||
{cell(t('stats.cacheHit'), hit === null ? '—' : `${hit}%`, t('stats.cacheHitTip'))}
|
||||
{cell(t('stats.cost'), cost === null ? '—' : formatCost(cost, currency), costTip, costHref)}
|
||||
{cell(t('stats.subCost'), subCost === null ? '—' : formatCost(subCost, currency), subTip)}
|
||||
</div>
|
||||
|
||||
@@ -20,17 +20,15 @@ export function fmtBytes(n: number | null | undefined): string {
|
||||
|
||||
/**
|
||||
* Cache-hit share of billed prompt-side input (`reads` over `billed`),
|
||||
* TRUNCATED to `decimals` places (cut, not round) — same formula as the
|
||||
* harness chat stats line's '缓存命中' figure and the stats board's cell
|
||||
* (which shows two decimals). Null when nothing was billed. The 1e-9 epsilon
|
||||
* absorbs only float noise (integer token counts never sit that close to a
|
||||
* boundary).
|
||||
* TRUNCATED to two decimals (cut, not round) — same formula as the harness
|
||||
* chat stats line's '缓存命中' figure and the token card's corner. Null when
|
||||
* nothing was billed. The 1e-9 epsilon absorbs only float noise (integer
|
||||
* token counts never sit that close to a boundary).
|
||||
*/
|
||||
export function cacheHitPercent(reads: number, billed: number, decimals = 2): string | null {
|
||||
export function cacheHitPercent(reads: number, billed: number): string | null {
|
||||
if (!(billed > 0)) return null
|
||||
const factor = 10 ** decimals
|
||||
const scaled = Math.trunc((reads / billed) * 100 * factor + 1e-9)
|
||||
return `${Math.floor(scaled / factor)}.${String(scaled % factor).padStart(decimals, '0')}`
|
||||
const scaled = Math.trunc((reads / billed) * 100 * 100 + 1e-9)
|
||||
return `${Math.floor(scaled / 100)}.${String(scaled % 100).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** One shared formatter: building an Intl formatter per call costs ~50x the format itself (issue #116). */
|
||||
|
||||
@@ -545,10 +545,10 @@ export function contextBreakdownOf(value: unknown): ContextBreakdown | null {
|
||||
* Narrow a delivered projection value to the official token-meter
|
||||
* `tokenUsage` projection (durable cumulative provider usage). Absent key or
|
||||
* value = the meter's projection is not composed (or no request has reported
|
||||
* usage yet) — callers drop the cache-hit cell to a dash. The wire schema is
|
||||
* strict with all four buckets REQUIRED (dsh token-meter's projectionSchema),
|
||||
* so a partial/corrupt value degrades the whole value to null instead of
|
||||
* undercounting the billed total.
|
||||
* usage yet) — callers drop the affected figures instead of estimating. The
|
||||
* wire schema is strict with all four buckets REQUIRED (dsh token-meter's
|
||||
* projectionSchema), so a partial/corrupt value degrades the whole value to
|
||||
* null instead of undercounting the billed total.
|
||||
*/
|
||||
export function tokenUsageOf(value: unknown): TokenUsage | null {
|
||||
const data = asRecord(value)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
card it visually is, with the pointer as the only affordance. */
|
||||
a.lc-stat { color: inherit; text-decoration: none; cursor: pointer; }
|
||||
a.lc-stat:hover { border-color: var(--dsw-alias-label-secondary); }
|
||||
/* The 5-cell stats grid shares a half-width head-row card with a sibling, so its cells are moderate: tighter padding plus a corner-pinned '?' (out of the label's line) keep the longest labels on one line. */
|
||||
/* The stats grid shares a half-width head-row card with a sibling, so its cells are moderate: tighter padding plus a corner-pinned '?' (out of the label's line) keep the longest labels on one line. */
|
||||
.lc-stats .lc-stat { padding: 6px 3px; }
|
||||
.lc-stats .lc-stat-q { position: absolute; top: 3px; right: 4px; margin: 0; }
|
||||
.lc-stat-label { color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
@@ -195,11 +195,11 @@ describe('ContextView — projection guards', () => {
|
||||
assert.ok(text(m.container).includes(DICT_EN['overview.used']))
|
||||
// The Token card's center is the chat stats line's whole-session billed
|
||||
// total off the official tokenUsage projection (100 + 200 + 0 + 50), and
|
||||
// the Context card's cache-hit cell shows the line's own rate two decimals
|
||||
// deep (200 / 300, truncated).
|
||||
// its corner shows the line's own cache-hit rate two decimals deep
|
||||
// (200 / 300, truncated).
|
||||
const tokensCard = queryAll(m.container, '.lc-head > .lc-col-donut')[0]
|
||||
assert.equal(query(tokensCard, '.lc-donut-center b').textContent, '350')
|
||||
assert.ok(text(m.container).includes('66.66%'))
|
||||
assert.equal(query(tokensCard, '.lc-card-rate').textContent, 'Cache Hit 66.66%')
|
||||
assert.ok(text(m.container).includes('m-only'))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// StatsContext (src/client/components/statsContext.tsx) rendered with real
|
||||
// React: the seven-cell grid — session shape with the whole-session
|
||||
// human-input tally, the chat-line cache-hit cell with its whole-session
|
||||
// share tip, the family-scope priced cost cell (current agent + subagents)
|
||||
// with its per-model rate tooltip, and the subagents' own share — in both
|
||||
// locales, against an injected model-price
|
||||
// React: the six-cell grid — session shape with the whole-session
|
||||
// human-input tally, the family-scope priced cost cell (current agent +
|
||||
// subagents) with its per-model rate tooltip, and the subagents' own share —
|
||||
// in both locales, against an injected model-price
|
||||
// book (the store never reaches the network). The context-event tallies live
|
||||
// on the events card's kind filters (contextView.spec.ts); `countsOfRecords`
|
||||
// still derives every count the split generation's wire head carries, pinned
|
||||
@@ -15,7 +14,7 @@ import { afterEach, describe, test, vi, beforeEach } from 'vitest'
|
||||
import { countsOfRecords, makeStatsContext, makeSubagentCost } from '../../../src/client/components/statsContext'
|
||||
import { makeAgentHeads } from '../../../src/client/agentHeads'
|
||||
import { resetModelPrices, setModelPricesLoader } from '../../../src/client/modelPrices'
|
||||
import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage, TokenUsage } from '../../../src/shared/types'
|
||||
import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage } from '../../../src/shared/types'
|
||||
import { TestClientCtx, asClientCtx } from '../helpers/harness'
|
||||
import { flush, makeKit, mount, queryAll, text } from '../helpers/kit'
|
||||
|
||||
@@ -34,8 +33,6 @@ const PROVIDERS = {
|
||||
const COST: SessionCostUsage = {
|
||||
'deepseek-official': { 'deepseek-v4-flash': { peak: { uncached: 1_000_000, cacheRead: 0, cacheWrite: 0, output: 0 } } },
|
||||
}
|
||||
// Prompt-side billed input 300 (100 uncached + 200 read) → hit 66.66% truncated.
|
||||
const USAGE: TokenUsage = { uncachedInputTokens: 100, outputTokens: 50, cacheReadTokens: 200, cacheWriteTokens: 0 }
|
||||
|
||||
function req(turn?: number): RequestRecord {
|
||||
return {
|
||||
@@ -82,60 +79,46 @@ describe('countsOfRecords (the inline generation derivation)', () => {
|
||||
})
|
||||
|
||||
describe('StatsContext', () => {
|
||||
test('folds the seven-cell grid: shape stats, the cache-hit cell, and the two cost cells', async () => {
|
||||
test('folds the six-cell grid: the shape stats and the two cost cells', async () => {
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 3, steps: 4, injects: 3, compactions: 2, prunes: 1 },
|
||||
humanInputs: 7,
|
||||
toolCalls: 3,
|
||||
usage: USAGE,
|
||||
cost: COST,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
assert.ok(text(m.container).includes('Context Stats'))
|
||||
const { labels, values } = cells(m.container)
|
||||
assert.equal(labels.length, 7)
|
||||
assert.deepEqual(labels, ['Turns', 'Steps', 'Human Inputs?', 'Tool Calls', 'Cache Hit?', 'Cost?', 'Subagent Cost?'])
|
||||
assert.equal(labels.length, 6)
|
||||
assert.deepEqual(labels, ['Turns', 'Steps', 'Human Inputs?', 'Tool Calls', 'Cost?', 'Subagent Cost?'])
|
||||
// 1M uncached input at the doubled peak miss rate (2 × $0.15); no subagent usage → the sub cell dashes.
|
||||
assert.deepEqual(values, ['3', '4', '7', '3', '66.66%', '$0.30', '—'])
|
||||
assert.deepEqual(values, ['3', '4', '7', '3', '$0.30', '—'])
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('absent counters, usage, and cost degrade to zeros and the dash', async () => {
|
||||
test('absent counters and cost degrade to zeros and the dash', async () => {
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
assert.deepEqual(cells(m.container).values, ['0', '0', '0', '0', '—', '—', '—'])
|
||||
assert.deepEqual(cells(m.container).values, ['0', '0', '0', '0', '—', '—'])
|
||||
await m.unmount()
|
||||
// A usage report with nothing billed prompt-side dashes the hit too.
|
||||
const zero: TokenUsage = { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
|
||||
const m2 = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: zero,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
assert.deepEqual(cells(m2.container).values, ['0', '0', '0', '0', '—', '—', '—'])
|
||||
await m2.unmount()
|
||||
})
|
||||
|
||||
test('the cost bubble lists the billed models with their billed rates', async () => {
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
cost: COST,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
assert.equal(queryAll(m.container, '.lc-stat-tip').length, 4)
|
||||
assert.equal(queryAll(m.container, '.lc-stat-q').length, 4)
|
||||
assert.equal(queryAll(m.container, '.lc-stat-tip').length, 3)
|
||||
assert.equal(queryAll(m.container, '.lc-stat-q').length, 3)
|
||||
const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el))
|
||||
assert.ok(tips[0].includes('question answerings'), 'the human-inputs tip explains its tally')
|
||||
assert.ok(tips[1].includes('Cumulative cache-read'), 'the cache-hit tip names the whole-session share')
|
||||
const costTip = tips[2]
|
||||
const costTip = tips[1]
|
||||
assert.ok(costTip.includes('this agent and all its subagents'), 'the cost tip names the family scope')
|
||||
assert.ok(costTip.includes('Per-1M-token rates:'))
|
||||
assert.ok(costTip.includes('Priced as listed on models.dev for deepseek · deepseek-v4-flash.'), 'the listing line names the resolved registry face')
|
||||
@@ -146,19 +129,18 @@ describe('StatsContext', () => {
|
||||
assert.ok(costTip.includes('Output $0.60'))
|
||||
assert.ok(costTip.includes('peak windows'), 'a DeepSeek session explains the peak/off-peak scheme')
|
||||
assert.ok(!costTip.includes('|'), 'no peak|off pairs — the footnotes carry the scheme')
|
||||
assert.ok(tips[3].includes('every subagent session'), 'the sub cell explains its own scope')
|
||||
assert.ok(tips[2].includes('every subagent session'), 'the sub cell explains its own scope')
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('a non-DeepSeek session never sees DeepSeek-specific notes', async () => {
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
cost: { 'zai-coding-cn': { 'glm-5.3-flash': { peak: { uncached: 1_000_000, cacheRead: 0, cacheWrite: 0, output: 0 } } } },
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
|
||||
assert.ok(costTip.includes('Priced as listed on models.dev for zhipuai · glm-5.3-flash.'))
|
||||
assert.ok(!costTip.includes('DeepSeek'), 'the DeepSeek scheme note stays out of other providers’ bubbles')
|
||||
// glm-5.3-flash lists cache_write at 0 — the zero band drops from the table.
|
||||
@@ -170,7 +152,6 @@ describe('StatsContext', () => {
|
||||
test('the cost cell links to the models.dev provider listing when one provider priced the scope', async () => {
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
cost: COST,
|
||||
locale: 'en',
|
||||
}))
|
||||
@@ -191,7 +172,6 @@ describe('StatsContext', () => {
|
||||
}
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
cost: two,
|
||||
locale: 'en',
|
||||
}))
|
||||
@@ -207,12 +187,11 @@ describe('StatsContext', () => {
|
||||
}
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
cost: two,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
|
||||
assert.ok(costTip.includes('for deepseek · deepseek-v4-flash.'))
|
||||
assert.ok(costTip.includes('for zhipuai · glm-5.3-flash.'))
|
||||
// 1M × $0.15 × 2 (the DeepSeek peak) + 2M × $0.075 = $0.45.
|
||||
@@ -223,18 +202,16 @@ describe('StatsContext', () => {
|
||||
test('the zh locale localizes labels and prices the cost in CNY at 1 CNY = 0.15 USD', async () => {
|
||||
const m = await mount(h(StatsContextZh, {
|
||||
counts: { turns: 1, steps: 1, injects: 0, compactions: 1, prunes: 0 },
|
||||
usage: USAGE,
|
||||
cost: COST,
|
||||
locale: 'zh',
|
||||
}))
|
||||
await flush()
|
||||
assert.ok(text(m.container).includes('上下文统计'))
|
||||
const { labels, values } = cells(m.container)
|
||||
assert.deepEqual(labels, ['轮次', '步数', '用户输入?', '工具调用', '缓存命中?', '费用?', '子 Agent 费用?'])
|
||||
assert.deepEqual(labels, ['轮次', '步数', '用户输入?', '工具调用', '费用?', '子 Agent 费用?'])
|
||||
// $0.30 / 0.15 = ¥2; the rates convert through the same fixed rate.
|
||||
assert.deepEqual(values, ['1', '1', '0', '0', '66.66%', '¥2.00', '—'])
|
||||
assert.ok(text(queryAll(m.container, '.lc-stat-tip')[1]).includes('整个会话累计'), 'the cache-hit tip localizes too')
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
|
||||
assert.deepEqual(values, ['1', '1', '0', '0', '¥2.00', '—'])
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
|
||||
assert.ok(costTip.includes('含当前智能体及所有子智能体'), 'the cost tip names the family scope too')
|
||||
assert.ok(costTip.includes('每百万 Token 价格'))
|
||||
assert.ok(costTip.includes('按 deepseek · deepseek-v4-flash 在 models.dev 的刊登价格如上。'))
|
||||
@@ -244,15 +221,14 @@ describe('StatsContext', () => {
|
||||
assert.ok(costTip.includes('未缓存输入 ¥1.00'))
|
||||
assert.ok(costTip.includes('缓存写入 ¥1.00'))
|
||||
assert.ok(costTip.includes('输出 ¥4.00'))
|
||||
assert.ok(text(queryAll(m.container, '.lc-stat-tip')[3]).includes('子 Agent 会话'), 'the sub tip localizes too')
|
||||
assert.ok(text(queryAll(m.container, '.lc-stat-tip')[2]).includes('子 Agent 会话'), 'the sub tip localizes too')
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('usage with no book yet stays a dash until the fetch lands', async () => {
|
||||
test('a book that has not landed yet keeps the cost cell dashed', async () => {
|
||||
setModelPricesLoader(() => new Promise(() => {}))
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: USAGE,
|
||||
cost: COST,
|
||||
locale: 'en',
|
||||
}))
|
||||
@@ -266,13 +242,12 @@ describe('StatsContext', () => {
|
||||
setModelPricesLoader(() => Promise.reject(new Error('down')))
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: USAGE,
|
||||
cost: COST,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
assert.ok(cells(m.container).values.at(-2) === '—')
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
|
||||
assert.ok(costTip.includes('unavailable'))
|
||||
assert.ok(!costTip.includes('Per-1M-token rates'))
|
||||
await m.unmount()
|
||||
@@ -289,14 +264,13 @@ describe('StatsContext', () => {
|
||||
}
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
cost: split,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
// 1M at the doubled $0.3 peak miss rate + 2M at the $0.15 off-peak (book) rate.
|
||||
assert.ok(cells(m.container).values.at(-2) === '$0.60')
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
|
||||
assert.ok(costTip.includes('Cache input $0.00'), 'the table lists the book rates once, whatever the billed buckets')
|
||||
assert.ok(costTip.includes('Uncached input $0.15'))
|
||||
assert.ok(costTip.includes('peak windows'), 'the peak-window footnote explains the bucket split')
|
||||
@@ -307,13 +281,12 @@ describe('StatsContext', () => {
|
||||
test('a session whose models the book cannot price notes the outage too', async () => {
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: USAGE,
|
||||
cost: { 'future-provider': { 'mystery-model': { peak: { uncached: 1, cacheRead: 0, cacheWrite: 0, output: 0 } } } },
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
assert.ok(cells(m.container).values.at(-2) === '—')
|
||||
assert.ok(text(queryAll(m.container, '.lc-stat-tip')[2]).includes('unavailable'))
|
||||
assert.ok(text(queryAll(m.container, '.lc-stat-tip')[1]).includes('unavailable'))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
@@ -329,12 +302,11 @@ describe('StatsContext', () => {
|
||||
} as unknown as SessionCostUsage
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: USAGE,
|
||||
cost: hostile,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
|
||||
const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
|
||||
assert.ok(costTip.includes('glm-5.3-flash'))
|
||||
assert.ok(!costTip.includes('junk'))
|
||||
// No row may show the peak|off-peak pair — that is DeepSeek's alone.
|
||||
@@ -355,7 +327,6 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
|
||||
const Stats = makeStatsContext(kit, () => SUB)
|
||||
const m = await mount(h(Stats, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
cost: COST,
|
||||
locale: 'en',
|
||||
}))
|
||||
@@ -364,10 +335,10 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
|
||||
assert.deepEqual(cells(m.container).values.slice(-2), ['$0.45', '$0.15'])
|
||||
const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el))
|
||||
// The rate table covers BOTH sides' models in either tip.
|
||||
assert.ok(tips[2].includes('for deepseek · deepseek-v4-flash.'))
|
||||
assert.ok(tips[2].includes('for zhipuai · glm-5.3-flash.'))
|
||||
assert.ok(tips[3].includes('every subagent session'))
|
||||
assert.ok(tips[3].includes('for zhipuai · glm-5.3-flash.'), 'the sub tip carries its own price table')
|
||||
assert.ok(tips[1].includes('for deepseek · deepseek-v4-flash.'))
|
||||
assert.ok(tips[1].includes('for zhipuai · glm-5.3-flash.'))
|
||||
assert.ok(tips[2].includes('every subagent session'))
|
||||
assert.ok(tips[2].includes('for zhipuai · glm-5.3-flash.'), 'the sub tip carries its own price table')
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
@@ -375,7 +346,6 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
|
||||
const Stats = makeStatsContext(kit, () => SUB)
|
||||
const m = await mount(h(Stats, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
@@ -390,15 +360,14 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
|
||||
const Stats = makeStatsContext(kit, () => deepseekSub)
|
||||
const m = await mount(h(Stats, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
// The off bucket bills at book: 1M × $0.15.
|
||||
assert.deepEqual(cells(m.container).values.slice(-2), ['$0.15', '$0.15'])
|
||||
const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el))
|
||||
assert.ok(tips[2].includes('peak windows'))
|
||||
assert.ok(tips[3].includes('peak windows'), 'the sub tip explains the scheme its own figure rides')
|
||||
assert.ok(tips[1].includes('peak windows'))
|
||||
assert.ok(tips[2].includes('peak windows'), 'the sub tip explains the scheme its own figure rides')
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
@@ -406,7 +375,6 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
|
||||
const Stats = makeStatsContext(kit, () => ({ future: { 'mystery-model': { peak: { uncached: 1, cacheRead: 0, cacheWrite: 0, output: 0 } } } }))
|
||||
const m = await mount(h(Stats, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
cost: COST,
|
||||
locale: 'en',
|
||||
}))
|
||||
@@ -416,14 +384,13 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
|
||||
// total — the sub cell dashes.
|
||||
assert.deepEqual(cells(m.container).values.slice(-2), ['$0.30', '—'])
|
||||
const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el))
|
||||
assert.ok(tips[3].includes('unavailable'))
|
||||
assert.ok(tips[2].includes('unavailable'))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('a null seat keeps both cells dashed with no outage notes', async () => {
|
||||
const m = await mount(h(StatsContext, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
@@ -498,7 +465,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
|
||||
const Stats = makeStatsContext(kit, seat)
|
||||
const m = await mount(h(Stats, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
locale: 'en',
|
||||
sessionId: 'root',
|
||||
}))
|
||||
@@ -552,7 +518,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
|
||||
const Stats = makeStatsContext(kit, seat)
|
||||
const m = await mount(h(Stats, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
locale: 'en',
|
||||
sessionId: 'root',
|
||||
}))
|
||||
@@ -588,7 +553,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
|
||||
const Stats = makeStatsContext(kit, seat)
|
||||
const m = await mount(h(Stats, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
locale: 'en',
|
||||
sessionId: 'root',
|
||||
}))
|
||||
@@ -614,7 +578,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
|
||||
const Stats = makeStatsContext(kit, makeSubagentCost(asClientCtx(ctx), makeAgentHeads(asClientCtx(ctx))))
|
||||
const m = await mount(h(Stats, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
locale: 'en',
|
||||
sessionId: 'root',
|
||||
}))
|
||||
@@ -634,7 +597,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
|
||||
const Stats = makeStatsContext(kit, makeSubagentCost(asClientCtx(ctx), makeAgentHeads(asClientCtx(ctx))))
|
||||
const m = await mount(h(Stats, {
|
||||
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
|
||||
usage: null,
|
||||
locale: 'en',
|
||||
}))
|
||||
await flush()
|
||||
|
||||
@@ -60,13 +60,6 @@ describe('cacheHitPercent', () => {
|
||||
assert.equal(cacheHitPercent(82499, 83000), '99.39')
|
||||
assert.equal(cacheHitPercent(83000, 83000), '100.00')
|
||||
})
|
||||
|
||||
test('the stats board cell truncates to one decimal by the same formula', () => {
|
||||
assert.equal(cacheHitPercent(2, 3, 1), '66.6')
|
||||
assert.equal(cacheHitPercent(1, 3, 1), '33.3')
|
||||
assert.equal(cacheHitPercent(82499, 83000, 1), '99.3')
|
||||
assert.equal(cacheHitPercent(83000, 83000, 1), '100.0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fmtTime', () => {
|
||||
|
||||
Reference in new issue
Block a user