Skip to content

Commit 300ddee

Browse files
hhh2210steipete
andauthored
Fix unknown Codex model attribution (#2061)
* Fix unknown Codex model attribution * Normalize Codex model evidence * Preserve Codex turn model precedence * test: align Codex model attribution contract --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent 060ffe1 commit 300ddee

8 files changed

Lines changed: 148 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Settings: split provider pane "Settings" sections into "Menu bar" and "Connection" so metric pickers and auth/cookie/source controls are grouped by topic.
77

88
### Fixed
9+
- Codex cost history: keep model-less token events explicitly unattributed instead of pricing them as GPT-5 while preserving current turn model attribution. Thanks @hhh2210!
910
- Gemini: recover expired Workspace and education OAuth sessions when current CLI packages omit `oauth2.js`, with explicit credential and install-path discovery fallbacks. Thanks @Yuxin-Qiao!
1011
- Codex accounts: confirm apparent weekly resets before publishing them and isolate reset detection by stable account ownership, preventing transient full gauges and confetti across same-email workspaces (#2054). Thanks @Yuxin-Qiao!
1112
- Settings: render section footer captions (Advanced keychain note, refresh hints, quota-warning and provider subtitles) leading-aligned in footnote size instead of the trailing-aligned body text macOS gives bare form footers.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.
22

33
enum CodexParserHash {
4-
static let value = "4cf70ccd355c42ac"
4+
static let value = "96b33540df97223f"
55
}

Sources/CodexBarCore/UsageFormatter.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ public enum UsageFormatter {
364364
public static func modelDisplayName(_ raw: String) -> String {
365365
var cleaned = raw.trimmingCharacters(in: .whitespacesAndNewlines)
366366
guard !cleaned.isEmpty else { return raw }
367+
if cleaned == "unknown" { return "Unknown model" }
367368

368369
let patterns = [
369370
#"(?:-|\s)\d{8}$"#,

Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ enum CostUsageCacheIO {
99
private static func artifactVersion(for provider: UsageProvider) -> Int {
1010
switch provider {
1111
case .codex:
12-
8
12+
9
1313
case .claude, .vertexai:
1414
4
1515
default:

Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import Foundation
88

99
// swiftlint:disable type_body_length file_length
1010
enum CostUsageScanner {
11+
static let codexUnknownModel = "unknown"
12+
1113
static let codexProjectMetadataVersion = 1
1214
typealias CancellationCheck = () throws -> Void
1315

@@ -1054,6 +1056,11 @@ enum CostUsageScanner {
10541056
private static let codexJSONFieldType = Array("type".utf8)
10551057
private static let codexJSONFieldCwd = Array("cwd".utf8)
10561058

1059+
static func codexModelEvidence(_ raw: String?) -> String? {
1060+
guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else { return nil }
1061+
return trimmed
1062+
}
1063+
10571064
private static func codexForkParentId(from payload: [String: Any]?) -> String? {
10581065
guard let payload else { return nil }
10591066
for key in ["forked_from_id", "forkedFromId", "parent_session_id", "parentSessionId"] {
@@ -1268,26 +1275,26 @@ enum CostUsageScanner {
12681275
atDepth: 1)
12691276
else { return nil }
12701277

1271-
let model = Self.extractJSONByteStringField(
1278+
let model = Self.codexModelEvidence(Self.extractJSONByteStringField(
12721279
Self.codexJSONFieldModel,
12731280
from: rawBuffer,
12741281
in: infoRange,
1275-
atDepth: 1)
1276-
?? Self.extractJSONByteStringField(
1282+
atDepth: 1))
1283+
?? Self.codexModelEvidence(Self.extractJSONByteStringField(
12771284
Self.codexJSONFieldModelName,
12781285
from: rawBuffer,
12791286
in: infoRange,
1280-
atDepth: 1)
1281-
?? Self.extractJSONByteStringField(
1287+
atDepth: 1))
1288+
?? Self.codexModelEvidence(Self.extractJSONByteStringField(
12821289
Self.codexJSONFieldModel,
12831290
from: rawBuffer,
12841291
in: payloadRange,
1285-
atDepth: 1)
1286-
?? Self.extractJSONByteStringField(
1292+
atDepth: 1))
1293+
?? Self.codexModelEvidence(Self.extractJSONByteStringField(
12871294
Self.codexJSONFieldModel,
12881295
from: rawBuffer,
12891296
in: objectRange,
1290-
atDepth: 1)
1297+
atDepth: 1))
12911298
let total = Self.codexTotals(
12921299
from: rawBuffer,
12931300
in: Self.extractJSONByteObjectField(
@@ -1707,7 +1714,9 @@ enum CostUsageScanner {
17071714
guard let dayKey = Self.dayKeyFromTimestamp(record.timestamp) ?? Self.dayKeyFromParsedISO(record.timestamp)
17081715
else { return }
17091716

1710-
let model = currentModel ?? record.model ?? "gpt-5"
1717+
let model = Self.codexModelEvidence(currentModel)
1718+
?? Self.codexModelEvidence(record.model)
1719+
?? Self.codexUnknownModel
17111720
let total = record.total
17121721
let last = record.last
17131722

@@ -2030,11 +2039,13 @@ enum CostUsageScanner {
20302039
guard (payload["type"] as? String) == "token_count" else { return }
20312040

20322041
let info = payload["info"] as? [String: Any]
2033-
let modelFromInfo = info?["model"] as? String
2034-
?? info?["model_name"] as? String
2035-
?? payload["model"] as? String
2036-
?? obj["model"] as? String
2037-
let model = currentModel ?? modelFromInfo ?? "gpt-5"
2042+
let modelFromInfo = Self.codexModelEvidence(info?["model"] as? String)
2043+
?? Self.codexModelEvidence(info?["model_name"] as? String)
2044+
?? Self.codexModelEvidence(payload["model"] as? String)
2045+
?? Self.codexModelEvidence(obj["model"] as? String)
2046+
let model = Self.codexModelEvidence(currentModel)
2047+
?? modelFromInfo
2048+
?? Self.codexUnknownModel
20382049

20392050
func toInt(_ v: Any?) -> Int {
20402051
if let n = v as? NSNumber { return n.intValue }

Tests/CodexBarTests/CostUsageCacheTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ struct CostUsageCacheTests {
1111
let claudeURL = CostUsageCacheIO.cacheFileURL(provider: .claude, cacheRoot: root)
1212
let vertexURL = CostUsageCacheIO.cacheFileURL(provider: .vertexai, cacheRoot: root)
1313

14-
#expect(codexURL.lastPathComponent == "codex-v8.json")
14+
#expect(codexURL.lastPathComponent == "codex-v9.json")
1515
#expect(claudeURL.lastPathComponent == "claude-v4.json")
1616
#expect(vertexURL.lastPathComponent == "vertexai-v4.json")
1717
}

Tests/CodexBarTests/CostUsageScannerBreakdownTests.swift

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,122 @@ struct CostUsageScannerBreakdownTests {
13721372
#expect(parsed.days[dayKey]?["gpt-5"] == nil)
13731373
}
13741374

1375+
@Test
1376+
func `codex token count without model remains explicitly unknown`() throws {
1377+
let env = try CostUsageTestEnvironment()
1378+
defer { env.cleanup() }
1379+
1380+
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 18)
1381+
let contents = try env.jsonl([
1382+
self.codexTokenCountWithoutModel(
1383+
timestamp: env.isoString(for: day),
1384+
last: (input: 50, cached: 10, output: 5)),
1385+
])
1386+
let fileURL = try env.writeCodexSessionFile(
1387+
day: day,
1388+
filename: "token-count-without-model.jsonl",
1389+
contents: contents)
1390+
1391+
let parsed = CostUsageScanner.parseCodexFile(
1392+
fileURL: fileURL,
1393+
range: CostUsageScanner.CostUsageDayRange(since: day, until: day))
1394+
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)
1395+
1396+
#expect(parsed.days[dayKey]?[CostUsageScanner.codexUnknownModel] == [50, 10, 5])
1397+
#expect(parsed.days[dayKey]?["gpt-5"] == nil)
1398+
}
1399+
1400+
@Test
1401+
func `codex turn context remains authoritative over conflicting token model`() throws {
1402+
let env = try CostUsageTestEnvironment()
1403+
defer { env.cleanup() }
1404+
1405+
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 18)
1406+
let contents = try env.jsonl([
1407+
self.codexTurnContext(timestamp: env.isoString(for: day), model: "openai/gpt-5.5"),
1408+
self.codexTokenCount(
1409+
timestamp: env.isoString(for: day.addingTimeInterval(1)),
1410+
model: "openai/gpt-5.6-sol",
1411+
last: (input: 50, cached: 10, output: 5)),
1412+
])
1413+
let fileURL = try env.writeCodexSessionFile(
1414+
day: day,
1415+
filename: "token-count-model-override.jsonl",
1416+
contents: contents)
1417+
1418+
let parsed = CostUsageScanner.parseCodexFile(
1419+
fileURL: fileURL,
1420+
range: CostUsageScanner.CostUsageDayRange(since: day, until: day))
1421+
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)
1422+
1423+
#expect(parsed.days[dayKey]?["gpt-5.5"] == [50, 10, 5])
1424+
#expect(parsed.days[dayKey]?["gpt-5.6-sol"] == nil)
1425+
}
1426+
1427+
@Test
1428+
func `codex blank token count model preserves turn context`() throws {
1429+
let env = try CostUsageTestEnvironment()
1430+
defer { env.cleanup() }
1431+
1432+
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 18)
1433+
let contents = try env.jsonl([
1434+
self.codexTurnContext(timestamp: env.isoString(for: day), model: "openai/gpt-5.5"),
1435+
self.codexTokenCount(
1436+
timestamp: env.isoString(for: day.addingTimeInterval(1)),
1437+
model: " ",
1438+
last: (input: 50, cached: 10, output: 5)),
1439+
])
1440+
let fileURL = try env.writeCodexSessionFile(
1441+
day: day,
1442+
filename: "blank-token-count-model.jsonl",
1443+
contents: contents)
1444+
1445+
let parsed = CostUsageScanner.parseCodexFile(
1446+
fileURL: fileURL,
1447+
range: CostUsageScanner.CostUsageDayRange(since: day, until: day))
1448+
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)
1449+
1450+
#expect(parsed.days[dayKey]?["gpt-5.5"] == [50, 10, 5])
1451+
#expect(parsed.days[dayKey]?[""] == nil)
1452+
}
1453+
1454+
@Test
1455+
func `codex blank model falls through to model name`() throws {
1456+
let env = try CostUsageTestEnvironment()
1457+
defer { env.cleanup() }
1458+
1459+
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 18)
1460+
let event: [String: Any] = [
1461+
"type": "event_msg",
1462+
"timestamp": env.isoString(for: day),
1463+
"payload": [
1464+
"type": "token_count",
1465+
"info": [
1466+
"model": "",
1467+
"model_name": " openai/gpt-5.6-sol ",
1468+
"last_token_usage": [
1469+
"input_tokens": 50,
1470+
"cached_input_tokens": 10,
1471+
"output_tokens": 5,
1472+
],
1473+
],
1474+
],
1475+
]
1476+
let contents = try env.jsonl([event])
1477+
let fileURL = try env.writeCodexSessionFile(
1478+
day: day,
1479+
filename: "blank-model-valid-model-name.jsonl",
1480+
contents: contents)
1481+
1482+
let parsed = CostUsageScanner.parseCodexFile(
1483+
fileURL: fileURL,
1484+
range: CostUsageScanner.CostUsageDayRange(since: day, until: day))
1485+
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)
1486+
1487+
#expect(parsed.days[dayKey]?["gpt-5.6-sol"] == [50, 10, 5])
1488+
#expect(parsed.days[dayKey]?[""] == nil)
1489+
}
1490+
13751491
@Test
13761492
func `codex daily report writes corrected cache artifact for oversized turn context`() throws {
13771493
let env = try CostUsageTestEnvironment()
@@ -1417,7 +1533,7 @@ struct CostUsageScannerBreakdownTests {
14171533
#expect(first.data[0].totalTokens == 132)
14181534

14191535
let newCacheURL = CostUsageCacheIO.cacheFileURL(provider: .codex, cacheRoot: env.cacheRoot)
1420-
#expect(newCacheURL.lastPathComponent == "codex-v8.json")
1536+
#expect(newCacheURL.lastPathComponent == "codex-v9.json")
14211537
#expect(FileManager.default.fileExists(atPath: newCacheURL.path))
14221538
#expect(FileManager.default.fileExists(atPath: oldCacheURL.path))
14231539

Tests/CodexBarTests/UsageFormatterTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ struct UsageFormatterTests {
300300
#expect(UsageFormatter.modelDisplayName("Claude Opus 4.5 2025 1101") == "Claude Opus 4.5")
301301
#expect(UsageFormatter.modelDisplayName("claude-sonnet-4-5") == "claude-sonnet-4-5")
302302
#expect(UsageFormatter.modelDisplayName("gpt-5.3-codex-spark") == "gpt-5.3-codex-spark")
303+
#expect(UsageFormatter.modelDisplayName("unknown") == "Unknown model")
303304
}
304305

305306
@Test

0 commit comments

Comments
 (0)