mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-09 17:19:15 +00:00
* feat(codex): compute Codex credit usage (#408, #495) Codex/ChatGPT subscription users consume credits, a unit separate from API dollars: usage is billed as credits-per-million-tokens at per-model rates that differ from the API USD pricing CodeBurn uses for cost. So the reported dollar cost does not match what credits actually consume. Add a credit engine sourced from the official Codex credit rates (developers.openai.com/codex/pricing): GPT-5.5 125/12.5/750, GPT-5.4 62.5/6.25/375, GPT-5.4 mini 18.75/1.875/113 credits per 1M input/cached/output tokens. Surface per-model credit usage in `codeburn models` JSON output (credits field; null for non-Codex or unknown models). models-report already folds reasoning into output and keeps non-cached input + cached-read separately, which is exactly what the credit rates expect, so the figure is exact. Engine + computation are unit-tested. UI display surfaces (the models table, the TUI dashboard, the menubar "credits" view) are intentionally left for a follow-up so the display choice can be decided. * feat(menubar): opt-in Codex credits display metric (#408, #495) Surface Codex credit usage in the menubar as a selectable metric, without changing the default. Cost ($) stays the default in both the menubar and the CLI; credits only appear when explicitly chosen. - TS: buildMenubarPayloadForRange computes the period's Codex credits (via the tested aggregateModels, so reasoning/cached are handled) and exposes current.codexCredits in the menubar JSON. - Swift: new DisplayMetric.credits, a "Credits (Codex)" option in the metric picker, decodes codexCredits, and renders it in the menu-bar title. Default metric remains .cost.
51 lines
2.4 KiB
TypeScript
51 lines
2.4 KiB
TypeScript
import { describe, expect, it, beforeAll, afterAll } from 'vitest'
|
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { buildMenubarPayloadForRange } from '../src/usage-aggregator.js'
|
|
import { getDateRange } from '../src/cli-date.js'
|
|
import { loadPricing } from '../src/models.js'
|
|
|
|
describe('buildMenubarPayloadForRange', () => {
|
|
// Point HOME / config at an empty temp dir so the payload is built from an
|
|
// empty dataset. This keeps the test deterministic and fast regardless of how
|
|
// much real session data the developer's machine has for "today" (parsing a
|
|
// heavy day previously pushed this past its timeout).
|
|
const saved: Record<string, string | undefined> = {}
|
|
let tmp: string
|
|
|
|
beforeAll(async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'codeburn-agg-test-'))
|
|
for (const key of ['HOME', 'CLAUDE_CONFIG_DIR', 'XDG_CONFIG_HOME', 'CODEBURN_CACHE_DIR']) {
|
|
saved[key] = process.env[key]
|
|
}
|
|
process.env['HOME'] = tmp
|
|
process.env['CLAUDE_CONFIG_DIR'] = join(tmp, '.claude')
|
|
process.env['XDG_CONFIG_HOME'] = join(tmp, '.config')
|
|
process.env['CODEBURN_CACHE_DIR'] = join(tmp, 'cache')
|
|
await loadPricing()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
for (const [key, value] of Object.entries(saved)) {
|
|
if (value === undefined) delete process.env[key]
|
|
else process.env[key] = value
|
|
}
|
|
await rm(tmp, { recursive: true, force: true })
|
|
})
|
|
|
|
it('returns a valid payload and skips optimize findings when optimize:false', async () => {
|
|
const payload = await buildMenubarPayloadForRange(getDateRange('today'), { provider: 'all', optimize: false })
|
|
expect(typeof payload.current.label).toBe('string')
|
|
expect(payload.current.cost).toBeGreaterThanOrEqual(0)
|
|
expect(Array.isArray(payload.current.topProjects)).toBe(true)
|
|
expect(Array.isArray(payload.current.topModels)).toBe(true)
|
|
expect(Array.isArray(payload.history.daily)).toBe(true)
|
|
expect(payload.current.retryTax.totalUSD).toBeGreaterThanOrEqual(0)
|
|
// Codex credits are always present in the payload (display gates them); 0 with no data.
|
|
expect(typeof payload.current.codexCredits).toBe('number')
|
|
expect(payload.current.codexCredits).toBeGreaterThanOrEqual(0)
|
|
// optimize:false => scanAndDetect skipped => empty optimize block regardless of data
|
|
expect(payload.optimize).toEqual({ findingCount: 0, savingsUSD: 0, topFindings: [] })
|
|
})
|
|
})
|