From 3deaa645670e14a3033cfe3136f2eb6dbecd77d1 Mon Sep 17 00:00:00 2001 From: catouse Date: Sat, 26 Sep 2026 20:17:21 +0800 Subject: [PATCH] feat(pricing): add GPT-6 Sol and Luna prices --- Sources/CodexRunwayCore/CostScanner.swift | 20 ++++++++++++- .../CostScannerTests.swift | 2 +- .../OpenAIPricingCatalogTests.swift | 28 +++++++++++++------ .../UsageCostRepositoryAggregationTests.swift | 14 ++++++---- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/Sources/CodexRunwayCore/CostScanner.swift b/Sources/CodexRunwayCore/CostScanner.swift index db10a2c..ec4c654 100644 --- a/Sources/CodexRunwayCore/CostScanner.swift +++ b/Sources/CodexRunwayCore/CostScanner.swift @@ -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 @@ -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, diff --git a/Tests/CodexRunwayCoreTests/CostScannerTests.swift b/Tests/CodexRunwayCoreTests/CostScannerTests.swift index d973125..7d0c1d4 100644 --- a/Tests/CodexRunwayCoreTests/CostScannerTests.swift +++ b/Tests/CodexRunwayCoreTests/CostScannerTests.swift @@ -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() diff --git a/Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift b/Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift index ddbb317..5943fa3 100644 --- a/Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift +++ b/Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift @@ -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"]) @@ -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) @@ -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) @@ -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") }) @@ -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, @@ -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") @@ -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) @@ -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 | - | - | - | - | diff --git a/Tests/CodexRunwayCoreTests/UsageCostRepositoryAggregationTests.swift b/Tests/CodexRunwayCoreTests/UsageCostRepositoryAggregationTests.swift index 9c77bc8..8292390 100644 --- a/Tests/CodexRunwayCoreTests/UsageCostRepositoryAggregationTests.swift +++ b/Tests/CodexRunwayCoreTests/UsageCostRepositoryAggregationTests.swift @@ -4,16 +4,18 @@ 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]) @@ -21,7 +23,7 @@ struct UsageCostRepositoryAggregationTests { 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)