From 7876c8e9d7bb6bb3f6043ff4c74b58e4a1dd56a1 Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Fri, 21 Aug 2026 12:48:55 -0700 Subject: [PATCH 1/2] fix(pricing): version the pricing cache and drop codex-credits' dead reasoning param The pricing cache written to disk had no schema version, so a cache written by a pre-#1078 binary lacked cacheWriteCostIsExplicit on every entry. Reading it back resolved the missing key to undefined (falsy), silently reintroducing the surcharge-fabrication bug #1078 killed for up to CACHE_TTL_MS after an upgrade. loadCachedPricing now rejects any cache whose version doesn't match the current schema instead of reading it verbatim. codexCredits() still accepted an optional reasoningTokens param that added it to output - the exact double-count #1078 removed from every real caller. The only caller never passed it; deleted it so it can't be reintroduced by accident. parser.ts's activeGeneratedTokens fallback went through billableOutputTokens in #1078, but codex is the only caller of activeDurationMs/activeGeneratedTokens and always sets both together, so the fallback branch is unreachable for it. Reverted to reduce diff noise. --- src/codex-credits.ts | 7 +++---- src/models.ts | 9 ++++++++- src/parser.ts | 2 +- tests/codex-credits.test.ts | 5 ----- tests/models.test.ts | 38 +++++++++++++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/codex-credits.ts b/src/codex-credits.ts index 10d5b1f4f..664b1eb21 100644 --- a/src/codex-credits.ts +++ b/src/codex-credits.ts @@ -36,9 +36,9 @@ export type CodexCreditTokens = { inputTokens: number /// Cache-read (cached input) tokens, billed at the cheaper cached rate. cachedReadTokens: number + /// Billable output tokens: reasoning is already included (billableOutputTokens + /// in models.ts), so callers must not add it on top here. outputTokens: number - /// Reasoning tokens are billed as output, matching CodeBurn's cost model. - reasoningTokens?: number } /// Credits consumed for one Codex usage record. Returns null when the model has @@ -48,10 +48,9 @@ export function codexCredits(model: string, tokens: CodexCreditTokens): number | if (!rate) return null const safe = (n: number) => (Number.isFinite(n) && n > 0 ? n : 0) const PER_MILLION = 1_000_000 - const output = safe(tokens.outputTokens) + safe(tokens.reasoningTokens ?? 0) return ( (safe(tokens.inputTokens) / PER_MILLION) * rate.input + (safe(tokens.cachedReadTokens) / PER_MILLION) * rate.cachedInput + - (output / PER_MILLION) * rate.output + (safe(tokens.outputTokens) / PER_MILLION) * rate.output ) } diff --git a/src/models.ts b/src/models.ts index 1b81748a3..804452d0e 100644 --- a/src/models.ts +++ b/src/models.ts @@ -59,6 +59,11 @@ type SnapshotEntry = [number, number, number | null, number | null, (number | nu const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json' const CACHE_TTL_MS = 24 * 60 * 60 * 1000 +// Bump whenever a ModelCosts field changes pricing behavior (cacheWriteCostIsExplicit, +// added in #1075/#1078). A cache written under an older/missing version is treated as a +// miss instead of read verbatim, so a stale on-disk file can't reintroduce a killed bug +// for up to CACHE_TTL_MS after an upgrade. +const CACHE_SCHEMA_VERSION = 2 const WEB_SEARCH_COST = 0.01 const ONE_HOUR_CACHE_WRITE_MULTIPLIER_FROM_FIVE_MINUTE_RATE = 1.6 @@ -223,6 +228,7 @@ async function fetchAndCachePricing(): Promise> { await mkdir(getCodeburnCacheDir(), { recursive: true }) await writeFile(getCachePath(), JSON.stringify({ + version: CACHE_SCHEMA_VERSION, timestamp: Date.now(), data: Object.fromEntries(pricing), })) @@ -233,7 +239,8 @@ async function fetchAndCachePricing(): Promise> { async function loadCachedPricing(): Promise | null> { try { const raw = await readFile(getCachePath(), 'utf-8') - const cached = JSON.parse(raw) as { timestamp: number; data: Record } + const cached = JSON.parse(raw) as { version?: number; timestamp: number; data: Record } + if (cached.version !== CACHE_SCHEMA_VERSION) return null if (Date.now() - cached.timestamp > CACHE_TTL_MS) return null return new Map(Object.entries(cached.data)) } catch { diff --git a/src/parser.ts b/src/parser.ts index 071bde686..43137d74d 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1768,7 +1768,7 @@ function buildSessionSummary( modelBreakdown[modelKey].tokens.reasoningTokens += call.usage.reasoningTokens if (call.activeDurationMs !== undefined) { modelBreakdown[modelKey].activeDurationMs = (modelBreakdown[modelKey].activeDurationMs ?? 0) + call.activeDurationMs - modelBreakdown[modelKey].activeGeneratedTokens = (modelBreakdown[modelKey].activeGeneratedTokens ?? 0) + (call.activeGeneratedTokens ?? billableOutputTokens(call.provider, call.usage.outputTokens, call.usage.reasoningTokens)) + modelBreakdown[modelKey].activeGeneratedTokens = (modelBreakdown[modelKey].activeGeneratedTokens ?? 0) + (call.activeGeneratedTokens ?? call.usage.outputTokens + call.usage.reasoningTokens) modelBreakdown[modelKey].toolWaitMs = (modelBreakdown[modelKey].toolWaitMs ?? 0) + (call.toolWaitMs ?? 0) } diff --git a/tests/codex-credits.test.ts b/tests/codex-credits.test.ts index cfa6c482b..46ecd70f3 100644 --- a/tests/codex-credits.test.ts +++ b/tests/codex-credits.test.ts @@ -32,11 +32,6 @@ describe('codexCredits', () => { expect(codexCredits('gpt-5.5', { inputTokens: 0, cachedReadTokens: 1_000_000, outputTokens: 0 })).toBe(12.5) }) - it('folds reasoning tokens into the output rate', () => { - // 500k output + 500k reasoning = 1M output-billed => 750 credits. - expect(codexCredits('gpt-5.5', { inputTokens: 0, cachedReadTokens: 0, outputTokens: 500_000, reasoningTokens: 500_000 })).toBe(750) - }) - it('sums a mixed record (gpt-5.4)', () => { // 2M input (125) + 1M cached (6.25) + 0.5M output (187.5) = 318.75 const credits = codexCredits('gpt-5.4', { inputTokens: 2_000_000, cachedReadTokens: 1_000_000, outputTokens: 500_000 }) diff --git a/tests/models.test.ts b/tests/models.test.ts index b644b784b..8d7a86454 100644 --- a/tests/models.test.ts +++ b/tests/models.test.ts @@ -753,6 +753,7 @@ describe('DeepSeek v4 models resolve to pricing', () => { process.env['CODEBURN_CACHE_DIR'] = cacheRoot await mkdir(cacheRoot, { recursive: true }) await writeFile(join(cacheRoot, 'litellm-pricing.json'), JSON.stringify({ + version: 2, // must match models.ts's CACHE_SCHEMA_VERSION or the cache is treated as a miss timestamp: Date.now(), data: { 'gpt-4o-mini': { @@ -778,6 +779,42 @@ describe('DeepSeek v4 models resolve to pricing', () => { }) }) +describe('pricing cache schema version (#1075/#1078 follow-up)', () => { + it('discards a cache written by a pre-#1078 binary instead of reading its missing cacheWriteCostIsExplicit as false', async () => { + const cacheRoot = await mkdtemp(join(tmpdir(), 'codeburn-pricing-cache-')) + try { + process.env['CODEBURN_CACHE_DIR'] = cacheRoot + await mkdir(cacheRoot, { recursive: true }) + // Shape of a cache file written before #1078 added `version` and + // `cacheWriteCostIsExplicit`: no version field, and entries missing the + // key despite carrying a real (non-default) cache-write rate. + await writeFile(join(cacheRoot, 'litellm-pricing.json'), JSON.stringify({ + timestamp: Date.now(), + data: { + 'gpt-5.6': { + inputCostPerToken: 5e-6, + outputCostPerToken: 3e-5, + cacheWriteCostPerToken: 6.25e-6, + cacheReadCostPerToken: 5e-7, + webSearchCostPerRequest: 0.01, + fastMultiplier: 1, + }, + }, + }), 'utf-8') + + await loadPricing() + + // Pre-fix, loadCachedPricing had no version check: it would read this + // cache verbatim, and gpt-5.6's missing key would resolve to undefined + // (falsy) here instead of the true its LiteLLM entry actually carries. + expect(getModelCosts('gpt-5.6')!.cacheWriteCostIsExplicit).toBe(true) + } finally { + await rm(cacheRoot, { recursive: true, force: true }) + await loadPricing() + } + }) +}) + describe('provider pricing suffix variants', () => { const cases: Array<[string, string]> = [ ['GLM-4.7-TEE', 'glm-4.7'], @@ -915,6 +952,7 @@ describe('findUnpricedModels', () => { try { process.env['CODEBURN_CACHE_DIR'] = cacheRoot await writeFile(join(cacheRoot, 'litellm-pricing.json'), JSON.stringify({ + version: 2, // must match models.ts's CACHE_SCHEMA_VERSION or the cache is treated as a miss timestamp: Date.now(), data: { 'zz-zero-stub-model': { From f92949c0812fea42637bea8c4fa8fcc3a5783ac8 Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Fri, 21 Aug 2026 12:49:05 -0700 Subject: [PATCH 2/2] docs(pricing): bound the codex repricing drift in verify:upgrade, fix stale comments The COST_CHANGED_BY_DESIGN carve-out in compare.mjs left codex cost entirely unasserted after #1075/#1078. It now requires the upgraded cost to be strictly lower than baseline and within 25% of it, and the row verdict says "repriced" instead of the misleading "identical (cost N% drift)". grok.ts's comment on the reasoning/output split still claimed provider-side splitting was the repo's only mechanism; it now also names billableOutputTokens/REASONING_INCLUDED_IN_OUTPUT (models.ts), which is the other half since #1078. usage-aggregator.ts's "folds reasoning into output" comment was true pre-#1078 but is backwards for codex now (reasoning is already inside output, not added to it). Test exemplars for the "reasoning is additive" case used hermes, whose upstream is OpenAI-shaped and may not stay a safe example; swapped to gemini, which documents "thoughts" as genuinely separate output. CHANGELOG's #1075 entry gets one line noting days whose codex transcripts have aged out keep their pre-fix totals via the daily-cache never-lose guard, matching the disclosure already given for #1040. --- CHANGELOG.md | 2 +- scripts/upgrade-path/compare.mjs | 24 ++++++++++++++++++++---- src/providers/grok.ts | 15 +++++++++------ src/usage-aggregator.ts | 6 +++--- tests/codex-pricing-1075.test.ts | 9 +++++---- tests/models-report.test.ts | 4 ++-- 6 files changed, 40 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b9bad09..6db988cfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ - **The resident `codeburn serve` child.** The first real panel request is also the cache warm-up, so startup never runs an artificial warm-up query beside a duplicate one-shot child; each served command carries its own read-only option allowlist, and anything outside it falls back to a normal spawn; the child exits when its stdin closes, so it can never outlive the app. Requests whose response exceeds the 16 MiB frame limit still replace the child, but that deliberate kill no longer spends the resident's unexpected-death budget. (#972) ### Fixed -- **Codex spend no longer counts reasoning tokens twice, and cache writes are priced only where OpenAI actually charges for them.** OpenAI bills reasoning tokens as *part of* `output_tokens`, not on top of it — on a 1,396-rollout corpus all 134,316 events carrying a total satisfy `input + output == total` — but CodeBurn added `reasoning_output_tokens` to output when pricing a Codex call and again in the models, audit and per-model displays. Every Codex number was therefore too high: on that corpus **cost by $166.03 (3.5%)** and **displayed Output tokens by 34.6%** ($4,713.12 -> $4,547.09; 22.6M -> 16.8M output tokens). The raw `reasoningTokens` figure is unchanged and still reported on its own; only the double-count is gone. Both places that price a Codex call — the parser and the cache-rehydration re-price — now go through one shared `billableOutputTokens` helper, so a cold run and a warm run can never disagree. Separately, Codex's `cache_write_input_tokens` (new in codex PR #33454) was never read and cache-creation tokens were hardcoded to 0; they are now carved out of the uncached-input bucket and clamped so they can never exceed it. That carve-out happens **only on models whose pricing source publishes a real cache-write rate** — gpt-5.6 and its terra/sol/luna variants charge 1.25x input for a cache write, everything before it charges nothing extra — because CodeBurn fabricates a 1.25x rate when a source omits one, and charging that would have invented a surcharge on gpt-5.5, gpt-5.4, gpt-5.3-codex and gpt-5. On models without an explicit rate the tokens stay in the plain input bucket and the price is unchanged to the cent. The field is new enough that today's impact is $0 on that corpus. Codex sessions re-parse once and the daily cache re-derives once off the warm session cache (a global re-derivation of every day and every provider, since it has no per-provider invalidation); no other provider's numbers move. Long-context pricing tiers from the same report are tracked separately in #1076 and the missing `gpt-5.6-codex` snapshot rows in #1077. Thanks @chr-evensen. (#1075) +- **Codex spend no longer counts reasoning tokens twice, and cache writes are priced only where OpenAI actually charges for them.** OpenAI bills reasoning tokens as *part of* `output_tokens`, not on top of it — on a 1,396-rollout corpus all 134,316 events carrying a total satisfy `input + output == total` — but CodeBurn added `reasoning_output_tokens` to output when pricing a Codex call and again in the models, audit and per-model displays. Every Codex number was therefore too high: on that corpus **cost by $166.03 (3.5%)** and **displayed Output tokens by 34.6%** ($4,713.12 -> $4,547.09; 22.6M -> 16.8M output tokens). The raw `reasoningTokens` figure is unchanged and still reported on its own; only the double-count is gone. Both places that price a Codex call — the parser and the cache-rehydration re-price — now go through one shared `billableOutputTokens` helper, so a cold run and a warm run can never disagree. Separately, Codex's `cache_write_input_tokens` (new in codex PR #33454) was never read and cache-creation tokens were hardcoded to 0; they are now carved out of the uncached-input bucket and clamped so they can never exceed it. That carve-out happens **only on models whose pricing source publishes a real cache-write rate** — gpt-5.6 and its terra/sol/luna variants charge 1.25x input for a cache write, everything before it charges nothing extra — because CodeBurn fabricates a 1.25x rate when a source omits one, and charging that would have invented a surcharge on gpt-5.5, gpt-5.4, gpt-5.3-codex and gpt-5. On models without an explicit rate the tokens stay in the plain input bucket and the price is unchanged to the cent. The field is new enough that today's impact is $0 on that corpus. Codex sessions re-parse once and the daily cache re-derives once off the warm session cache (a global re-derivation of every day and every provider, since it has no per-provider invalidation); no other provider's numbers move. Days whose Codex transcripts have since aged out are held by the same never-lose guard #1040 relies on: a re-derivation that finds fewer calls than the settled baseline keeps the older, pre-fix (double-counted) total rather than truncating it, so those days do not pick up the repricing until their sources are re-derived with equal or greater evidence. Long-context pricing tiers from the same report are tracked separately in #1076 and the missing `gpt-5.6-codex` snapshot rows in #1077. Thanks @chr-evensen. (#1075) - **Codex calls attributed from session metadata no longer carry a stale model.** The Buffer fast path scanned `session_meta` for the first `"model"` string anywhere in the payload, so a nested `base_instructions.provenance.model` was read as if it were `payload.model` — and since the model is last-writer-wins state, that wrong value was credited to every call before the rollout's first `turn_context` and to every call after any mid-file `session_meta` (29 of 1380 rollouts on one real corpus carry a late `session_meta`, and 57 record usage before any `turn_context`). Direct payload fields are now read depth-aware, which is what the non-fast `JSON.parse` path always did. Codex sessions re-parse once (~9s on a 4 GB rollout corpus) and the daily cache re-derives once off the warm session cache, a global re-derivation of every day and every provider since it has no per-provider invalidation; it moves per-model attribution, and clears any rollup an earlier parse change had left stale. Days whose transcripts have partly aged out are held by the never-lose guard: on a real 110-day cache no day lost value and none disappeared — 100 days came back identical and 9 grok days rose by $19.80 in total. Thanks @timdp. (#1040) - **Codex `session_meta` cwd / session id / originator follow the same depth-1 window as `model`.** #1040 fixed nested `provenance.model`; the compact Buffer path still took the first `cwd`, `session_id`, `originator`, `name`, `forked_from_id` or `model_provider` anywhere in the payload, so a `dynamic_tools[].name` (or any same-named nested key) could steal the top-level field. Those strings now use the existing payload-depth-1 scan. Function-call `name` on other event types is unchanged. Codex sessions re-parse once. (#1045) - **Plan rows for sticker-price presets read as a budget instead of live provider quota.** There is no Grok quota endpoint, so a SuperGrok row was parsed API-equivalent spend divided by the plan's sticker price on a monthly reset — but the TUI labelled that math "plan" and "reset", which next to a client showing xAI's real weekly window read as CodeBurn being wrong. The bars and the arithmetic are unchanged; the words are not. Both the dashboard and the desktop app now say the number is an API-equivalent monthly budget and not a live provider window, in the same wording on both surfaces, and for every preset rather than as a SuperGrok special case. The window is anniversary-based (`plan.resetDay`, settable with `codeburn plan set --reset-day`), so it is called a budget reset rather than a calendar one. The row was also shortened to fit 80 columns: at that width the percentage and the projected month were being truncated away, including on custom plans, whose label carries the provider. diff --git a/scripts/upgrade-path/compare.mjs b/scripts/upgrade-path/compare.mjs index 46b01c5cd..67ad34260 100644 --- a/scripts/upgrade-path/compare.mjs +++ b/scripts/upgrade-path/compare.mjs @@ -25,15 +25,23 @@ // codex PRICING changed by design in #1075: reasoning tokens are billed // inside output rather than on top of it, and cache writes are carved out // of the input bucket. Nothing about what was PARSED moved, so codex keeps -// the full exact treatment for the call count and every token field; only -// the cost tolerance is lifted, and the delta is reported instead. Drop it -// from this list once a published CLI carries the fix. +// the full exact treatment for the call count and every token field; the +// cost tolerance is instead replaced with REPRICE_TOLERANCE — the +// upgraded cost must be strictly lower than the baseline and within 25% +// of it, since #1075 only ever removes a double-count and never raises +// cost — and the delta is reported instead. Drop it from this list once a +// published CLI carries the fix. const EXACT = ['claude', 'codex', 'gemini', 'kiro', 'cursor'] const CHANGED_BY_DESIGN = ['grok'] const COST_CHANGED_BY_DESIGN = ['codex'] const NEW_IN_THIS_RELEASE = ['dsh'] const COST_TOLERANCE = 0.005 // 0.5% relative +// #1075 only ever LOWERS codex cost (double-counted reasoning removed, cache +// writes carved out of the input bucket) and by a bounded amount on any real +// corpus; a rise, or a drop past this bound, means something beyond the known +// repricing changed. +const REPRICE_TOLERANCE = 0.25 // 25% relative import { readFileSync } from 'node:fs' import { join } from 'node:path' @@ -102,8 +110,14 @@ for (const name of providers) { if (b.calls !== u.calls) diffs.push(`calls ${b.calls} != ${u.calls}`) for (const f of TOKEN_FIELDS) if (b[f] !== u[f]) diffs.push(`${f} ${b[f]} != ${u[f]}`) const costDrift = relDiff(b.cost, u.cost) + let repriced = false if (COST_CHANGED_BY_DESIGN.includes(name)) { - notes.push(`${name}: cost ${fmt(b.cost)} -> ${fmt(u.cost)} (${(costDrift * 100).toFixed(3)}%) — repricing expected (#1075); tokens and calls still asserted exactly`) + if (u.cost > b.cost) diffs.push(`cost ${fmt(b.cost)} -> ${fmt(u.cost)} rose; #1075 should only lower codex cost`) + else if (costDrift > REPRICE_TOLERANCE) diffs.push(`cost ${fmt(b.cost)} -> ${fmt(u.cost)} (${(costDrift * 100).toFixed(3)}% > ${(REPRICE_TOLERANCE * 100).toFixed(0)}% expected bound for #1075)`) + else { + repriced = true + notes.push(`${name}: cost ${fmt(b.cost)} -> ${fmt(u.cost)} (${(costDrift * 100).toFixed(3)}%) — repricing expected (#1075); tokens and calls still asserted exactly`) + } } else if (costDrift > COST_TOLERANCE) { diffs.push(`cost ${fmt(b.cost)} != ${fmt(u.cost)} (${(costDrift * 100).toFixed(3)}% > ${(COST_TOLERANCE * 100).toFixed(1)}%)`) } @@ -113,6 +127,8 @@ for (const name of providers) { } else if (diffs.length) { failures.push(`${name}: ${diffs.join(', ')}`) verdict = 'DIFFERS' + } else if (repriced) { + verdict = `repriced (cost ${(costDrift * 100).toFixed(3)}% drift)` } else { verdict = costDrift === 0 ? 'identical' : `identical (cost ${(costDrift * 100).toFixed(3)}% drift)` } diff --git a/src/providers/grok.ts b/src/providers/grok.ts index 4724a8af2..804bb4f12 100644 --- a/src/providers/grok.ts +++ b/src/providers/grok.ts @@ -405,12 +405,15 @@ function createParser(source: SessionSource, seenKeys: Set): SessionPars inputTokens: parsed.usage.input, // Grok reports reasoning INSIDE outputTokens, but the repo contract is // the opposite: ParsedProviderCall.reasoningTokens is exclusive of - // outputTokens, and every consumer sums the two (parser.ts's - // cachedCallToApiCall for cost, modelBreakdown for tokens, and the - // models/audit reports). tests/providers/kiro.test.ts states it - // outright. So split it here rather than special-casing grok in five - // downstream places: subtracting reasoning makes `output + reasoning` - // reconstruct exactly the number Grok reported. + // outputTokens. Downstream consumers reconstitute the billable total + // through billableOutputTokens() (models.ts): it adds reasoning back on + // top for grok and every other provider, except the + // REASONING_INCLUDED_IN_OUTPUT set (claude, codex) whose reasoning is + // already inside output_tokens and must not be added again. + // tests/providers/kiro.test.ts states the exclusive contract outright. + // So split it here rather than special-casing grok in every downstream + // site: subtracting reasoning makes `output + reasoning` reconstruct + // exactly the number Grok reported. outputTokens: parsed.usage.output - reasoningTokens, cacheCreationInputTokens: parsed.usage.cacheCreation, cacheReadInputTokens: parsed.usage.cacheRead, diff --git a/src/usage-aggregator.ts b/src/usage-aggregator.ts index 4350413bd..78b1d4830 100644 --- a/src/usage-aggregator.ts +++ b/src/usage-aggregator.ts @@ -609,9 +609,9 @@ export async function buildMenubarPayloadForRange(periodInfo: PeriodInfo, opts: } claudeConfigs = claudeConfigs ?? await claudeConfigSelector(scanProjects, null) - // Codex credits for the period. Reuses the models aggregation (folds reasoning - // into output, keeps non-cached input + cached-read separate) so the figure - // matches the official credit rates. + // Codex credits for the period. Reuses the models aggregation (billable output + // already includes reasoning for codex, keeps non-cached input + cached-read + // separate) so the figure matches the official credit rates. const modelRows = await aggregateModels(scanProjects) currentData.codexCredits = modelRows.reduce( (sum, r) => sum + (r.provider === 'codex' && r.credits != null ? r.credits : 0), diff --git a/tests/codex-pricing-1075.test.ts b/tests/codex-pricing-1075.test.ts index 9e1b5eb6e..2ed6f1243 100644 --- a/tests/codex-pricing-1075.test.ts +++ b/tests/codex-pricing-1075.test.ts @@ -112,17 +112,18 @@ describe('#1075 A - reasoning is not billed on top of output', () => { const codex = makeApiCall('codex', 'gpt-5.5', { outputTokens: 1000, reasoningTokens: 400 }) // A provider that really does report reasoning as a separate bucket keeps // the additive behaviour, so this is a codex carve-out and not a blanket - // change to every display sum. - const additive = makeApiCall('hermes', 'gpt-5.5', { outputTokens: 1000, reasoningTokens: 400 }) + // change to every display sum. Gemini documents "thoughts" as genuinely + // separate from output (src/providers/gemini.ts), unlike codex/claude. + const additive = makeApiCall('gemini', 'gemini-2.5-pro', { outputTokens: 1000, reasoningTokens: 400 }) const projects = [makeProject([codex, additive])] const auditRows = await aggregateAudit(projects) expect(auditRows.find(r => r.provider === 'codex')!.displayed.outputTokens).toBe(1000) - expect(auditRows.find(r => r.provider === 'hermes')!.displayed.outputTokens).toBe(1400) + expect(auditRows.find(r => r.provider === 'gemini')!.displayed.outputTokens).toBe(1400) const modelRows = await aggregateModels(projects) expect(modelRows.find(r => r.provider === 'codex')!.outputTokens).toBe(1000) - expect(modelRows.find(r => r.provider === 'hermes')!.outputTokens).toBe(1400) + expect(modelRows.find(r => r.provider === 'gemini')!.outputTokens).toBe(1400) }) }) diff --git a/tests/models-report.test.ts b/tests/models-report.test.ts index dac721f9d..67b36f4e6 100644 --- a/tests/models-report.test.ts +++ b/tests/models-report.test.ts @@ -244,8 +244,8 @@ describe('aggregateModels', () => { const project = makeProject([ makeTurn('feature', [ { - provider: 'hermes', - model: 'gpt-5', + provider: 'gemini', + model: 'gemini-2.5-pro', usage: { ...emptyTokens(), inputTokens: 100, outputTokens: 50, reasoningTokens: 200 }, costUSD: 1.0, tools: [],