Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Sources/CodexRunwayCore/CostScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ extension ApiEquivalentTotals {

public enum PricingTable {
/// Bundled fallback verified against the official OpenAI pricing documentation.
public static let version = "openai-builtin-2026-09-11"
public static let version = "openai-builtin-2026-09-26"

public struct Price: Codable, Equatable, Sendable {
var inputPerMillion: Decimal
Expand Down Expand Up @@ -389,6 +389,24 @@ public enum PricingTable {
longContextCachedInputPerMillion: 2,
longContextCacheWritePerMillion: 25,
longContextOutputPerMillion: 75),
"gpt-6-sol": Price(
inputPerMillion: 2,
cachedInputPerMillion: 0.2,
cacheWritePerMillion: 2.5,
outputPerMillion: 10,
longContextInputPerMillion: 4,
longContextCachedInputPerMillion: 0.4,
longContextCacheWritePerMillion: 5,
longContextOutputPerMillion: 15),
"gpt-6-luna": Price(
inputPerMillion: 0.1,
cachedInputPerMillion: 0.01,
cacheWritePerMillion: 0.125,
outputPerMillion: 0.5,
longContextInputPerMillion: 0.2,
longContextCachedInputPerMillion: 0.02,
longContextCacheWritePerMillion: 0.25,
longContextOutputPerMillion: 0.75),
"gpt-5.6": Price(
inputPerMillion: 5,
cachedInputPerMillion: 0.5,
Expand Down
2 changes: 1 addition & 1 deletion Tests/CodexRunwayCoreTests/CostScannerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ struct CostScannerTests {
}

@Test("cost cache rejects a stale pricing version", arguments: [
"stale-pricing-version", "openai-builtin-2026-08-13",
"stale-pricing-version", "openai-builtin-2026-08-13", "openai-builtin-2026-09-11",
])
func costCacheRejectsStalePricingVersion(pricingVersion: String) throws {
let root = try TemporaryDirectory()
Expand Down
28 changes: 20 additions & 8 deletions Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ struct OpenAIPricingCatalogTests {
func parsesStandardPricingTable() throws {
let prices = try OpenAIPricingMarkdownParser.parseStandardPrices(Self.markdown)
let astra = try #require(prices["gpt-6-astra"])
let sol6 = try #require(prices["gpt-6-sol"])
let luna6 = try #require(prices["gpt-6-luna"])
let sol = try #require(prices["gpt-5.6-sol"])
let terra = try #require(prices["gpt-5.6-terra"])

Expand All @@ -19,6 +21,8 @@ struct OpenAIPricingCatalogTests {
#expect(astra.longContextCachedInputPerMillion == 2)
#expect(astra.longContextCacheWritePerMillion == 25)
#expect(astra.longContextOutputPerMillion == 75)
#expect(PricingTable.price(for: "gpt-6-sol") == sol6)
#expect(PricingTable.price(for: "gpt-6-luna") == luna6)
#expect(sol.inputPerMillion == 5)
#expect(sol.cachedInputPerMillion == 0.5)
#expect(sol.cacheWritePerMillion == 6.25)
Expand All @@ -29,8 +33,10 @@ struct OpenAIPricingCatalogTests {
#expect(prices["unrelated-provider"] == nil)
}

@Test("uses a downloaded catalog and writes a last-known-good cache")
func downloadsAndCachesCatalog() async throws {
@Test("uses a downloaded catalog and writes a last-known-good cache", arguments: [
("gpt-6-astra", "10.00"), ("gpt-6-sol", "2.00"), ("gpt-6-luna", "0.10"),
])
func downloadsAndCachesCatalog(model: String, bundledInput: String) async throws {
let directory = try PricingTestDirectory()
let cacheURL = directory.url.appendingPathComponent("pricing.json")
let now = Date(timeIntervalSince1970: 1_786_579_200)
Expand All @@ -41,8 +47,8 @@ struct OpenAIPricingCatalogTests {
OpenAIPricingHTTPResponse(
statusCode: 200,
data: Data(Self.markdown.replacingOccurrences(
of: "| gpt-6-astra | $10.00 |",
with: "| gpt-6-astra | $12.00 |").utf8),
of: "| \(model) | $\(bundledInput) |",
with: "| \(model) | $12.00 |").utf8),
eTag: #""official-etag""#,
lastModified: "Thu, 13 Aug 2026 04:57:43 GMT")
})
Expand All @@ -58,18 +64,18 @@ struct OpenAIPricingCatalogTests {

#expect(priceBook.version.hasPrefix("openai-docs-"))
#expect(priceBook.cost(model: "gpt-5.6-terra", totals: oneMillionInput) == 2)
#expect(priceBook.cost(model: "gpt-6-astra", totals: oneMillionInput) == 12)
#expect(priceBook.cost(model: model, totals: oneMillionInput) == 12)
#expect(FileManager.default.fileExists(atPath: cacheURL.path))
}

@Test("bundled Astra pricing works offline with no cache or a pre-Astra cache", arguments: [false, true])
func bundledAstraOfflineFallback(useOlderCache: Bool) async throws {
@Test("bundled GPT-6 pricing works offline with no cache or an older cache", arguments: [false, true])
func bundledGPT6OfflineFallback(useOlderCache: Bool) async throws {
let directory = try PricingTestDirectory()
let cacheURL = directory.url.appendingPathComponent("pricing.json")
let fetchedAt = Date(timeIntervalSince1970: 1_786_579_200)
if useOlderCache {
let olderMarkdown = Self.markdown.split(separator: "\n")
.filter { !$0.contains("| gpt-6-astra |") }
.filter { !$0.contains("| gpt-6-") }
.joined(separator: "\n")
let initial = OpenAIPricingCatalogProvider(
cacheURL: cacheURL,
Expand Down Expand Up @@ -97,6 +103,8 @@ struct OpenAIPricingCatalogTests {
threads: 1)

#expect(priceBook.cost(model: "gpt-6-astra", totals: totals) == Decimal(string: "0.132"))
#expect(priceBook.cost(model: "gpt-6-sol", totals: totals) == Decimal(string: "0.0264"))
#expect(priceBook.cost(model: "gpt-6-luna", totals: totals) == Decimal(string: "0.00132"))
}

@Test("falls back to the cached catalog when refresh fails")
Expand Down Expand Up @@ -130,6 +138,8 @@ struct OpenAIPricingCatalogTests {
func exactModelLookup() {
#expect(PricingTable.price(for: "gpt-6") == nil)
#expect(PricingTable.price(for: "gpt-6-astra-mini") == nil)
#expect(PricingTable.price(for: "gpt-6-solstice") == nil)
#expect(PricingTable.price(for: "gpt-6-luna-mini") == nil)
#expect(PricingTable.price(for: "gpt-5.6-sol")?.inputPerMillion == 5)
#expect(PricingTable.price(for: "gpt-5.6-sol-2026-08-13")?.inputPerMillion == 5)
#expect(PricingTable.price(for: "gpt-5.6-solstice") == nil)
Expand All @@ -147,6 +157,8 @@ struct OpenAIPricingCatalogTests {
| Model | Short context input | Short context cached input | Short context cache writes | Short context output | Long context input | Long context cached input | Long context cache writes | Long context output |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| gpt-6-astra | $10.00 | $1.00 | $12.50 | $50.00 | $20.00 | $2.00 | $25.00 | $75.00 |
| gpt-6-sol | $2.00 | $0.20 | $2.50 | $10.00 | $4.00 | $0.40 | $5.00 | $15.00 |
| gpt-6-luna | $0.10 | $0.01 | $0.125 | $0.50 | $0.20 | $0.02 | $0.25 | $0.75 |
| gpt-5.6-sol | $5.00 | $0.50 | $6.25 | $30.00 | $10.00 | $1.00 | $12.50 | $45.00 |
| gpt-5.6-terra | $2.00 | $0.20 | $2.50 | $12.00 | $4.00 | $0.40 | $5.00 | $18.00 |
| unrelated-provider | $1.00 | $0.10 | - | $2.00 | - | - | - | - |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,26 @@ import Testing

@Suite("Usage cost repository — aggregation")
struct UsageCostRepositoryAggregationTests {
@Test("Astra usage is priced in the repository, streaming scanner, and recent sessions", arguments: [
"gpt-6-astra", "gpt-6-astra-2026-09-10", " GPT-6-ASTRA ",
@Test("GPT-6 usage is priced in the repository, streaming scanner, and recent sessions", arguments: [
("gpt-6-astra", "0.132"), ("gpt-6-astra-2026-09-10", "0.132"), (" GPT-6-ASTRA ", "0.132"),
("gpt-6-sol", "0.0264"), ("gpt-6-sol-2026-09-26", "0.0264"), (" GPT-6-SOL ", "0.0264"),
("gpt-6-luna", "0.00132"), ("gpt-6-luna-2026-09-26", "0.00132"), (" GPT-6-LUNA ", "0.00132"),
])
func astraPricingAcrossLocalScanners(model: String) async throws {
func gpt6PricingAcrossLocalScanners(model: String, expectedUSD: String) async throws {
let fixture = try RepositoryFixture()
let contents = """
{"timestamp":"2026-06-29T00:00:00Z","type":"session_meta","payload":{"id":"astra-session","cwd":"/tmp/astra-project"}}
{"timestamp":"2026-06-29T00:00:00Z","type":"session_meta","payload":{"id":"gpt6-session","cwd":"/tmp/gpt6-project"}}
\(tokenLine(timestamp: "2026-06-29T01:00:00Z", input: 10_000, cached: 2_000, output: 1_000, model: model))
"""
try fixture.write(contents, basename: "rollout-astra.jsonl")
try fixture.write(contents, basename: "rollout-gpt6.jsonl")
let request = fullWindowQuery()
let indexed = try #require(try await fixture.repository().summaries(
for: [request], calculatedAt: fixedNow, policy: .ifChanged)[request.id])
let streamed = try UsageCostScanner(codexHome: fixture.codexHome).scanAPIEquivalent(
window: request.window, calculatedAt: fixedNow)
let activity = try SessionActivityScanner(codexHome: fixture.codexHome).scan(limit: 1)
let recent = try #require(activity.items.first)
let expectedCost = Decimal(string: "0.132")!
let expectedCost = try #require(Decimal(string: expectedUSD))

for summary in [indexed, streamed] {
#expect(summary.estimatedUSD == expectedCost)
Expand Down