812 lines
36 KiB
Swift
812 lines
36 KiB
Swift
import Foundation
|
|
|
|
enum CostUsagePricing {
|
|
private static let codexPriorityInputTokenLimit = 272_000
|
|
static let codexUnattributedModel = "unknown"
|
|
|
|
struct CodexPricing {
|
|
let inputCostPerToken: Double
|
|
let outputCostPerToken: Double
|
|
let cacheReadInputCostPerToken: Double?
|
|
/// Optional cache-write (cache creation) rate. When nil, write tokens are billed at the
|
|
/// uncached input rate (legacy Codex folding behavior).
|
|
let cacheWriteInputCostPerToken: Double?
|
|
let displayLabel: String?
|
|
|
|
let thresholdTokens: Int?
|
|
let inputCostPerTokenAboveThreshold: Double?
|
|
let outputCostPerTokenAboveThreshold: Double?
|
|
let cacheReadInputCostPerTokenAboveThreshold: Double?
|
|
let cacheWriteInputCostPerTokenAboveThreshold: Double?
|
|
let priorityInputCostPerToken: Double?
|
|
let priorityOutputCostPerToken: Double?
|
|
let priorityCacheReadInputCostPerToken: Double?
|
|
let priorityCacheWriteInputCostPerToken: Double?
|
|
|
|
init(
|
|
inputCostPerToken: Double,
|
|
outputCostPerToken: Double,
|
|
cacheReadInputCostPerToken: Double?,
|
|
displayLabel: String?,
|
|
cacheWriteInputCostPerToken: Double? = nil,
|
|
thresholdTokens: Int? = nil,
|
|
inputCostPerTokenAboveThreshold: Double? = nil,
|
|
outputCostPerTokenAboveThreshold: Double? = nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: Double? = nil,
|
|
cacheWriteInputCostPerTokenAboveThreshold: Double? = nil,
|
|
priorityInputCostPerToken: Double? = nil,
|
|
priorityOutputCostPerToken: Double? = nil,
|
|
priorityCacheReadInputCostPerToken: Double? = nil,
|
|
priorityCacheWriteInputCostPerToken: Double? = nil)
|
|
{
|
|
self.inputCostPerToken = inputCostPerToken
|
|
self.outputCostPerToken = outputCostPerToken
|
|
self.cacheReadInputCostPerToken = cacheReadInputCostPerToken
|
|
self.cacheWriteInputCostPerToken = cacheWriteInputCostPerToken
|
|
self.displayLabel = displayLabel
|
|
self.thresholdTokens = thresholdTokens
|
|
self.inputCostPerTokenAboveThreshold = inputCostPerTokenAboveThreshold
|
|
self.outputCostPerTokenAboveThreshold = outputCostPerTokenAboveThreshold
|
|
self.cacheReadInputCostPerTokenAboveThreshold = cacheReadInputCostPerTokenAboveThreshold
|
|
self.cacheWriteInputCostPerTokenAboveThreshold = cacheWriteInputCostPerTokenAboveThreshold
|
|
self.priorityInputCostPerToken = priorityInputCostPerToken
|
|
self.priorityOutputCostPerToken = priorityOutputCostPerToken
|
|
self.priorityCacheReadInputCostPerToken = priorityCacheReadInputCostPerToken
|
|
self.priorityCacheWriteInputCostPerToken = priorityCacheWriteInputCostPerToken
|
|
}
|
|
}
|
|
|
|
struct ClaudePricing {
|
|
let inputCostPerToken: Double
|
|
let outputCostPerToken: Double
|
|
let cacheCreationInputCostPerToken: Double
|
|
let cacheReadInputCostPerToken: Double
|
|
|
|
let thresholdTokens: Int?
|
|
let inputCostPerTokenAboveThreshold: Double?
|
|
let outputCostPerTokenAboveThreshold: Double?
|
|
let cacheCreationInputCostPerTokenAboveThreshold: Double?
|
|
let cacheReadInputCostPerTokenAboveThreshold: Double?
|
|
}
|
|
|
|
private struct ClaudeCostTokens {
|
|
let input: Int
|
|
let cacheRead: Int
|
|
let cacheCreation: Int
|
|
let cacheCreation1h: Int
|
|
let output: Int
|
|
}
|
|
|
|
private static let codex: [String: CodexPricing] = [
|
|
"gpt-5": CodexPricing(
|
|
inputCostPerToken: 1.25e-6,
|
|
outputCostPerToken: 1e-5,
|
|
cacheReadInputCostPerToken: 1.25e-7,
|
|
displayLabel: nil),
|
|
"gpt-5-codex": CodexPricing(
|
|
inputCostPerToken: 1.25e-6,
|
|
outputCostPerToken: 1e-5,
|
|
cacheReadInputCostPerToken: 1.25e-7,
|
|
displayLabel: nil),
|
|
"gpt-5-mini": CodexPricing(
|
|
inputCostPerToken: 2.5e-7,
|
|
outputCostPerToken: 2e-6,
|
|
cacheReadInputCostPerToken: 2.5e-8,
|
|
displayLabel: nil),
|
|
"gpt-5-nano": CodexPricing(
|
|
inputCostPerToken: 5e-8,
|
|
outputCostPerToken: 4e-7,
|
|
cacheReadInputCostPerToken: 5e-9,
|
|
displayLabel: nil),
|
|
"gpt-5-pro": CodexPricing(
|
|
inputCostPerToken: 1.5e-5,
|
|
outputCostPerToken: 1.2e-4,
|
|
cacheReadInputCostPerToken: nil,
|
|
displayLabel: nil),
|
|
"gpt-5.1": CodexPricing(
|
|
inputCostPerToken: 1.25e-6,
|
|
outputCostPerToken: 1e-5,
|
|
cacheReadInputCostPerToken: 1.25e-7,
|
|
displayLabel: nil),
|
|
"gpt-5.1-codex": CodexPricing(
|
|
inputCostPerToken: 1.25e-6,
|
|
outputCostPerToken: 1e-5,
|
|
cacheReadInputCostPerToken: 1.25e-7,
|
|
displayLabel: nil),
|
|
"gpt-5.1-codex-max": CodexPricing(
|
|
inputCostPerToken: 1.25e-6,
|
|
outputCostPerToken: 1e-5,
|
|
cacheReadInputCostPerToken: 1.25e-7,
|
|
displayLabel: nil),
|
|
"gpt-5.1-codex-mini": CodexPricing(
|
|
inputCostPerToken: 2.5e-7,
|
|
outputCostPerToken: 2e-6,
|
|
cacheReadInputCostPerToken: 2.5e-8,
|
|
displayLabel: nil),
|
|
"gpt-5.2": CodexPricing(
|
|
inputCostPerToken: 1.75e-6,
|
|
outputCostPerToken: 1.4e-5,
|
|
cacheReadInputCostPerToken: 1.75e-7,
|
|
displayLabel: nil),
|
|
"gpt-5.2-codex": CodexPricing(
|
|
inputCostPerToken: 1.75e-6,
|
|
outputCostPerToken: 1.4e-5,
|
|
cacheReadInputCostPerToken: 1.75e-7,
|
|
displayLabel: nil),
|
|
"gpt-5.2-pro": CodexPricing(
|
|
inputCostPerToken: 2.1e-5,
|
|
outputCostPerToken: 1.68e-4,
|
|
cacheReadInputCostPerToken: nil,
|
|
displayLabel: nil),
|
|
"gpt-5.3-codex": CodexPricing(
|
|
inputCostPerToken: 1.75e-6,
|
|
outputCostPerToken: 1.4e-5,
|
|
cacheReadInputCostPerToken: 1.75e-7,
|
|
displayLabel: nil),
|
|
"gpt-5.3-codex-spark": CodexPricing(
|
|
inputCostPerToken: 0,
|
|
outputCostPerToken: 0,
|
|
cacheReadInputCostPerToken: 0,
|
|
displayLabel: "Research Preview"),
|
|
"gpt-5.4": CodexPricing(
|
|
inputCostPerToken: 2.5e-6,
|
|
outputCostPerToken: 1.5e-5,
|
|
cacheReadInputCostPerToken: 2.5e-7,
|
|
displayLabel: nil,
|
|
thresholdTokens: 272_000,
|
|
inputCostPerTokenAboveThreshold: 5e-6,
|
|
outputCostPerTokenAboveThreshold: 2.25e-5,
|
|
cacheReadInputCostPerTokenAboveThreshold: 5e-7,
|
|
priorityInputCostPerToken: 5e-6,
|
|
priorityOutputCostPerToken: 3e-5,
|
|
priorityCacheReadInputCostPerToken: 5e-7),
|
|
"gpt-5.4-mini": CodexPricing(
|
|
inputCostPerToken: 7.5e-7,
|
|
outputCostPerToken: 4.5e-6,
|
|
cacheReadInputCostPerToken: 7.5e-8,
|
|
displayLabel: nil,
|
|
priorityInputCostPerToken: 1.5e-6,
|
|
priorityOutputCostPerToken: 9e-6,
|
|
priorityCacheReadInputCostPerToken: 1.5e-7),
|
|
"gpt-5.4-nano": CodexPricing(
|
|
inputCostPerToken: 2e-7,
|
|
outputCostPerToken: 1.25e-6,
|
|
cacheReadInputCostPerToken: 2e-8,
|
|
displayLabel: nil),
|
|
"gpt-5.4-pro": CodexPricing(
|
|
inputCostPerToken: 3e-5,
|
|
outputCostPerToken: 1.8e-4,
|
|
cacheReadInputCostPerToken: nil,
|
|
displayLabel: nil),
|
|
"gpt-5.5": CodexPricing(
|
|
inputCostPerToken: 5e-6,
|
|
outputCostPerToken: 3e-5,
|
|
cacheReadInputCostPerToken: 5e-7,
|
|
displayLabel: nil,
|
|
thresholdTokens: 272_000,
|
|
inputCostPerTokenAboveThreshold: 1e-5,
|
|
outputCostPerTokenAboveThreshold: 4.5e-5,
|
|
cacheReadInputCostPerTokenAboveThreshold: 1e-6,
|
|
priorityInputCostPerToken: 1.25e-5,
|
|
priorityOutputCostPerToken: 7.5e-5,
|
|
priorityCacheReadInputCostPerToken: 1.25e-6),
|
|
"gpt-5.5-pro": CodexPricing(
|
|
inputCostPerToken: 3e-5,
|
|
outputCostPerToken: 1.8e-4,
|
|
cacheReadInputCostPerToken: nil,
|
|
displayLabel: nil),
|
|
// GPT-5.6 Sol/Terra/Luna (OpenAI pricing page + model cards).
|
|
// Long context: prompts with >272K input tokens are 2x input / 1.5x output for the full
|
|
// request. Cache writes: 1.25x uncached input. Priority rates are explicit because support
|
|
// and multipliers are provider contracts, not properties that can be inferred from Standard.
|
|
"gpt-5.6-sol": CodexPricing(
|
|
inputCostPerToken: 5e-6,
|
|
outputCostPerToken: 3e-5,
|
|
cacheReadInputCostPerToken: 5e-7,
|
|
displayLabel: nil,
|
|
cacheWriteInputCostPerToken: 6.25e-6,
|
|
thresholdTokens: 272_000,
|
|
inputCostPerTokenAboveThreshold: 1e-5,
|
|
outputCostPerTokenAboveThreshold: 4.5e-5,
|
|
cacheReadInputCostPerTokenAboveThreshold: 1e-6,
|
|
cacheWriteInputCostPerTokenAboveThreshold: 1.25e-5,
|
|
priorityInputCostPerToken: 1e-5,
|
|
priorityOutputCostPerToken: 6e-5,
|
|
priorityCacheReadInputCostPerToken: 1e-6,
|
|
priorityCacheWriteInputCostPerToken: 1.25e-5),
|
|
"gpt-5.6-terra": CodexPricing(
|
|
inputCostPerToken: 2.5e-6,
|
|
outputCostPerToken: 1.5e-5,
|
|
cacheReadInputCostPerToken: 2.5e-7,
|
|
displayLabel: nil,
|
|
cacheWriteInputCostPerToken: 3.125e-6,
|
|
thresholdTokens: 272_000,
|
|
inputCostPerTokenAboveThreshold: 5e-6,
|
|
outputCostPerTokenAboveThreshold: 2.25e-5,
|
|
cacheReadInputCostPerTokenAboveThreshold: 5e-7,
|
|
cacheWriteInputCostPerTokenAboveThreshold: 6.25e-6,
|
|
priorityInputCostPerToken: 5e-6,
|
|
priorityOutputCostPerToken: 3e-5,
|
|
priorityCacheReadInputCostPerToken: 5e-7,
|
|
priorityCacheWriteInputCostPerToken: 6.25e-6),
|
|
"gpt-5.6-luna": CodexPricing(
|
|
inputCostPerToken: 1e-6,
|
|
outputCostPerToken: 6e-6,
|
|
cacheReadInputCostPerToken: 1e-7,
|
|
displayLabel: nil,
|
|
cacheWriteInputCostPerToken: 1.25e-6,
|
|
thresholdTokens: 272_000,
|
|
inputCostPerTokenAboveThreshold: 2e-6,
|
|
outputCostPerTokenAboveThreshold: 9e-6,
|
|
cacheReadInputCostPerTokenAboveThreshold: 2e-7,
|
|
cacheWriteInputCostPerTokenAboveThreshold: 2.5e-6,
|
|
priorityInputCostPerToken: 2e-6,
|
|
priorityOutputCostPerToken: 1.2e-5,
|
|
priorityCacheReadInputCostPerToken: 2e-7,
|
|
priorityCacheWriteInputCostPerToken: 2.5e-6),
|
|
]
|
|
|
|
static func codexBuiltInPricingFingerprint() -> String {
|
|
var parts = ["priorityInputTokenLimit=\(self.codexPriorityInputTokenLimit)"]
|
|
for model in self.codex.keys.sorted() {
|
|
guard let pricing = self.codex[model] else { continue }
|
|
parts.append([
|
|
"model=\(model)",
|
|
self.optionalPricingFingerprint(pricing.inputCostPerToken),
|
|
self.optionalPricingFingerprint(pricing.outputCostPerToken),
|
|
self.optionalPricingFingerprint(pricing.cacheReadInputCostPerToken),
|
|
self.optionalPricingFingerprint(pricing.cacheWriteInputCostPerToken),
|
|
pricing.displayLabel ?? "nil",
|
|
pricing.thresholdTokens.map(String.init) ?? "nil",
|
|
self.optionalPricingFingerprint(pricing.inputCostPerTokenAboveThreshold),
|
|
self.optionalPricingFingerprint(pricing.outputCostPerTokenAboveThreshold),
|
|
self.optionalPricingFingerprint(pricing.cacheReadInputCostPerTokenAboveThreshold),
|
|
self.optionalPricingFingerprint(pricing.cacheWriteInputCostPerTokenAboveThreshold),
|
|
self.optionalPricingFingerprint(pricing.priorityInputCostPerToken),
|
|
self.optionalPricingFingerprint(pricing.priorityOutputCostPerToken),
|
|
self.optionalPricingFingerprint(pricing.priorityCacheReadInputCostPerToken),
|
|
self.optionalPricingFingerprint(pricing.priorityCacheWriteInputCostPerToken),
|
|
].joined(separator: "|"))
|
|
}
|
|
return parts.joined(separator: "\n")
|
|
}
|
|
|
|
private static func optionalPricingFingerprint(_ value: Double?) -> String {
|
|
guard let value else { return "nil" }
|
|
return String(format: "%.17g", value)
|
|
}
|
|
|
|
private static let claude: [String: ClaudePricing] = [
|
|
"claude-fable-5": ClaudePricing(
|
|
inputCostPerToken: 1e-5,
|
|
outputCostPerToken: 5e-5,
|
|
cacheCreationInputCostPerToken: 1.25e-5,
|
|
cacheReadInputCostPerToken: 1e-6,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-haiku-4-5-20251001": ClaudePricing(
|
|
inputCostPerToken: 1e-6,
|
|
outputCostPerToken: 5e-6,
|
|
cacheCreationInputCostPerToken: 1.25e-6,
|
|
cacheReadInputCostPerToken: 1e-7,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-haiku-4-5": ClaudePricing(
|
|
inputCostPerToken: 1e-6,
|
|
outputCostPerToken: 5e-6,
|
|
cacheCreationInputCostPerToken: 1.25e-6,
|
|
cacheReadInputCostPerToken: 1e-7,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-opus-4-5-20251101": ClaudePricing(
|
|
inputCostPerToken: 5e-6,
|
|
outputCostPerToken: 2.5e-5,
|
|
cacheCreationInputCostPerToken: 6.25e-6,
|
|
cacheReadInputCostPerToken: 5e-7,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-opus-4-5": ClaudePricing(
|
|
inputCostPerToken: 5e-6,
|
|
outputCostPerToken: 2.5e-5,
|
|
cacheCreationInputCostPerToken: 6.25e-6,
|
|
cacheReadInputCostPerToken: 5e-7,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-opus-4-6-20260205": ClaudePricing(
|
|
inputCostPerToken: 5e-6,
|
|
outputCostPerToken: 2.5e-5,
|
|
cacheCreationInputCostPerToken: 6.25e-6,
|
|
cacheReadInputCostPerToken: 5e-7,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-opus-4-6": ClaudePricing(
|
|
inputCostPerToken: 5e-6,
|
|
outputCostPerToken: 2.5e-5,
|
|
cacheCreationInputCostPerToken: 6.25e-6,
|
|
cacheReadInputCostPerToken: 5e-7,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-opus-4-7": ClaudePricing(
|
|
inputCostPerToken: 5e-6,
|
|
outputCostPerToken: 2.5e-5,
|
|
cacheCreationInputCostPerToken: 6.25e-6,
|
|
cacheReadInputCostPerToken: 5e-7,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-opus-4-8": ClaudePricing(
|
|
inputCostPerToken: 5e-6,
|
|
outputCostPerToken: 2.5e-5,
|
|
cacheCreationInputCostPerToken: 6.25e-6,
|
|
cacheReadInputCostPerToken: 5e-7,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-sonnet-4-5": ClaudePricing(
|
|
inputCostPerToken: 3e-6,
|
|
outputCostPerToken: 1.5e-5,
|
|
cacheCreationInputCostPerToken: 3.75e-6,
|
|
cacheReadInputCostPerToken: 3e-7,
|
|
thresholdTokens: 200_000,
|
|
inputCostPerTokenAboveThreshold: 6e-6,
|
|
outputCostPerTokenAboveThreshold: 2.25e-5,
|
|
cacheCreationInputCostPerTokenAboveThreshold: 7.5e-6,
|
|
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
|
|
"claude-sonnet-4-6": ClaudePricing(
|
|
inputCostPerToken: 3e-6,
|
|
outputCostPerToken: 1.5e-5,
|
|
cacheCreationInputCostPerToken: 3.75e-6,
|
|
cacheReadInputCostPerToken: 3e-7,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-sonnet-4-5-20250929": ClaudePricing(
|
|
inputCostPerToken: 3e-6,
|
|
outputCostPerToken: 1.5e-5,
|
|
cacheCreationInputCostPerToken: 3.75e-6,
|
|
cacheReadInputCostPerToken: 3e-7,
|
|
thresholdTokens: 200_000,
|
|
inputCostPerTokenAboveThreshold: 6e-6,
|
|
outputCostPerTokenAboveThreshold: 2.25e-5,
|
|
cacheCreationInputCostPerTokenAboveThreshold: 7.5e-6,
|
|
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
|
|
"claude-opus-4-20250514": ClaudePricing(
|
|
inputCostPerToken: 1.5e-5,
|
|
outputCostPerToken: 7.5e-5,
|
|
cacheCreationInputCostPerToken: 1.875e-5,
|
|
cacheReadInputCostPerToken: 1.5e-6,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-opus-4-1": ClaudePricing(
|
|
inputCostPerToken: 1.5e-5,
|
|
outputCostPerToken: 7.5e-5,
|
|
cacheCreationInputCostPerToken: 1.875e-5,
|
|
cacheReadInputCostPerToken: 1.5e-6,
|
|
thresholdTokens: nil,
|
|
inputCostPerTokenAboveThreshold: nil,
|
|
outputCostPerTokenAboveThreshold: nil,
|
|
cacheCreationInputCostPerTokenAboveThreshold: nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: nil),
|
|
"claude-sonnet-4-20250514": ClaudePricing(
|
|
inputCostPerToken: 3e-6,
|
|
outputCostPerToken: 1.5e-5,
|
|
cacheCreationInputCostPerToken: 3.75e-6,
|
|
cacheReadInputCostPerToken: 3e-7,
|
|
thresholdTokens: 200_000,
|
|
inputCostPerTokenAboveThreshold: 6e-6,
|
|
outputCostPerTokenAboveThreshold: 2.25e-5,
|
|
cacheCreationInputCostPerTokenAboveThreshold: 7.5e-6,
|
|
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
|
|
]
|
|
|
|
private static let claudeFullContextStandardPricingCutoff = Date(timeIntervalSince1970: 1_773_360_000)
|
|
private static let claudeHistoricalLongContext: [String: ClaudePricing] = [
|
|
"claude-opus-4-6": ClaudePricing(
|
|
inputCostPerToken: 5e-6,
|
|
outputCostPerToken: 2.5e-5,
|
|
cacheCreationInputCostPerToken: 6.25e-6,
|
|
cacheReadInputCostPerToken: 5e-7,
|
|
thresholdTokens: 200_000,
|
|
inputCostPerTokenAboveThreshold: 1e-5,
|
|
outputCostPerTokenAboveThreshold: 3.75e-5,
|
|
cacheCreationInputCostPerTokenAboveThreshold: 1.25e-5,
|
|
cacheReadInputCostPerTokenAboveThreshold: 1e-6),
|
|
"claude-sonnet-4-6": ClaudePricing(
|
|
inputCostPerToken: 3e-6,
|
|
outputCostPerToken: 1.5e-5,
|
|
cacheCreationInputCostPerToken: 3.75e-6,
|
|
cacheReadInputCostPerToken: 3e-7,
|
|
thresholdTokens: 200_000,
|
|
inputCostPerTokenAboveThreshold: 6e-6,
|
|
outputCostPerTokenAboveThreshold: 2.25e-5,
|
|
cacheCreationInputCostPerTokenAboveThreshold: 7.5e-6,
|
|
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
|
|
]
|
|
|
|
private static let codexModelsDevProviderID = "openai"
|
|
private static let claudeModelsDevProviderID = "anthropic"
|
|
|
|
static func normalizeCodexModel(_ raw: String) -> String {
|
|
var trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if trimmed.hasPrefix("openai/") {
|
|
trimmed = String(trimmed.dropFirst("openai/".count))
|
|
}
|
|
|
|
// OpenAI routes the unsuffixed gpt-5.6 alias to Sol.
|
|
if trimmed == "gpt-5.6" {
|
|
return "gpt-5.6-sol"
|
|
}
|
|
|
|
if self.codex[trimmed] != nil {
|
|
return trimmed
|
|
}
|
|
|
|
if let datedSuffix = trimmed.range(of: #"-\d{4}-\d{2}-\d{2}$"#, options: .regularExpression) {
|
|
let base = String(trimmed[..<datedSuffix.lowerBound])
|
|
if self.codex[base] != nil {
|
|
return base
|
|
}
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
static func isCodexUnattributedModel(_ raw: String) -> Bool {
|
|
self.normalizeCodexModel(raw) == self.codexUnattributedModel
|
|
}
|
|
|
|
static func codexDisplayLabel(model: String) -> String? {
|
|
let key = self.normalizeCodexModel(model)
|
|
return self.codex[key]?.displayLabel
|
|
}
|
|
|
|
static func normalizeClaudeModel(_ raw: String) -> String {
|
|
var trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if trimmed.hasPrefix("anthropic.") {
|
|
trimmed = String(trimmed.dropFirst("anthropic.".count))
|
|
}
|
|
|
|
if let lastDot = trimmed.lastIndex(of: "."),
|
|
trimmed.contains("claude-")
|
|
{
|
|
let tail = String(trimmed[trimmed.index(after: lastDot)...])
|
|
if tail.hasPrefix("claude-") {
|
|
trimmed = tail
|
|
}
|
|
}
|
|
|
|
if let vRange = trimmed.range(of: #"-v\d+:\d+$"#, options: .regularExpression) {
|
|
trimmed.removeSubrange(vRange)
|
|
}
|
|
|
|
if let baseRange = trimmed.range(of: #"-\d{8}$"#, options: .regularExpression) {
|
|
let base = String(trimmed[..<baseRange.lowerBound])
|
|
if self.claude[base] != nil {
|
|
return base
|
|
}
|
|
}
|
|
|
|
return trimmed
|
|
}
|
|
|
|
static func codexCostUSD(
|
|
model: String,
|
|
inputTokens: Int,
|
|
cachedInputTokens: Int,
|
|
outputTokens: Int,
|
|
cacheWriteInputTokens: Int = 0,
|
|
modelsDevCatalog: ModelsDevCatalog? = nil,
|
|
modelsDevCacheRoot: URL? = nil) -> Double?
|
|
{
|
|
let key = self.normalizeCodexModel(model)
|
|
guard key != self.codexUnattributedModel else { return nil }
|
|
let modelsDevLookup = self.modelsDevLookup(
|
|
providerID: self.codexModelsDevProviderID,
|
|
model: model,
|
|
catalog: modelsDevCatalog,
|
|
cacheRoot: modelsDevCacheRoot)
|
|
?? (model == key ? nil : self.modelsDevLookup(
|
|
providerID: self.codexModelsDevProviderID,
|
|
model: key,
|
|
catalog: modelsDevCatalog,
|
|
cacheRoot: modelsDevCacheRoot))
|
|
if let lookup = modelsDevLookup {
|
|
let bundled = self.codex[key]
|
|
// A missing catalog context block means models.dev has no long-context opinion, so use
|
|
// the bundled tuple. Once the block exists, preserve its omissions and normal fallback
|
|
// semantics instead of filling individual fields from a different pricing source.
|
|
let bundledLongContext = lookup.pricing.thresholdTokens == nil ? bundled : nil
|
|
let cacheReadAboveThreshold = lookup.pricing.cacheReadInputCostPerTokenAboveThreshold
|
|
?? (lookup.pricing.thresholdTokens != nil
|
|
? lookup.pricing.cacheReadInputCostPerToken
|
|
?? lookup.pricing.inputCostPerTokenAboveThreshold
|
|
?? lookup.pricing.inputCostPerToken
|
|
: bundledLongContext?.cacheReadInputCostPerTokenAboveThreshold)
|
|
let cacheWriteAboveThreshold = lookup.pricing.cacheCreationInputCostPerTokenAboveThreshold
|
|
?? (lookup.pricing.thresholdTokens != nil
|
|
? lookup.pricing.cacheCreationInputCostPerToken
|
|
?? lookup.pricing.inputCostPerTokenAboveThreshold
|
|
?? lookup.pricing.inputCostPerToken
|
|
: bundledLongContext?.cacheWriteInputCostPerTokenAboveThreshold)
|
|
return self.codexCostUSD(
|
|
pricing: lookup.pricing,
|
|
thresholdTokens: bundled?.thresholdTokens ?? lookup.pricing.thresholdTokens,
|
|
inputCostPerTokenAboveThreshold: lookup.pricing.inputCostPerTokenAboveThreshold
|
|
?? bundledLongContext?.inputCostPerTokenAboveThreshold,
|
|
outputCostPerTokenAboveThreshold: lookup.pricing.outputCostPerTokenAboveThreshold
|
|
?? bundledLongContext?.outputCostPerTokenAboveThreshold,
|
|
cacheReadInputCostPerToken: lookup.pricing.cacheReadInputCostPerToken
|
|
?? bundled?.cacheReadInputCostPerToken,
|
|
cacheReadInputCostPerTokenAboveThreshold: cacheReadAboveThreshold,
|
|
cacheWriteInputCostPerToken: lookup.pricing.cacheCreationInputCostPerToken
|
|
?? bundled?.cacheWriteInputCostPerToken,
|
|
cacheWriteInputCostPerTokenAboveThreshold: cacheWriteAboveThreshold,
|
|
inputTokens: inputTokens,
|
|
cachedInputTokens: cachedInputTokens,
|
|
cacheWriteInputTokens: cacheWriteInputTokens,
|
|
outputTokens: outputTokens)
|
|
}
|
|
|
|
guard let pricing = self.codex[key] else { return nil }
|
|
return self.codexCostUSD(
|
|
pricing: pricing,
|
|
inputTokens: inputTokens,
|
|
cachedInputTokens: cachedInputTokens,
|
|
cacheWriteInputTokens: cacheWriteInputTokens,
|
|
outputTokens: outputTokens)
|
|
}
|
|
|
|
static func codexPriorityCostUSD(
|
|
model: String,
|
|
inputTokens: Int,
|
|
cachedInputTokens: Int = 0,
|
|
cacheWriteInputTokens: Int = 0,
|
|
outputTokens: Int) -> Double?
|
|
{
|
|
let key = self.normalizeCodexModel(model)
|
|
guard let pricing = self.codex[key],
|
|
let priorityInputCostPerToken = pricing.priorityInputCostPerToken,
|
|
let priorityOutputCostPerToken = pricing.priorityOutputCostPerToken
|
|
else { return nil }
|
|
// OpenAI does not support Priority processing for long-context requests. Do not combine
|
|
// the independent Standard long-context and Priority short-context rate tables.
|
|
if max(0, inputTokens) > self.codexPriorityInputTokenLimit {
|
|
return nil
|
|
}
|
|
|
|
let priorityPricing = CodexPricing(
|
|
inputCostPerToken: priorityInputCostPerToken,
|
|
outputCostPerToken: priorityOutputCostPerToken,
|
|
cacheReadInputCostPerToken: pricing.priorityCacheReadInputCostPerToken,
|
|
displayLabel: nil,
|
|
cacheWriteInputCostPerToken: pricing.priorityCacheWriteInputCostPerToken)
|
|
return self.codexCostUSD(
|
|
pricing: priorityPricing,
|
|
inputTokens: inputTokens,
|
|
cachedInputTokens: cachedInputTokens,
|
|
cacheWriteInputTokens: cacheWriteInputTokens,
|
|
outputTokens: outputTokens)
|
|
}
|
|
|
|
private static func codexCostUSD(
|
|
pricing: CodexPricing,
|
|
inputTokens: Int,
|
|
cachedInputTokens: Int,
|
|
cacheWriteInputTokens: Int = 0,
|
|
outputTokens: Int) -> Double
|
|
{
|
|
// Codex/OpenAI reports `input_tokens` as the total prompt size, with cached reads as a
|
|
// SUBSET of it. Cache writes (when tracked separately, e.g. Pi) are also a subset of the
|
|
// non-cached remainder. Clamp so tokens are never invented or double-billed.
|
|
let totalInput = max(0, inputTokens)
|
|
let cached = min(max(0, cachedInputTokens), totalInput)
|
|
let remainingAfterCache = totalInput - cached
|
|
let cacheWrite = min(max(0, cacheWriteInputTokens), remainingAfterCache)
|
|
let nonCached = remainingAfterCache - cacheWrite
|
|
let cachedRate = pricing.cacheReadInputCostPerToken ?? pricing.inputCostPerToken
|
|
|
|
let usesLongContextRates = pricing.thresholdTokens.map { totalInput > $0 } ?? false
|
|
let inputRate = usesLongContextRates
|
|
? pricing.inputCostPerTokenAboveThreshold ?? pricing.inputCostPerToken
|
|
: pricing.inputCostPerToken
|
|
let cachedInputRate = usesLongContextRates
|
|
? pricing.cacheReadInputCostPerTokenAboveThreshold ?? pricing.cacheReadInputCostPerToken ?? inputRate
|
|
: cachedRate
|
|
let cacheWriteRate = usesLongContextRates
|
|
? pricing.cacheWriteInputCostPerTokenAboveThreshold
|
|
?? pricing.cacheWriteInputCostPerToken
|
|
?? inputRate
|
|
: pricing.cacheWriteInputCostPerToken ?? inputRate
|
|
let outputRate = usesLongContextRates
|
|
? pricing.outputCostPerTokenAboveThreshold ?? pricing.outputCostPerToken
|
|
: pricing.outputCostPerToken
|
|
|
|
return (Double(nonCached) * inputRate)
|
|
+ (Double(cached) * cachedInputRate)
|
|
+ (Double(cacheWrite) * cacheWriteRate)
|
|
+ (Double(max(0, outputTokens)) * outputRate)
|
|
}
|
|
|
|
private static func codexCostUSD(
|
|
pricing: ModelsDevPricingInfo,
|
|
thresholdTokens: Int? = nil,
|
|
inputCostPerTokenAboveThreshold: Double? = nil,
|
|
outputCostPerTokenAboveThreshold: Double? = nil,
|
|
cacheReadInputCostPerToken: Double? = nil,
|
|
cacheReadInputCostPerTokenAboveThreshold: Double? = nil,
|
|
cacheWriteInputCostPerToken: Double? = nil,
|
|
cacheWriteInputCostPerTokenAboveThreshold: Double? = nil,
|
|
inputTokens: Int,
|
|
cachedInputTokens: Int,
|
|
cacheWriteInputTokens: Int = 0,
|
|
outputTokens: Int) -> Double
|
|
{
|
|
self.codexCostUSD(
|
|
pricing: CodexPricing(
|
|
inputCostPerToken: pricing.inputCostPerToken,
|
|
outputCostPerToken: pricing.outputCostPerToken,
|
|
cacheReadInputCostPerToken: cacheReadInputCostPerToken
|
|
?? pricing.cacheReadInputCostPerToken,
|
|
displayLabel: nil,
|
|
cacheWriteInputCostPerToken: cacheWriteInputCostPerToken
|
|
?? pricing.cacheCreationInputCostPerToken,
|
|
thresholdTokens: thresholdTokens ?? pricing.thresholdTokens,
|
|
inputCostPerTokenAboveThreshold: inputCostPerTokenAboveThreshold
|
|
?? pricing.inputCostPerTokenAboveThreshold,
|
|
outputCostPerTokenAboveThreshold: outputCostPerTokenAboveThreshold
|
|
?? pricing.outputCostPerTokenAboveThreshold,
|
|
cacheReadInputCostPerTokenAboveThreshold: cacheReadInputCostPerTokenAboveThreshold
|
|
?? pricing.cacheReadInputCostPerTokenAboveThreshold,
|
|
cacheWriteInputCostPerTokenAboveThreshold: cacheWriteInputCostPerTokenAboveThreshold
|
|
?? pricing.cacheCreationInputCostPerTokenAboveThreshold),
|
|
inputTokens: inputTokens,
|
|
cachedInputTokens: cachedInputTokens,
|
|
cacheWriteInputTokens: cacheWriteInputTokens,
|
|
outputTokens: outputTokens)
|
|
}
|
|
|
|
static func claudeCostUSD(
|
|
model: String,
|
|
inputTokens: Int,
|
|
cacheReadInputTokens: Int,
|
|
cacheCreationInputTokens: Int,
|
|
cacheCreationInputTokens1h: Int = 0,
|
|
outputTokens: Int,
|
|
pricingDate: Date? = nil,
|
|
modelsDevCatalog: ModelsDevCatalog? = nil,
|
|
modelsDevCacheRoot: URL? = nil) -> Double?
|
|
{
|
|
let tokens = ClaudeCostTokens(
|
|
input: inputTokens,
|
|
cacheRead: cacheReadInputTokens,
|
|
cacheCreation: cacheCreationInputTokens,
|
|
cacheCreation1h: cacheCreationInputTokens1h,
|
|
output: outputTokens)
|
|
let key = self.normalizeClaudeModel(model)
|
|
if let pricingDate,
|
|
let historicalPricing = self.claudeHistoricalLongContext[key],
|
|
let currentPricing = self.claude[key]
|
|
{
|
|
return self.claudeCostUSD(
|
|
pricing: pricingDate < self.claudeFullContextStandardPricingCutoff
|
|
? historicalPricing
|
|
: currentPricing,
|
|
tokens: tokens)
|
|
}
|
|
if let lookup = self.modelsDevLookup(
|
|
providerID: self.claudeModelsDevProviderID,
|
|
model: model,
|
|
catalog: modelsDevCatalog,
|
|
cacheRoot: modelsDevCacheRoot)
|
|
{
|
|
return self.claudeCostUSD(
|
|
pricing: lookup.pricing,
|
|
tokens: tokens)
|
|
}
|
|
|
|
guard let pricing = self.claude[key] else { return nil }
|
|
return self.claudeCostUSD(
|
|
pricing: pricing,
|
|
tokens: tokens)
|
|
}
|
|
|
|
private static func claudeCostUSD(
|
|
pricing: ClaudePricing,
|
|
tokens: ClaudeCostTokens) -> Double
|
|
{
|
|
let input = max(0, tokens.input)
|
|
let cacheRead = max(0, tokens.cacheRead)
|
|
let cacheCreationTotal = max(0, tokens.cacheCreation)
|
|
let cacheCreation1h = min(max(0, tokens.cacheCreation1h), cacheCreationTotal)
|
|
let cacheCreation5m = cacheCreationTotal - cacheCreation1h
|
|
let usesLongContextRates = pricing.thresholdTokens.map {
|
|
input + cacheRead + cacheCreationTotal > $0
|
|
} ?? false
|
|
let inputRate = usesLongContextRates
|
|
? pricing.inputCostPerTokenAboveThreshold ?? pricing.inputCostPerToken
|
|
: pricing.inputCostPerToken
|
|
let cacheReadRate = usesLongContextRates
|
|
? pricing.cacheReadInputCostPerTokenAboveThreshold ?? pricing.cacheReadInputCostPerToken
|
|
: pricing.cacheReadInputCostPerToken
|
|
let cacheCreation5mRate = usesLongContextRates
|
|
? pricing.cacheCreationInputCostPerTokenAboveThreshold ?? pricing.cacheCreationInputCostPerToken
|
|
: pricing.cacheCreationInputCostPerToken
|
|
let outputRate = usesLongContextRates
|
|
? pricing.outputCostPerTokenAboveThreshold ?? pricing.outputCostPerToken
|
|
: pricing.outputCostPerToken
|
|
|
|
return Double(input) * inputRate
|
|
+ Double(cacheRead) * cacheReadRate
|
|
+ Double(cacheCreation5m) * cacheCreation5mRate
|
|
+ Double(cacheCreation1h) * inputRate * 2
|
|
+ Double(max(0, tokens.output)) * outputRate
|
|
}
|
|
|
|
private static func claudeCostUSD(
|
|
pricing: ModelsDevPricingInfo,
|
|
tokens: ClaudeCostTokens) -> Double
|
|
{
|
|
self.claudeCostUSD(
|
|
pricing: ClaudePricing(
|
|
inputCostPerToken: pricing.inputCostPerToken,
|
|
outputCostPerToken: pricing.outputCostPerToken,
|
|
cacheCreationInputCostPerToken: pricing.cacheCreationInputCostPerToken ?? pricing.inputCostPerToken,
|
|
cacheReadInputCostPerToken: pricing.cacheReadInputCostPerToken ?? pricing.inputCostPerToken,
|
|
thresholdTokens: pricing.thresholdTokens,
|
|
inputCostPerTokenAboveThreshold: pricing.inputCostPerTokenAboveThreshold,
|
|
outputCostPerTokenAboveThreshold: pricing.outputCostPerTokenAboveThreshold,
|
|
cacheCreationInputCostPerTokenAboveThreshold: pricing.cacheCreationInputCostPerTokenAboveThreshold,
|
|
cacheReadInputCostPerTokenAboveThreshold: pricing.cacheReadInputCostPerTokenAboveThreshold),
|
|
tokens: tokens)
|
|
}
|
|
|
|
static func modelsDevCatalog(now: Date = Date(), cacheRoot: URL? = nil) -> ModelsDevCatalog? {
|
|
ModelsDevCache.load(now: now, cacheRoot: cacheRoot).artifact?.catalog
|
|
}
|
|
|
|
private static func modelsDevLookup(
|
|
providerID: String,
|
|
model: String,
|
|
catalog: ModelsDevCatalog?,
|
|
cacheRoot: URL?) -> ModelsDevPricingLookup?
|
|
{
|
|
if let catalog {
|
|
return catalog.pricing(providerID: providerID, modelID: model)
|
|
}
|
|
|
|
return ModelsDevPricingPipeline.lookup(
|
|
providerID: providerID,
|
|
modelID: model,
|
|
cacheRoot: cacheRoot)
|
|
}
|
|
}
|