mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-10 01:29:41 +00:00
feat(mcp): markdown table renderers for usage and savings
This commit is contained in:
parent
1e82a9cf21
commit
296085dff1
2 changed files with 93 additions and 0 deletions
52
src/mcp/tables.ts
Normal file
52
src/mcp/tables.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { formatCost, formatTokens } from '../format.js'
|
||||
import type { MenubarPayload } from '../menubar-json.js'
|
||||
|
||||
export type BreakdownBy = 'project' | 'model' | 'task' | 'provider'
|
||||
|
||||
function mdTable(headers: string[], rows: string[][]): string {
|
||||
const head = `| ${headers.join(' | ')} |`
|
||||
const sep = `| ${headers.map(() => '---').join(' | ')} |`
|
||||
if (rows.length === 0) return `${head}\n${sep}\n| _(no data)_ |${' |'.repeat(headers.length - 1)}`
|
||||
return [head, sep, ...rows.map(r => `| ${r.join(' | ')} |`)].join('\n')
|
||||
}
|
||||
|
||||
const pct = (n: number) => `${Math.round(n)}%`
|
||||
const oneShot = (r: number | null) => (r === null ? 'n/a' : pct(r * 100))
|
||||
|
||||
export function renderSummaryTable(p: MenubarPayload): string {
|
||||
const c = p.current
|
||||
return [
|
||||
`**${c.label}** — ${formatCost(c.cost)} · ${c.calls} calls · ${c.sessions} sessions`,
|
||||
`cache hit ${pct(c.cacheHitPercent)} · one-shot ${oneShot(c.oneShotRate)} · in ${formatTokens(c.inputTokens)} / out ${formatTokens(c.outputTokens)}`,
|
||||
'',
|
||||
'_Top models_',
|
||||
mdTable(['Model', 'Cost', 'Calls'], c.topModels.slice(0, 5).map(m => [m.name, formatCost(m.cost), String(m.calls)])),
|
||||
'',
|
||||
'_Top projects_',
|
||||
mdTable(['Project', 'Cost', 'Sessions'], c.topProjects.slice(0, 5).map(x => [x.name, formatCost(x.cost), String(x.sessions)])),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function renderBreakdownTable(p: MenubarPayload, by: BreakdownBy, limit: number): string {
|
||||
const c = p.current
|
||||
if (by === 'model') return mdTable(['Model', 'Cost', 'Calls'], c.topModels.slice(0, limit).map(m => [m.name, formatCost(m.cost), String(m.calls)]))
|
||||
if (by === 'project') return mdTable(['Project', 'Cost', 'Sessions'], c.topProjects.slice(0, limit).map(x => [x.name, formatCost(x.cost), String(x.sessions)]))
|
||||
if (by === 'task') return mdTable(['Task', 'Cost', 'Turns', 'One-shot'], c.topActivities.slice(0, limit).map(a => [a.name, formatCost(a.cost), String(a.turns), oneShot(a.oneShotRate)]))
|
||||
return mdTable(['Provider', 'Cost'], Object.entries(c.providers).sort(([, a], [, b]) => b - a).slice(0, limit).map(([name, cost]) => [name, formatCost(cost)]))
|
||||
}
|
||||
|
||||
export function renderSavingsTable(p: MenubarPayload): string {
|
||||
const c = p.current
|
||||
const findings = mdTable(['Finding', 'Impact', 'Saves'], p.optimize.topFindings.slice(0, 10).map(f => [f.title, f.impact, formatCost(f.savingsUSD)]))
|
||||
const retry = mdTable(['Model', 'Retry tax', 'Retries'], c.retryTax.byModel.map(m => [m.name, formatCost(m.taxUSD), String(m.retries)]))
|
||||
const routing = mdTable(['Model', 'Overpaid', 'vs baseline'], c.routingWaste.byModel.map(m => [m.name, formatCost(m.savingsUSD), c.routingWaste.baselineModel]))
|
||||
return [
|
||||
`**Savings — ${c.label}**`,
|
||||
`Optimize findings: ${p.optimize.findingCount} (≈ ${formatCost(p.optimize.savingsUSD)})`,
|
||||
findings, '',
|
||||
`_Retry tax_ — ${formatCost(c.retryTax.totalUSD)} on ${c.retryTax.retries} retries`,
|
||||
retry, '',
|
||||
`_Routing waste_ — ${formatCost(c.routingWaste.totalSavingsUSD)} vs ${c.routingWaste.baselineModel || 'n/a'}`,
|
||||
routing,
|
||||
].join('\n')
|
||||
}
|
||||
41
tests/mcp-tables.test.ts
Normal file
41
tests/mcp-tables.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { renderSummaryTable, renderBreakdownTable, renderSavingsTable } from '../src/mcp/tables.js'
|
||||
import type { MenubarPayload } from '../src/menubar-json.js'
|
||||
|
||||
function payload(): MenubarPayload {
|
||||
return {
|
||||
generated: '', optimize: { findingCount: 1, savingsUSD: 2.5, topFindings: [{ title: 'Trim system prompt', impact: 'high', savingsUSD: 2.5 }] }, history: { daily: [] },
|
||||
current: {
|
||||
label: 'Last 7 Days', cost: 12.5, calls: 100, sessions: 4, oneShotRate: 0.5, inputTokens: 1000, outputTokens: 500,
|
||||
cacheHitPercent: 80, topActivities: [{ name: 'feature', cost: 8, turns: 30, oneShotRate: 0.6 }],
|
||||
topModels: [{ name: 'Opus 4.8', cost: 10, calls: 60 }], providers: { 'claude code': 12.5 },
|
||||
topProjects: [{ name: 'project-abc123', cost: 12.5, sessions: 4, avgCostPerSession: 3.125, sessionDetails: [] }],
|
||||
modelEfficiency: [], topSessions: [],
|
||||
retryTax: { totalUSD: 1.2, retries: 4, editTurns: 20, byModel: [{ name: 'Opus 4.8', taxUSD: 1.2, retries: 4, retriesPerEdit: 0.2 }] },
|
||||
routingWaste: { totalSavingsUSD: 3, baselineModel: 'Haiku 4.5', baselineCostPerEdit: 0.01, byModel: [{ name: 'Opus 4.8', costPerEdit: 0.05, editTurns: 20, actualUSD: 1, counterfactualUSD: 0.2, savingsUSD: 0.8 }] },
|
||||
tools: [], skills: [], subagents: [], mcpServers: [],
|
||||
},
|
||||
} as MenubarPayload
|
||||
}
|
||||
|
||||
describe('tables', () => {
|
||||
it('summary shows headline cost and top models', () => {
|
||||
const t = renderSummaryTable(payload())
|
||||
expect(t).toContain('Last 7 Days')
|
||||
expect(t).toContain('Opus 4.8')
|
||||
expect(t).toContain('| Model | Cost | Calls |')
|
||||
})
|
||||
it('breakdown by provider lists providers', () => {
|
||||
expect(renderBreakdownTable(payload(), 'provider', 20)).toContain('claude code')
|
||||
})
|
||||
it('breakdown handles empty dimension gracefully', () => {
|
||||
const p = payload(); p.current.topActivities = []
|
||||
expect(renderBreakdownTable(p, 'task', 20)).toContain('no data')
|
||||
})
|
||||
it('savings shows retry tax and routing waste', () => {
|
||||
const t = renderSavingsTable(payload())
|
||||
expect(t).toContain('Retry tax')
|
||||
expect(t).toContain('Routing waste')
|
||||
expect(t).toContain('Trim system prompt')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue