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
11 changes: 10 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-08-13"
public static let version = "openai-builtin-2026-09-11"

public struct Price: Codable, Equatable, Sendable {
var inputPerMillion: Decimal
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions Tests/CodexRunwayCoreTests/CostScannerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -830,7 +832,7 @@ struct CostScannerTests {
clientRows: [],
rawCredits: 0,
warnings: [],
pricingVersion: "stale-pricing-version",
pricingVersion: pricingVersion,
calculatedAt: Date(timeIntervalSince1970: 60))

try store.save(summary)
Expand Down
55 changes: 54 additions & 1 deletion Tests/CodexRunwayCoreTests/OpenAIPricingCatalogTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
})
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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 | - | - | - | - |
Expand All @@ -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 |
"""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down