fix(client): count only the sessions each KPI figure covers
The cost cell's sub-line now counts the sessions whose spend the price book could estimate (costSessions), and the cache-hit cell's the sessions whose usage feeds the rate (usageSessions) — a session with usage but no book rates feeds the rate while pricing to nothing, and a session without usage counts toward neither figure.
This commit is contained in:
1 parent
0d34b468fb
commit
0003ceabaa
4 files changed
+40
-4
No files matched your search
@@ -150,12 +150,12 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
|
||||
<div className="lc-stat lc-ov-kpi">
|
||||
<span className="lc-stat-label">{t('stats.cost')}</span>
|
||||
<span className="lc-stat-value">{kpi.cost === null ? '—' : formatCost(kpi.cost, currency)}</span>
|
||||
<span className="lc-stat-sub">{t('ov.kpi.sessionsSub', { n: kpi.sessions })}</span>
|
||||
<span className="lc-stat-sub">{t('ov.kpi.sessionsSub', { n: kpi.costSessions })}</span>
|
||||
</div>
|
||||
<div className="lc-stat lc-ov-kpi">
|
||||
<span className="lc-stat-label">{t('stats.cacheHit')}</span>
|
||||
<span className="lc-stat-value">{kpi.cacheHit === null ? '—' : kpi.cacheHit + '%'}</span>
|
||||
<span className="lc-stat-sub">{t('ov.kpi.sessionsSub', { n: kpi.sessions })}</span>
|
||||
<span className="lc-stat-sub">{t('ov.kpi.sessionsSub', { n: kpi.usageSessions })}</span>
|
||||
</div>
|
||||
<div className="lc-stat lc-ov-kpi">
|
||||
<span className="lc-stat-label">{t('stats.toolCalls')}</span>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user