diff --git a/src/client/components/overviewPanel.tsx b/src/client/components/overviewPanel.tsx
index fd4149e..8c883d7 100644
--- a/src/client/components/overviewPanel.tsx
+++ b/src/client/components/overviewPanel.tsx
@@ -150,12 +150,12 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
{t('stats.cost')}
{kpi.cost === null ? '—' : formatCost(kpi.cost, currency)}
- {t('ov.kpi.sessionsSub', { n: kpi.sessions })}
+ {t('ov.kpi.sessionsSub', { n: kpi.costSessions })}
{t('stats.cacheHit')}
{kpi.cacheHit === null ? '—' : kpi.cacheHit + '%'}
- {t('ov.kpi.sessionsSub', { n: kpi.sessions })}
+ {t('ov.kpi.sessionsSub', { n: kpi.usageSessions })}
{t('stats.toolCalls')}
diff --git a/src/client/overview.ts b/src/client/overview.ts
index 0a6288a..c588214 100644
--- a/src/client/overview.ts
+++ b/src/client/overview.ts
@@ -392,8 +392,12 @@ export interface OverviewKpis {
turns: number
/** Estimated spend in the display currency (null: nothing priced). */
cost: number | null
+ /** Sessions whose spend the book could price (the cost cell's sub-line). */
+ costSessions: number
/** Cache-hit share of billed input, truncated (null: nothing billed). */
cacheHit: string | null
+ /** Sessions whose usage feeds the cache-hit rate (the cache-hit cell's sub-line). */
+ usageSessions: number
/** Their completed tool calls. */
toolCalls: number
/** Their summed tool-run time (the tool-calls cell's sub-line). */
@@ -417,7 +421,15 @@ export function kpisOf(
let toolsMs = 0
let calls = 0
let wallMs = 0
+ let costSessions = 0
+ let usageSessions = 0
for (const row of rows) {
+ const cost = row.timeline?.cost
+ // Each qualifying sub-line counts the sessions its own figure covers: a
+ // session with usage but no book rates feeds the cache-hit rate while
+ // pricing to nothing.
+ if (estimateSessionCost(cost, prices, currency) !== null) costSessions++
+ if (usageTotalsOf(cost) !== null) usageSessions++
turns += turnsOf(row.timeline)
const timing = row.timeline?.timing
toolCalls += timing?.toolCalls ?? 0
@@ -431,7 +443,9 @@ export function kpisOf(
tokens: totals?.total ?? 0,
turns,
cost: estimateSessionCost(usage, prices, currency),
+ costSessions,
cacheHit: totals === null ? null : cacheHitPercent(totals.cacheRead, totals.input + totals.cacheRead + totals.cacheWrite),
+ usageSessions,
toolCalls,
toolsMs,
calls,
diff --git a/tests/client/components/overviewPanel.spec.ts b/tests/client/components/overviewPanel.spec.ts
index 83a6aa4..678c7b6 100644
--- a/tests/client/components/overviewPanel.spec.ts
+++ b/tests/client/components/overviewPanel.spec.ts
@@ -139,9 +139,10 @@ describe('OverviewPanel', () => {
assert.equal(values[5], '3m0s')
assert.ok(text(m.container).includes('tool runs 40.0s'), 'the tool calls cell qualifies with the summed run time')
assert.ok(text(m.container).includes('8 model calls'), 'the active-time cell qualifies with the model-call count')
- // Cost and cache hit qualify with the session count they aggregate over.
+ // Cost and cache hit qualify with the session count each figure covers
+ // (only session a carries usage; b folds no cost at all).
const subs = queryAll(m.container, '.lc-stat-sub').map(el => el.textContent)
- assert.deepEqual(subs.slice(2, 4), ['across 2 sessions', 'across 2 sessions'])
+ assert.deepEqual(subs.slice(2, 4), ['across 1 sessions', 'across 1 sessions'])
// Heatmap drew cells for the two ledger days.
assert.ok(queryAll(m.container, 'button.lc-heat-cell').length >= 2)
// Cards: a (current, running, grouped), b, and c is outside the 30d range.
diff --git a/tests/client/overview.spec.ts b/tests/client/overview.spec.ts
index fe969af..743a91d 100644
--- a/tests/client/overview.spec.ts
+++ b/tests/client/overview.spec.ts
@@ -348,18 +348,39 @@ describe('kpisOf', () => {
assert.equal(kpi.turns, 5)
assert.ok(kpi.cost !== null && Math.abs(kpi.cost - 195e-6) < 1e-12, '50×0.1 + 100×1 + 10×1 + 40×2 per 1M')
assert.equal(kpi.cacheHit, '31.25', '50 reads of 160 billed input, truncated')
+ assert.equal(kpi.costSessions, 1, 'only the priced session counts toward the cost cell')
+ assert.equal(kpi.usageSessions, 1, 'only the billed session feeds the cache-hit rate')
assert.equal(kpi.toolCalls, 7, 'tool calls sum across rows')
assert.equal(kpi.toolsMs, 20_000)
assert.equal(kpi.calls, 6)
assert.equal(kpi.wallMs, 120_000)
})
+ test('a session with usage the book cannot price feeds the cache-hit rate but prices to nothing', () => {
+ const rows = [
+ rowOf({ timeline: { cost: COST, requests: [] } as unknown as ContextTimeline }),
+ rowOf({
+ timeline: {
+ cost: { openai: { 'gpt-5': { peak: { uncached: 10, cacheRead: 5, cacheWrite: 1, output: 2 } } } },
+ requests: [],
+ } as unknown as ContextTimeline,
+ }),
+ rowOf(),
+ ]
+ const kpi = kpisOf(rows, 3, prices, 'usd')
+ assert.equal(kpi.costSessions, 1, 'only the priced session counts toward the cost cell')
+ assert.equal(kpi.usageSessions, 2, 'both billed sessions feed the cache-hit rate')
+ assert.ok(kpi.cost !== null && Math.abs(kpi.cost - 195e-6) < 1e-12, 'the unpriced session adds nothing to the estimate')
+ })
+
test('an unbilled set zeroes and dashes', () => {
const kpi = kpisOf([rowOf()], 1, null, 'cny')
assert.equal(kpi.tokens, 0)
assert.equal(kpi.turns, 0)
assert.equal(kpi.cost, null)
assert.equal(kpi.cacheHit, null)
+ assert.equal(kpi.costSessions, 0)
+ assert.equal(kpi.usageSessions, 0)
assert.equal(kpi.toolCalls, 0, 'no timing folds to zeroed tools and time')
assert.equal(kpi.toolsMs, 0)
assert.equal(kpi.calls, 0)