mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-21 14:12:34 +00:00
* Mark estimated costs with the same ~ in every view The ~ marker and legend that report already used now appear wherever a dollar figure is mostly a guess: the models and sessions CLI output, the desktop Models, Overview, Sessions and Settings provider rows, the macOS menu bar model rows and provider tabs, and the Windows tray. One rule decides the marker everywhere: the estimated portion is at least 1% of the figure. report, overview and the MCP tables switch to it from "any estimated cent", so a metered Codex row holding a few text-estimated turns stays unmarked. JSON is additive: estimatedCostUSD and isEstimated on models rows, estimatedCost and isEstimated on sessions rows, estimatedCostUSD on menubar providerDetails rows. Totals are unchanged. * Skip the ~ marker on figures that print as zero A sub-cent row that is fully estimated printed as "~$0.00": nothing visible to qualify. The shared helper now also requires the amount, as the surface prints it in the active currency, to show a non-zero digit. CLI, desktop, macOS and Windows helpers all apply it.
183 lines
8.1 KiB
Swift
183 lines
8.1 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodeBurnMenubar
|
|
|
|
/// Per-model token counts on `current.topModels` rows: decode shape, the
|
|
/// unknown-vs-zero rule, and the row presentation contract (compact secondary
|
|
/// line, exact values in accessibility, dashes for unknown).
|
|
@Suite("ModelEntry token counts")
|
|
struct ModelEntryTokenCountsTests {
|
|
|
|
private func payloadJSON(topModels: String) -> Data {
|
|
Data("""
|
|
{
|
|
"generated": "2026-09-07T00:00:00Z",
|
|
"current": {
|
|
"label": "Today",
|
|
"cost": 3.25,
|
|
"calls": 12,
|
|
"sessions": 2,
|
|
"inputTokens": 1000,
|
|
"outputTokens": 500,
|
|
"cacheHitPercent": 40,
|
|
"topModels": \(topModels)
|
|
},
|
|
"optimize": { "findingCount": 0, "savingsUSD": 0, "topFindings": [] },
|
|
"history": { "daily": [] }
|
|
}
|
|
""".utf8)
|
|
}
|
|
|
|
private func decode(_ topModels: String) throws -> MenubarPayload {
|
|
try JSONDecoder().decode(MenubarPayload.self, from: payloadJSON(topModels: topModels))
|
|
}
|
|
|
|
@Test("decodes per-model counts when the payload carries them")
|
|
func decodesCounts() throws {
|
|
let payload = try decode("""
|
|
[
|
|
{ "name": "Sonnet 4.6", "cost": 2.5, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 9,
|
|
"inputTokens": 152300, "outputTokens": 40200, "cacheReadTokens": 1180000, "cacheWriteTokens": 46000 }
|
|
]
|
|
""")
|
|
let row = payload.current.topModels[0]
|
|
#expect(row.inputTokens == 152_300)
|
|
#expect(row.outputTokens == 40_200)
|
|
#expect(row.cacheReadTokens == 1_180_000)
|
|
#expect(row.cacheWriteTokens == 46_000)
|
|
#expect(row.hasTokenCounts)
|
|
}
|
|
|
|
@Test("marks fully and partly estimated rows, not exact ones, sub-1% slivers or $0.00 figures")
|
|
@MainActor func estimatedMarker() throws {
|
|
let payload = try decode("""
|
|
[
|
|
{ "name": "Cursor (auto)", "cost": 579.13, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 9, "estimatedCostUSD": 579.13 },
|
|
{ "name": "Kimi K3", "cost": 177.51, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 9, "estimatedCostUSD": 20 },
|
|
{ "name": "GPT-5.6 Terra", "cost": 1.44, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 9, "estimatedCostUSD": 0.004 },
|
|
{ "name": "Opus 5.5", "cost": 3771.39, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 9 },
|
|
{ "name": "GPT-5.5", "cost": 0.0002, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 1, "estimatedCostUSD": 0.0002 }
|
|
]
|
|
""")
|
|
#expect(payload.current.topModels.map(\.isEstimated) == [true, true, false, false, false])
|
|
}
|
|
|
|
@Test("counts stay nil on legacy rows that predate the fields")
|
|
func legacyRowsStayNil() throws {
|
|
let payload = try decode("""
|
|
[
|
|
{ "name": "Sonnet 4.6", "cost": 2.5, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 9 }
|
|
]
|
|
""")
|
|
let row = payload.current.topModels[0]
|
|
#expect(row.inputTokens == nil)
|
|
#expect(row.outputTokens == nil)
|
|
#expect(row.cacheReadTokens == nil)
|
|
#expect(row.cacheWriteTokens == nil)
|
|
#expect(!row.hasTokenCounts)
|
|
}
|
|
|
|
@Test("a known zero decodes as zero, never as unknown")
|
|
func knownZeroStaysZero() throws {
|
|
let payload = try decode("""
|
|
[
|
|
{ "name": "Haiku 4.5", "cost": 0, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 3,
|
|
"inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 900000, "cacheWriteTokens": 0 }
|
|
]
|
|
""")
|
|
let row = payload.current.topModels[0]
|
|
#expect(row.inputTokens == 0)
|
|
#expect(row.outputTokens == 0)
|
|
#expect(row.cacheReadTokens == 900_000)
|
|
#expect(row.cacheWriteTokens == 0)
|
|
#expect(row.hasTokenCounts)
|
|
}
|
|
|
|
@Test("partially present counts keep the missing ones nil")
|
|
func partialCountsStayNil() throws {
|
|
let payload = try decode("""
|
|
[
|
|
{ "name": "Sonnet 4.6", "cost": 1, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 1,
|
|
"inputTokens": 100 }
|
|
]
|
|
""")
|
|
let row = payload.current.topModels[0]
|
|
#expect(row.inputTokens == 100)
|
|
#expect(row.outputTokens == nil)
|
|
#expect(row.cacheReadTokens == nil)
|
|
#expect(row.hasTokenCounts)
|
|
}
|
|
|
|
@Test("secondary line: known zeros render as 0, unknown as a dash")
|
|
func secondaryLineRenderings() throws {
|
|
let zero = ModelEntry(name: "zero", cost: 0, savingsUSD: 0, savingsBaselineModel: "", calls: 1,
|
|
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0)
|
|
#expect(zero.hasTokenCounts)
|
|
let unknown = ModelEntry(name: "unknown", cost: 1, savingsUSD: 0, savingsBaselineModel: "", calls: 1,
|
|
outputTokens: 12)
|
|
#expect(unknown.hasTokenCounts)
|
|
// hasTokenCounts drives whether the secondary line renders at all.
|
|
#expect(!ModelEntry(name: "legacy", cost: 1, savingsUSD: 0, savingsBaselineModel: "", calls: 1).hasTokenCounts)
|
|
}
|
|
|
|
@Test("accessibility text: exact counts, cache read labelled reused input, cache write separate")
|
|
func accessibilityTextKeepsCacheKindsDistinct() throws {
|
|
let payload = try decode("""
|
|
[
|
|
{ "name": "Sonnet 4.6", "cost": 2.5, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 9,
|
|
"inputTokens": 152300, "outputTokens": 40200, "cacheReadTokens": 1180000, "cacheWriteTokens": 46000 }
|
|
]
|
|
""")
|
|
let label = payload.current.topModels[0].tokenAccessibilityText
|
|
#expect(label.contains("152,300 input"))
|
|
#expect(label.contains("40,200 output"))
|
|
#expect(label.contains("1,180,000 cache read (reused input)"))
|
|
#expect(label.contains("46,000 cache write"))
|
|
// The two cache flavors must never merge into one bucket.
|
|
#expect(!label.contains("cache 1,226,000"))
|
|
}
|
|
|
|
@Test("accessibility text omits zero cache write instead of pairing it with cache read")
|
|
func accessibilityTextOmitsZeroCacheWrite() throws {
|
|
let payload = try decode("""
|
|
[
|
|
{ "name": "Haiku 4.5", "cost": 0, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 3,
|
|
"inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 900000, "cacheWriteTokens": 0 }
|
|
]
|
|
""")
|
|
let label = payload.current.topModels[0].tokenAccessibilityText
|
|
#expect(label.contains("900,000 cache read (reused input)"))
|
|
#expect(!label.contains("cache write"))
|
|
}
|
|
|
|
@Test("accessibility text groups a negative count with a single minus sign")
|
|
func accessibilityTextGroupsNegativeCountsOnce() throws {
|
|
// A negative count is not expected from the CLI, but the payload field
|
|
// is a plain Int and a corrupt or hand-edited snapshot can carry one.
|
|
// Grouping the signed string would emit the sign twice ("--1,234,567"),
|
|
// which a screen reader reads as a different number.
|
|
let payload = try decode("""
|
|
[
|
|
{ "name": "Broken", "cost": 0, "savingsUSD": 0, "savingsBaselineModel": "", "calls": 1,
|
|
"inputTokens": -1234567, "outputTokens": -12, "cacheReadTokens": 0, "cacheWriteTokens": 0 }
|
|
]
|
|
""")
|
|
let label = payload.current.topModels[0].tokenAccessibilityText
|
|
#expect(label.contains("-1,234,567 input"))
|
|
#expect(!label.contains("--"))
|
|
// Under the grouping threshold the sign still appears exactly once.
|
|
#expect(label.contains("-12 output"))
|
|
}
|
|
|
|
@Test("compact token count crosses into a billions rung (#1318)")
|
|
func compactTokenCountHasBillionsRung() {
|
|
// Period cache-read totals on heavy machines cross a billion; before the
|
|
// rung existed they rendered as `12345.7M` in the row's secondary line.
|
|
#expect(compactTokenCount(12_345_700_000) == "12.3B")
|
|
#expect(compactTokenCount(1_000_000_000) == "1.0B")
|
|
// The existing rungs are unchanged on both sides of the boundary.
|
|
#expect(compactTokenCount(999_999_999) == "1000.0M")
|
|
#expect(compactTokenCount(1_234) == "1.2K")
|
|
#expect(compactTokenCount(999) == "999")
|
|
}
|
|
}
|