diff --git a/Sources/CodexRunwayCore/CostScanner.swift b/Sources/CodexRunwayCore/CostScanner.swift index d5f8c18..db10a2c 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-08-13" + public static let version = "openai-builtin-2026-09-11" public struct Price: Codable, Equatable, Sendable { var inputPerMillion: Decimal @@ -380,6 +380,15 @@ public enum PricingTable { } static let builtInPrices: [String: Price] = [ + "gpt-6-astra": Price( + inputPerMillion: 10, + cachedInputPerMillion: 1, + cacheWritePerMillion: 12.5, + outputPerMillion: 50, + longContextInputPerMillion: 20, + longContextCachedInputPerMillion: 2, + longContextCacheWritePerMillion: 25, + longContextOutputPerMillion: 75), "gpt-5.6": Price( inputPerMillion: 5, cachedInputPerMillion: 0.5, diff --git a/Tests/CodexRunwayCoreTests/CostScannerTests.swift b/Tests/CodexRunwayCoreTests/CostScannerTests.swift index 269ee35..d973125 100644 --- a/Tests/CodexRunwayCoreTests/CostScannerTests.swift +++ b/Tests/CodexRunwayCoreTests/CostScannerTests.swift @@ -814,8 +814,10 @@ struct CostScannerTests { #expect(loaded == summary) } - @Test("cost cache rejects a stale pricing version") - func costCacheRejectsStalePricingVersion() throws { + @Test("cost cache rejects a stale pricing version", arguments: [ + "stale-pricing-version", "openai-builtin-2026-08-13", + ]) + func costCacheRejectsStalePricingVersion(pricingVersion: String) throws { let root = try TemporaryDirectory() let cacheURL = root.url.appending(path: "api-equivalent-cost.json") let store = UsageCostCacheStore(cacheURL: cacheURL) @@ -830,7 +832,7 @@ struct CostScannerTests { clientRows: [], rawCredits: 0, warnings: [], - pricingVersion: "stale-pricing-version", + pricingVersion: pricingVersion, calculatedAt: Date(timeIntervalSince1970: 60)) try store.save(summary) diff --git a/Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift b/Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift index 0a3e1e2..ddbb317 100644 --- a/Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift +++ b/Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift @@ -7,9 +7,18 @@ struct OpenAIPricingCatalogTests { @Test("parses the official Standard pricing Markdown table") func parsesStandardPricingTable() throws { let prices = try OpenAIPricingMarkdownParser.parseStandardPrices(Self.markdown) + let astra = try #require(prices["gpt-6-astra"]) let sol = try #require(prices["gpt-5.6-sol"]) let terra = try #require(prices["gpt-5.6-terra"]) + #expect(astra.inputPerMillion == 10) + #expect(astra.cachedInputPerMillion == 1) + #expect(astra.cacheWritePerMillion == 12.5) + #expect(astra.outputPerMillion == 50) + #expect(astra.longContextInputPerMillion == 20) + #expect(astra.longContextCachedInputPerMillion == 2) + #expect(astra.longContextCacheWritePerMillion == 25) + #expect(astra.longContextOutputPerMillion == 75) #expect(sol.inputPerMillion == 5) #expect(sol.cachedInputPerMillion == 0.5) #expect(sol.cacheWritePerMillion == 6.25) @@ -31,7 +40,9 @@ struct OpenAIPricingCatalogTests { fetch: { _ in OpenAIPricingHTTPResponse( statusCode: 200, - data: Data(Self.markdown.utf8), + data: Data(Self.markdown.replacingOccurrences( + of: "| gpt-6-astra | $10.00 |", + with: "| gpt-6-astra | $12.00 |").utf8), eTag: #""official-etag""#, lastModified: "Thu, 13 Aug 2026 04:57:43 GMT") }) @@ -47,9 +58,47 @@ 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(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 { + 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 |") } + .joined(separator: "\n") + let initial = OpenAIPricingCatalogProvider( + cacheURL: cacheURL, + fetch: { _ in + OpenAIPricingHTTPResponse( + statusCode: 200, + data: Data(olderMarkdown.utf8), + eTag: nil, + lastModified: nil) + }) + _ = await initial.priceBook(now: fetchedAt) + #expect(FileManager.default.fileExists(atPath: cacheURL.path)) + } + + let offline = OpenAIPricingCatalogProvider( + cacheURL: cacheURL, + fetch: { _ in throw URLError(.notConnectedToInternet) }) + let priceBook = await offline.priceBook(now: fetchedAt.addingTimeInterval(86_400)) + let totals = ApiEquivalentTotals( + totalTokens: 11_000, + uncachedInputTokens: 8_000, + cachedInputTokens: 2_000, + outputTokens: 1_000, + turns: 1, + threads: 1) + + #expect(priceBook.cost(model: "gpt-6-astra", totals: totals) == Decimal(string: "0.132")) + } + @Test("falls back to the cached catalog when refresh fails") func staleCacheFallback() async throws { let directory = try PricingTestDirectory() @@ -79,6 +128,8 @@ struct OpenAIPricingCatalogTests { @Test("bundled lookup is exact except for dated snapshots") func exactModelLookup() { + #expect(PricingTable.price(for: "gpt-6") == nil) + #expect(PricingTable.price(for: "gpt-6-astra-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) @@ -95,6 +146,7 @@ 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-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 | - | - | - | - | @@ -103,6 +155,7 @@ 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 | $5.00 | $0.50 | $6.25 | $25.00 | $10.00 | $1.00 | $12.50 | $37.50 | | gpt-5.6-sol | $2.50 | $0.25 | $3.125 | $15.00 | $5.00 | $0.50 | $6.25 | $22.50 | """ } diff --git a/Tests/CodexRunwayCoreTests/UsageCostRepositoryAggregationTests.swift b/Tests/CodexRunwayCoreTests/UsageCostRepositoryAggregationTests.swift index 9cfd355..9c77bc8 100644 --- a/Tests/CodexRunwayCoreTests/UsageCostRepositoryAggregationTests.swift +++ b/Tests/CodexRunwayCoreTests/UsageCostRepositoryAggregationTests.swift @@ -4,6 +4,35 @@ 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 ", + ]) + func astraPricingAcrossLocalScanners(model: 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"}} + \(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") + 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")! + + for summary in [indexed, streamed] { + #expect(summary.estimatedUSD == expectedCost) + #expect(summary.modelRows.first?.estimatedUSD == expectedCost) + #expect(summary.confidence == .priced) + #expect(summary.warnings.isEmpty) + #expect(summary.pricingVersion == PricingTable.version) + } + #expect(recent.estimatedUSD == expectedCost) + } + @Test("repository aggregation matches the streaming scanner field by field") func repositoryMatchesStreamingScanner() async throws { let fixture = try RepositoryFixture()