Fix $0.00 cost on OpenRouter image generation models

Image-gen models are billed per-image/per-request, not at the plain
per-token prompt/completion rates ModelInfo.Pricing captures — so
token-based cost calculation was always near-zero for them regardless
of actual spend. Now requests OpenRouter's usage.include=true, decodes
the actual billed usage.cost, and prefers it over calculated cost via
a new resolveCost() helper (covers the dedicated Images API, regular
chat completions, streaming, and the tool-calling loop alike).
This commit is contained in:
2026-08-11 10:55:52 +02:00
parent ef6265c1c5
commit 1e0e81b9bc
4 changed files with 64 additions and 12 deletions
@@ -82,6 +82,36 @@ struct ChatViewModelPureLogicTests {
#expect(ChatViewModel.calculateCost(usage: usage, pricing: pricing) == 0.0)
}
// MARK: - resolveCost
@Test("Raw provider-billed cost wins over token-based pricing when present")
func resolveCostPrefersRawCostUSD() {
let usage = ChatResponse.Usage(promptTokens: 1_000_000, completionTokens: 1_000_000, totalTokens: 2_000_000, rawCostUSD: 0.19)
let pricing = ModelInfo.Pricing(prompt: 3.0, completion: 15.0)
#expect(ChatViewModel.resolveCost(usage: usage, pricing: pricing) == 0.19)
}
@Test("Raw cost of zero is trusted, not treated as missing")
func resolveCostTrustsZeroRawCost() {
let usage = ChatResponse.Usage(promptTokens: 0, completionTokens: 0, totalTokens: 0, rawCostUSD: 0.0)
let pricing = ModelInfo.Pricing(prompt: 3.0, completion: 15.0)
#expect(ChatViewModel.resolveCost(usage: usage, pricing: pricing) == 0.0)
}
@Test("Falls back to token-based pricing when no raw cost is reported")
func resolveCostFallsBackToCalculation() {
let usage = ChatResponse.Usage(promptTokens: 1_000_000, completionTokens: 1_000_000, totalTokens: 2_000_000)
let pricing = ModelInfo.Pricing(prompt: 3.0, completion: 15.0)
#expect(ChatViewModel.resolveCost(usage: usage, pricing: pricing) == 18.0)
}
@Test("Returns nil when neither raw cost nor per-token pricing is available")
func resolveCostNilWhenNoDataAvailable() {
let usage = ChatResponse.Usage(promptTokens: 1_000_000, completionTokens: 1_000_000, totalTokens: 2_000_000)
let pricing = ModelInfo.Pricing(prompt: 0, completion: 0)
#expect(ChatViewModel.resolveCost(usage: usage, pricing: pricing) == nil)
}
// MARK: - draftFingerprint
@Test("Identical message content produces the same fingerprint")