Skip to content

Commit e1331c3

Browse files
committed
Restore GPT-5 pricing normalization rules
1 parent 85a7a30 commit e1331c3

2 files changed

Lines changed: 242 additions & 14 deletions

File tree

Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift

Lines changed: 116 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,31 @@ enum CostUsagePricing {
55
let inputCostPerToken: Double
66
let outputCostPerToken: Double
77
let cacheReadInputCostPerToken: Double?
8+
let thresholdTokens: Int?
9+
let inputCostPerTokenAboveThreshold: Double?
10+
let outputCostPerTokenAboveThreshold: Double?
11+
let cacheReadInputCostPerTokenAboveThreshold: Double?
812
let displayLabel: String?
13+
14+
init(
15+
inputCostPerToken: Double,
16+
outputCostPerToken: Double,
17+
cacheReadInputCostPerToken: Double?,
18+
thresholdTokens: Int? = nil,
19+
inputCostPerTokenAboveThreshold: Double? = nil,
20+
outputCostPerTokenAboveThreshold: Double? = nil,
21+
cacheReadInputCostPerTokenAboveThreshold: Double? = nil,
22+
displayLabel: String? = nil)
23+
{
24+
self.inputCostPerToken = inputCostPerToken
25+
self.outputCostPerToken = outputCostPerToken
26+
self.cacheReadInputCostPerToken = cacheReadInputCostPerToken
27+
self.thresholdTokens = thresholdTokens
28+
self.inputCostPerTokenAboveThreshold = inputCostPerTokenAboveThreshold
29+
self.outputCostPerTokenAboveThreshold = outputCostPerTokenAboveThreshold
30+
self.cacheReadInputCostPerTokenAboveThreshold = cacheReadInputCostPerTokenAboveThreshold
31+
self.displayLabel = displayLabel
32+
}
933
}
1034

1135
struct ClaudePricing: Sendable {
@@ -27,11 +51,26 @@ enum CostUsagePricing {
2751
outputCostPerToken: 1e-5,
2852
cacheReadInputCostPerToken: 1.25e-7,
2953
displayLabel: nil),
54+
"gpt-5-chat": CodexPricing(
55+
inputCostPerToken: 1.25e-6,
56+
outputCostPerToken: 1e-5,
57+
cacheReadInputCostPerToken: 1.25e-7,
58+
displayLabel: nil),
59+
"gpt-5-chat-latest": CodexPricing(
60+
inputCostPerToken: 1.25e-6,
61+
outputCostPerToken: 1e-5,
62+
cacheReadInputCostPerToken: 1.25e-7,
63+
displayLabel: nil),
3064
"gpt-5-codex": CodexPricing(
3165
inputCostPerToken: 1.25e-6,
3266
outputCostPerToken: 1e-5,
3367
cacheReadInputCostPerToken: 1.25e-7,
3468
displayLabel: nil),
69+
"gpt-5-codex-mini": CodexPricing(
70+
inputCostPerToken: 2.5e-7,
71+
outputCostPerToken: 2e-6,
72+
cacheReadInputCostPerToken: 2.5e-8,
73+
displayLabel: nil),
3574
"gpt-5-mini": CodexPricing(
3675
inputCostPerToken: 2.5e-7,
3776
outputCostPerToken: 2e-6,
@@ -52,6 +91,11 @@ enum CostUsagePricing {
5291
outputCostPerToken: 1e-5,
5392
cacheReadInputCostPerToken: 1.25e-7,
5493
displayLabel: nil),
94+
"gpt-5.1-chat-latest": CodexPricing(
95+
inputCostPerToken: 1.25e-6,
96+
outputCostPerToken: 1e-5,
97+
cacheReadInputCostPerToken: 1.25e-7,
98+
displayLabel: nil),
5599
"gpt-5.1-codex": CodexPricing(
56100
inputCostPerToken: 1.25e-6,
57101
outputCostPerToken: 1e-5,
@@ -72,6 +116,16 @@ enum CostUsagePricing {
72116
outputCostPerToken: 1.4e-5,
73117
cacheReadInputCostPerToken: 1.75e-7,
74118
displayLabel: nil),
119+
"gpt-5.2-chat": CodexPricing(
120+
inputCostPerToken: 1.75e-6,
121+
outputCostPerToken: 1.4e-5,
122+
cacheReadInputCostPerToken: 1.75e-7,
123+
displayLabel: nil),
124+
"gpt-5.2-chat-latest": CodexPricing(
125+
inputCostPerToken: 1.75e-6,
126+
outputCostPerToken: 1.4e-5,
127+
cacheReadInputCostPerToken: 1.75e-7,
128+
displayLabel: nil),
75129
"gpt-5.2-codex": CodexPricing(
76130
inputCostPerToken: 1.75e-6,
77131
outputCostPerToken: 1.4e-5,
@@ -87,6 +141,16 @@ enum CostUsagePricing {
87141
outputCostPerToken: 1.4e-5,
88142
cacheReadInputCostPerToken: 1.75e-7,
89143
displayLabel: nil),
144+
"gpt-5.3": CodexPricing(
145+
inputCostPerToken: 1.75e-6,
146+
outputCostPerToken: 1.4e-5,
147+
cacheReadInputCostPerToken: 1.75e-7,
148+
displayLabel: nil),
149+
"gpt-5.3-chat-latest": CodexPricing(
150+
inputCostPerToken: 1.75e-6,
151+
outputCostPerToken: 1.4e-5,
152+
cacheReadInputCostPerToken: 1.75e-7,
153+
displayLabel: nil),
90154
"gpt-5.3-codex-spark": CodexPricing(
91155
inputCostPerToken: 0,
92156
outputCostPerToken: 0,
@@ -218,22 +282,44 @@ enum CostUsagePricing {
218282
]
219283

220284
static func normalizeCodexModel(_ raw: String) -> String {
285+
var trimmed = self.displayCodexModel(raw)
286+
if let snapshotBase = self.codexSnapshotBaseModel(trimmed) {
287+
trimmed = snapshotBase
288+
}
289+
if self.codex[trimmed] != nil {
290+
return trimmed
291+
}
292+
if let codexRange = trimmed.range(of: "-codex"), !trimmed.contains("-codex-mini") {
293+
let base = String(trimmed[..<codexRange.lowerBound])
294+
if self.codex[base] != nil { return base }
295+
}
296+
return trimmed
297+
}
298+
299+
static func displayCodexModel(_ raw: String) -> String {
221300
var trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
222301
if trimmed.hasPrefix("openai/") {
223302
trimmed = String(trimmed.dropFirst("openai/".count))
224303
}
304+
return trimmed
305+
}
225306

226-
if self.codex[trimmed] != nil {
227-
return trimmed
228-
}
307+
private static func codexSnapshotBaseModel(_ raw: String) -> String? {
308+
let patterns = [
309+
#"-\d{4}-\d{2}-\d{2}$"#,
310+
#"-\d{8}$"#,
311+
]
229312

230-
if let datedSuffix = trimmed.range(of: #"-\d{4}-\d{2}-\d{2}$"#, options: .regularExpression) {
231-
let base = String(trimmed[..<datedSuffix.lowerBound])
232-
if self.codex[base] != nil {
233-
return base
313+
for pattern in patterns {
314+
if let range = raw.range(of: pattern, options: .regularExpression) {
315+
let base = String(raw[..<range.lowerBound])
316+
if self.codex[base] != nil {
317+
return base
318+
}
234319
}
235320
}
236-
return trimmed
321+
322+
return nil
237323
}
238324

239325
static func codexDisplayLabel(model: String) -> String? {
@@ -275,10 +361,28 @@ enum CostUsagePricing {
275361
guard let pricing = self.codex[key] else { return nil }
276362
let cached = min(max(0, cachedInputTokens), max(0, inputTokens))
277363
let nonCached = max(0, inputTokens - cached)
278-
let cachedRate = pricing.cacheReadInputCostPerToken ?? pricing.inputCostPerToken
279-
return Double(nonCached) * pricing.inputCostPerToken
280-
+ Double(cached) * cachedRate
281-
+ Double(max(0, outputTokens)) * pricing.outputCostPerToken
364+
let effectiveInputTokens = max(0, inputTokens)
365+
let useAboveThresholdPricing = if let threshold = pricing.thresholdTokens {
366+
effectiveInputTokens > threshold
367+
} else {
368+
false
369+
}
370+
let inputRate = useAboveThresholdPricing
371+
? (pricing.inputCostPerTokenAboveThreshold ?? pricing.inputCostPerToken)
372+
: pricing.inputCostPerToken
373+
let outputRate = useAboveThresholdPricing
374+
? (pricing.outputCostPerTokenAboveThreshold ?? pricing.outputCostPerToken)
375+
: pricing.outputCostPerToken
376+
let cacheRate = useAboveThresholdPricing
377+
? (pricing.cacheReadInputCostPerTokenAboveThreshold ?? pricing.cacheReadInputCostPerToken)
378+
: pricing.cacheReadInputCostPerToken
379+
380+
if cached > 0, cacheRate == nil {
381+
return nil
382+
}
383+
return Double(nonCached) * inputRate
384+
+ Double(cached) * (cacheRate ?? 0)
385+
+ Double(max(0, outputTokens)) * outputRate
282386
}
283387

284388
static func claudeCostUSD(

Tests/CodexBarTests/CostUsagePricingTests.swift

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,21 @@ struct CostUsagePricingTests {
66
@Test
77
func normalizesCodexModelVariantsExactly() {
88
#expect(CostUsagePricing.normalizeCodexModel("openai/gpt-5-codex") == "gpt-5-codex")
9+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5-codex-mini") == "gpt-5-codex-mini")
10+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5-2025-10-06") == "gpt-5")
11+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5-chat") == "gpt-5-chat")
12+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5-mini-2025-10-06") == "gpt-5-mini")
13+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5-pro-2025-10-06") == "gpt-5-pro")
914
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.2-codex") == "gpt-5.2-codex")
15+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.2-chat") == "gpt-5.2-chat")
16+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.2-pro") == "gpt-5.2-pro")
1017
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.1-codex-max") == "gpt-5.1-codex-max")
18+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.1-codex-mini") == "gpt-5.1-codex-mini")
1119
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.4-pro-2026-03-05") == "gpt-5.4-pro")
1220
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.3-codex-2026-03-05") == "gpt-5.3-codex")
21+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.3-codex-max") == "gpt-5.3")
22+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.3-chat-latest") == "gpt-5.3-chat-latest")
23+
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.4-2025-10-06") == "gpt-5.4")
1324
#expect(CostUsagePricing.normalizeCodexModel("gpt-5.3-codex-spark") == "gpt-5.3-codex-spark")
1425
}
1526

@@ -24,15 +35,128 @@ struct CostUsagePricingTests {
2435
}
2536

2637
@Test
27-
func codexCostSupportsGpt53Codex() {
38+
func codexMiniCostsLessThanBaseModel() throws {
39+
let baseCost = try #require(CostUsagePricing.codexCostUSD(
40+
model: "gpt-5",
41+
inputTokens: 100,
42+
cachedInputTokens: 10,
43+
outputTokens: 5))
44+
let miniCost = try #require(CostUsagePricing.codexCostUSD(
45+
model: "gpt-5-codex-mini",
46+
inputTokens: 100,
47+
cachedInputTokens: 10,
48+
outputTokens: 5))
49+
#expect(miniCost < baseCost)
50+
}
51+
52+
@Test
53+
func codexCostSupportsGpt53CodexMaxFallback() {
2854
let cost = CostUsagePricing.codexCostUSD(
29-
model: "gpt-5.3-codex",
55+
model: "gpt-5.3-codex-max",
3056
inputTokens: 100,
3157
cachedInputTokens: 10,
3258
outputTokens: 5)
3359
#expect(cost != nil)
3460
}
3561

62+
@Test
63+
func codexCostSupportsGpt54Base() {
64+
let cost = CostUsagePricing.codexCostUSD(
65+
model: "gpt-5.4",
66+
inputTokens: 100,
67+
cachedInputTokens: 10,
68+
outputTokens: 5)
69+
#expect(cost != nil)
70+
}
71+
72+
@Test
73+
func codexCostSupportsGpt5MiniAndChatAliases() {
74+
#expect(CostUsagePricing.codexCostUSD(
75+
model: "gpt-5-codex-mini",
76+
inputTokens: 100,
77+
cachedInputTokens: 10,
78+
outputTokens: 5) != nil)
79+
#expect(CostUsagePricing.codexCostUSD(
80+
model: "gpt-5-chat",
81+
inputTokens: 100,
82+
cachedInputTokens: 10,
83+
outputTokens: 5) != nil)
84+
#expect(CostUsagePricing.codexCostUSD(
85+
model: "gpt-5.2-chat",
86+
inputTokens: 100,
87+
cachedInputTokens: 10,
88+
outputTokens: 5) != nil)
89+
}
90+
91+
@Test
92+
func codexCostSupportsSnapshotModels() {
93+
#expect(CostUsagePricing.codexCostUSD(
94+
model: "gpt-5-mini-2025-10-06",
95+
inputTokens: 100,
96+
cachedInputTokens: 10,
97+
outputTokens: 5) != nil)
98+
#expect(CostUsagePricing.codexCostUSD(
99+
model: "gpt-5-pro-2025-10-06",
100+
inputTokens: 100,
101+
cachedInputTokens: 0,
102+
outputTokens: 5) != nil)
103+
#expect(CostUsagePricing.codexCostUSD(
104+
model: "gpt-5.4-2025-10-06",
105+
inputTokens: 100,
106+
cachedInputTokens: 10,
107+
outputTokens: 5) != nil)
108+
}
109+
110+
@Test
111+
func codexCostSupportsProModelsWithoutCachedReads() {
112+
#expect(CostUsagePricing.codexCostUSD(
113+
model: "gpt-5.2-pro",
114+
inputTokens: 100,
115+
cachedInputTokens: 0,
116+
outputTokens: 5) != nil)
117+
#expect(CostUsagePricing.codexCostUSD(
118+
model: "gpt-5.4-pro",
119+
inputTokens: 100,
120+
cachedInputTokens: 0,
121+
outputTokens: 5) != nil)
122+
}
123+
124+
@Test
125+
func codexCostUsesStandardGpt54RatesForLocalScans() throws {
126+
let cost = try #require(CostUsagePricing.codexCostUSD(
127+
model: "gpt-5.4",
128+
inputTokens: 300_000,
129+
cachedInputTokens: 10000,
130+
outputTokens: 0))
131+
let expected = Double(290_000) * 2.5e-6 + Double(10000) * 2.5e-7
132+
#expect(cost == expected)
133+
}
134+
135+
@Test
136+
func codexCostUsesStandardGpt54ProRatesForLocalScans() throws {
137+
let cost = try #require(CostUsagePricing.codexCostUSD(
138+
model: "gpt-5.4-pro",
139+
inputTokens: 300_000,
140+
cachedInputTokens: 0,
141+
outputTokens: 100))
142+
let expected = Double(300_000) * 3e-5 + Double(100) * 1.8e-4
143+
#expect(cost == expected)
144+
}
145+
146+
@Test
147+
func codexCostReturnsNilForProModelCachedReads() {
148+
#expect(CostUsagePricing.codexCostUSD(
149+
model: "gpt-5.2-pro",
150+
inputTokens: 100,
151+
cachedInputTokens: 10,
152+
outputTokens: 5) == nil)
153+
#expect(CostUsagePricing.codexCostUSD(
154+
model: "gpt-5.4-pro",
155+
inputTokens: 100,
156+
cachedInputTokens: 10,
157+
outputTokens: 5) == nil)
158+
}
159+
36160
@Test
37161
func codexCostReturnsZeroForResearchPreviewModel() {
38162
let cost = CostUsagePricing.codexCostUSD(

0 commit comments

Comments
 (0)