codeburn/tests/model-breakdown.test.ts
Rick Culpepper 475ff502e0
models: resolve Fireworks-hosted models to friendly display names (#787)
* Resolve Fireworks-hosted models to friendly display names

Fireworks fleet models arrive as `accounts/fireworks/models/<slug>` and
were leaking the raw path/slug in reports and the dashboard (e.g.
`fireworks/mode…`, `glm-5p2`). getShortModelName's path fallback now takes
the last path segment and re-resolves it, so a known slug earns a friendly
name (GLM-5.2, Qwen 3.7 Plus, Kimi K2.7 Code, DeepSeek v4 Pro/Flash) while
an unmapped slug still falls through to the raw segment.

Display-only: getModelCosts prices off the full id, so dollar amounts are
unchanged. The By Model dashboard panel now renders through getShortModelName
too, so already-cached raw keys normalize without a cache rebuild.

* Merge By Model rows that resolve to the same display name

Once path-style ids normalize, a mixed-vintage cache can hold more than one
raw key for the same model (the full accounts/fireworks/models/<slug> path
from an older build and the bare slug/friendly name from a newer one). The
dashboard By Model panel aggregated by raw id, so those rendered as duplicate
rows once both mapped to the same display name.

Extract the aggregation into a pure, tested aggregateModelTotals() keyed by the
resolved display name so the rows merge into one. This also aligns the panel
with modelEfficiency, which is already keyed by getShortModelName.
2026-07-20 14:22:12 -07:00

44 lines
1.8 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { aggregateModelTotals } from '../src/model-breakdown.js'
import type { ProjectSummary, TokenUsage } from '../src/types.js'
function tokens(input: number, cacheRead: number, cacheWrite: number): TokenUsage {
return {
inputTokens: input,
outputTokens: 0,
cacheCreationInputTokens: cacheWrite,
cacheReadInputTokens: cacheRead,
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
}
}
// Minimal ProjectSummary carrying only what aggregateModelTotals reads.
function project(modelBreakdown: Record<string, { calls: number; costUSD: number; tokens: TokenUsage }>): ProjectSummary {
return { sessions: [{ modelBreakdown }] } as unknown as ProjectSummary
}
describe('aggregateModelTotals', () => {
it('merges raw and normalized keys that resolve to the same model into one row', () => {
// A mixed-vintage cache: the full Fireworks path from an older build and the
// bare slug from a newer one — both resolve to `GLM-5.2`.
const totals = aggregateModelTotals([
project({ 'accounts/fireworks/models/glm-5p2': { calls: 2, costUSD: 3.0, tokens: tokens(100, 1000, 0) } }),
project({ 'glm-5p2': { calls: 5, costUSD: 2.18, tokens: tokens(50, 500, 0) } }),
])
expect(Object.keys(totals)).toEqual(['GLM-5.2'])
expect(totals['GLM-5.2']).toMatchObject({ calls: 7, costUSD: 5.18, freshInput: 150, cacheRead: 1500 })
})
it('keeps distinct models on separate rows', () => {
const totals = aggregateModelTotals([
project({
'accounts/fireworks/models/qwen3p7-plus': { calls: 1, costUSD: 0.5, tokens: tokens(10, 0, 0) },
'accounts/fireworks/models/kimi-k2p7-code': { calls: 1, costUSD: 0.6, tokens: tokens(20, 0, 0) },
}),
])
expect(new Set(Object.keys(totals))).toEqual(new Set(['Qwen 3.7 Plus', 'Kimi K2.7 Code']))
})
})