Skip to content

Commit b25c008

Browse files
committed
fix: normalize the trimmed model id when grouping OpenCode Go costs
The grouping key used the untrimmed modelID even though emptiness was checked against the trimmed value, so a model id with incidental leading/trailing whitespace would form its own bucket instead of merging with the clean value for the same model. Group by the trimmed value consistently, and add regression tests for whitespace-only and whitespace-padded model ids.
1 parent 821c51d commit b25c008

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

Sources/CodexBarCore/Providers/OpenCodeGo/OpenCodeGoLocalUsageReader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@ public struct OpenCodeGoLocalUsageReader: Sendable {
327327
let date = Date(timeIntervalSince1970: TimeInterval(row.createdMs) / 1000)
328328
guard date >= sinceStartOfDay, date <= now else { continue }
329329
let key = CostUsageScanner.CostUsageDayRange.dayKey(from: date)
330-
let model = row.model.trimmingCharacters(in: .whitespacesAndNewlines)
331-
.isEmpty ? Self.unknownModelName : row.model
330+
let trimmedModel = row.model.trimmingCharacters(in: .whitespacesAndNewlines)
331+
let model = trimmedModel.isEmpty ? Self.unknownModelName : trimmedModel
332332
var dayTotals = totalsByModel[key] ?? [:]
333333
var bucket = dayTotals[model] ?? (cost: 0, requestCount: 0)
334334
bucket.cost += row.cost

Tests/CodexBarTests/OpenCodeGoLocalUsageReaderTests.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,59 @@ struct OpenCodeGoLocalUsageReaderTests {
345345
#expect(entry.modelBreakdowns?.first?.costUSD == 4.0)
346346
}
347347

348+
@Test
349+
func `whitespace only model ids fall back to the unknown model bucket`() throws {
350+
let env = try Self.makeEnvironment()
351+
defer { try? FileManager.default.removeItem(at: env.root) }
352+
353+
try Self.writeAuth(to: env.authURL)
354+
try Self.createDatabase(at: env.databaseURL)
355+
try Self.insertMessage(
356+
databaseURL: env.databaseURL,
357+
createdMs: Self.ms("2026-03-06T11:00:00.000Z"),
358+
cost: 5.0,
359+
model: " ")
360+
361+
let reader = OpenCodeGoLocalUsageReader(authURL: env.authURL, databaseURL: env.databaseURL)
362+
let snapshot = try reader.fetch(now: Date(timeIntervalSince1970: 1_772_798_400))
363+
364+
let entry = try #require(snapshot.daily.first)
365+
#expect(entry.modelsUsed == ["unknown"])
366+
#expect(entry.modelBreakdowns?.first?.modelName == "unknown")
367+
#expect(entry.modelBreakdowns?.first?.costUSD == 5.0)
368+
}
369+
370+
@Test
371+
func `model ids with incidental whitespace merge with the trimmed model bucket`() throws {
372+
let env = try Self.makeEnvironment()
373+
defer { try? FileManager.default.removeItem(at: env.root) }
374+
375+
try Self.writeAuth(to: env.authURL)
376+
try Self.createDatabase(at: env.databaseURL)
377+
try Self.insertMessage(
378+
databaseURL: env.databaseURL,
379+
createdMs: Self.ms("2026-03-06T11:00:00.000Z"),
380+
cost: 2.0,
381+
model: "claude-sonnet-4-5")
382+
try Self.insertMessage(
383+
databaseURL: env.databaseURL,
384+
createdMs: Self.ms("2026-03-06T12:00:00.000Z"),
385+
cost: 3.0,
386+
model: " claude-sonnet-4-5 ")
387+
388+
let reader = OpenCodeGoLocalUsageReader(authURL: env.authURL, databaseURL: env.databaseURL)
389+
let now = Date(timeIntervalSince1970: TimeInterval(Self.ms("2026-03-06T15:00:00.000Z")) / 1000)
390+
let snapshot = try reader.fetch(now: now, historyDays: 30)
391+
392+
let entry = try #require(snapshot.daily.first)
393+
#expect(entry.modelsUsed == ["claude-sonnet-4-5"])
394+
let breakdowns = try #require(entry.modelBreakdowns)
395+
#expect(breakdowns.count == 1)
396+
#expect(breakdowns.first?.modelName == "claude-sonnet-4-5")
397+
#expect(breakdowns.first?.costUSD == 5.0)
398+
#expect(breakdowns.first?.requestCount == 2)
399+
}
400+
348401
@Test
349402
func `missing auth and history is not detected`() throws {
350403
let env = try Self.makeEnvironment()

0 commit comments

Comments
 (0)