Files
2026-07-13 12:22:33 +08:00

1368 lines
49 KiB
Swift

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import Testing
@testable import CodexBarCore
struct ModelsDevPricingTests {
@Test
func `parses models dev subset`() throws {
let catalog = try Self.fixtureCatalog()
#expect(catalog.providers["openai"]?.name == "OpenAI")
#expect(catalog.providers["anthropic"]?.models["claude-sonnet-4-6"]?.cost?.cacheWrite == 3.75)
#expect(catalog.providers["anthropic"]?.models["claude-sonnet-4-6"]?.limit?.context == 1_000_000)
}
@Test
func `looks up pricing by provider and model`() throws {
let catalog = try Self.fixtureCatalog()
let openAI = try #require(catalog.pricing(providerID: "openai", modelID: "shared-model"))
let anthropic = try #require(catalog.pricing(providerID: "anthropic", modelID: "shared-model"))
#expect(openAI.pricing.inputCostPerToken == 1 / 1_000_000.0)
#expect(openAI.pricing.outputCostPerToken == 2 / 1_000_000.0)
#expect(anthropic.pricing.inputCostPerToken == 3 / 1_000_000.0)
#expect(anthropic.pricing.outputCostPerToken == 4 / 1_000_000.0)
}
@Test
func `does not fall back across providers`() throws {
let catalog = try Self.fixtureCatalog()
#expect(catalog.pricing(providerID: "openai", modelID: "claude-sonnet-4-6") == nil)
#expect(catalog.pricing(providerID: "anthropic", modelID: "gpt-4o-mini") == nil)
}
@Test
func `supports provider scoped model normalization`() throws {
let catalog = try Self.fixtureCatalog()
let anthropic = try #require(catalog.pricing(
providerID: "anthropic",
modelID: "us.anthropic.claude-sonnet-4-6"))
let vertex = try #require(catalog.pricing(
providerID: "google-vertex-anthropic",
modelID: "claude-sonnet-4-6"))
#expect(anthropic.normalizedModelID == "claude-sonnet-4-6")
#expect(vertex.normalizedModelID == "claude-sonnet-4-6")
#expect(vertex.pricing.inputCostPerToken == 3.1 / 1_000_000.0)
}
@Test
func `converts models dev per million token prices to per token prices`() throws {
let pricing = try #require(try Self.fixtureCatalog().pricing(
providerID: "anthropic",
modelID: "claude-sonnet-4-6")?
.pricing)
#expect(pricing.inputCostPerToken == 3 / 1_000_000.0)
#expect(pricing.outputCostPerToken == 15 / 1_000_000.0)
#expect(pricing.cacheReadInputCostPerToken == 0.3 / 1_000_000.0)
#expect(pricing.cacheCreationInputCostPerToken == 3.75 / 1_000_000.0)
#expect(pricing.thresholdTokens == 200_000)
#expect(pricing.inputCostPerTokenAboveThreshold == 6 / 1_000_000.0)
#expect(pricing.outputCostPerTokenAboveThreshold == 22.5 / 1_000_000.0)
#expect(pricing.cacheReadInputCostPerTokenAboveThreshold == 0.6 / 1_000_000.0)
#expect(pricing.cacheCreationInputCostPerTokenAboveThreshold == 7.5 / 1_000_000.0)
}
@Test
func `stale cache is still readable`() throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: old, cacheRoot: root)
let load = ModelsDevCache.load(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root)
#expect(load.artifact != nil)
#expect(load.isStale)
#expect(load.error == nil)
}
@Test
func `pipeline lookup reads cached pricing`() throws {
let root = try Self.cacheRoot()
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: Date(), cacheRoot: root)
let lookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "gpt-4o-mini",
cacheRoot: root))
#expect(lookup.pricing.inputCostPerToken == 0.15 / 1_000_000.0)
}
@Test
func `network failure preserves last valid cache`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: old, cacheRoot: root)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(result: .failure(MockError.failed))))
let lookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "gpt-4o-mini",
cacheRoot: root))
#expect(lookup.pricing.inputCostPerToken == 0.15 / 1_000_000.0)
}
@Test
func `refresh preserves cache when fetched catalog drops cached provider`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: old, cacheRoot: root)
let partialCatalog = Data("""
{
"openai": { "id": 7, "models": [] },
"anthropic": {
"id": "anthropic",
"models": {
"shared-model": {
"id": "shared-model",
"cost": { "input": 99, "output": 99 }
}
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((partialCatalog, Self.response(status: 200))))))
let lookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "gpt-4o-mini",
cacheRoot: root))
#expect(lookup.pricing.inputCostPerToken == 0.15 / 1_000_000.0)
}
}
extension ModelsDevPricingTests {
@Test
func `unknown model refresh makes newly published pricing available`() async throws {
let root = try Self.cacheRoot()
let now = Date(timeIntervalSince1970: 10000)
try ModelsDevCache.save(
catalog: Self.fixtureCatalog(),
fetchedAt: now.addingTimeInterval(-901),
cacheRoot: root)
let refreshed = Data("""
{
"openai": {
"id": "openai",
"models": {
"gpt-new": { "id": "gpt-new", "cost": { "input": 2, "output": 8 } }
}
},
"anthropic": {
"id": "anthropic",
"models": {
"claude-new": { "id": "claude-new", "cost": { "input": 3, "output": 15 } }
}
}
}
""".utf8)
let transport = TrackingTransport(result: .success((refreshed, Self.response(status: 200))))
let client = ModelsDevClient(transport: transport)
let outcome = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["gpt-new"],
now: now,
cacheRoot: root,
client: client)
#expect(outcome == .pricingAvailable)
#expect(transport.calls == 1)
#expect(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "gpt-new",
cacheRoot: root) != nil)
}
@Test
func `unknown model refresh is bounded per provider cache`() async throws {
let root = try Self.cacheRoot()
let now = Date(timeIntervalSince1970: 20000)
try ModelsDevCache.save(
catalog: Self.fixtureCatalog(),
fetchedAt: now.addingTimeInterval(-901),
cacheRoot: root)
let transport = try TrackingTransport(result: .success((
JSONEncoder().encode(Self.fixtureCatalog()),
Self.response(status: 200))))
let client = ModelsDevClient(transport: transport)
let first = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["still-unknown"],
now: now,
cacheRoot: root,
client: client)
let second = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["another-unknown-model"],
now: now.addingTimeInterval(60),
cacheRoot: root,
client: client)
#expect(first == .unavailable)
#expect(second == .unavailable)
#expect(transport.calls == 1)
}
@Test
func `known requested model does not mask an unresolved unknown model`() async throws {
let root = try Self.cacheRoot()
let now = Date(timeIntervalSince1970: 25000)
let catalog = try Self.catalog("""
{
"openai": {
"id": "openai",
"models": {
"already-priced": { "id": "already-priced", "cost": { "input": 1, "output": 2 } }
}
},
"anthropic": {
"id": "anthropic",
"models": {
"catalog-anchor": { "id": "catalog-anchor", "cost": { "input": 3, "output": 4 } }
}
}
}
""")
ModelsDevCache.save(
catalog: catalog,
fetchedAt: now.addingTimeInterval(-901),
cacheRoot: root)
let transport = try TrackingTransport(result: .success((
JSONEncoder().encode(catalog),
Self.response(status: 200))))
let outcome = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["already-priced", "still-unknown"],
now: now,
cacheRoot: root,
client: ModelsDevClient(transport: transport))
#expect(outcome == .unavailable)
#expect(transport.calls == 1)
}
@Test
func `pricing added by a completed background refresh requests a rescan`() async throws {
let root = try Self.cacheRoot()
let now = Date(timeIntervalSince1970: 30000)
let refreshed = Data("""
{
"openai": {
"id": "openai",
"models": { "gpt-new": { "id": "gpt-new", "cost": { "input": 2, "output": 8 } } }
},
"anthropic": {
"id": "anthropic",
"models": { "claude-new": { "id": "claude-new", "cost": { "input": 3, "output": 15 } } }
}
}
""".utf8)
let refreshedCatalog = try JSONDecoder().decode(ModelsDevCatalog.self, from: refreshed)
ModelsDevCache.save(catalog: refreshedCatalog, fetchedAt: now, cacheRoot: root)
let transport = TrackingTransport(result: .failure(MockError.failed))
let outcome = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["gpt-new"],
now: now,
cacheRoot: root,
client: ModelsDevClient(transport: transport))
#expect(outcome == .pricingAvailable)
#expect(transport.calls == 0)
}
@Test
func `ttl and unknown model refreshes share one download`() async throws {
let root = try Self.cacheRoot()
let now = Date(timeIntervalSince1970: 40000)
try ModelsDevCache.save(
catalog: Self.fixtureCatalog(),
fetchedAt: now.addingTimeInterval(-ModelsDevCache.ttlSeconds - 1),
cacheRoot: root)
let transport = try TrackingTransport(
result: .success((JSONEncoder().encode(Self.fixtureCatalog()), Self.response(status: 200))),
delayNanoseconds: 100_000_000)
let client = ModelsDevClient(transport: transport)
async let ttl: Void = ModelsDevPricingPipeline.refreshIfNeeded(
now: now,
cacheRoot: root,
client: client)
async let unknown = ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["still-unknown"],
now: now,
cacheRoot: root,
client: client)
_ = await (ttl, unknown)
#expect(transport.calls == 1)
}
@Test
func `completed ttl refresh bounds a following unknown model refresh`() async throws {
let root = try Self.cacheRoot()
let now = Date(timeIntervalSince1970: 45000)
try ModelsDevCache.save(
catalog: Self.fixtureCatalog(),
fetchedAt: now.addingTimeInterval(-ModelsDevCache.ttlSeconds - 1),
cacheRoot: root)
let transport = try TrackingTransport(result: .success((
JSONEncoder().encode(Self.fixtureCatalog()),
Self.response(status: 200))))
let client = ModelsDevClient(transport: transport)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: now,
cacheRoot: root,
client: client)
let outcome = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["still-unknown"],
now: now,
cacheRoot: root,
client: client)
#expect(outcome == .unavailable)
#expect(transport.calls == 1)
}
@Test
func `failed ttl refresh bounds a following unknown model refresh within cooldown`() async throws {
let root = try Self.cacheRoot()
let now = Date(timeIntervalSince1970: 46000)
try ModelsDevCache.save(
catalog: Self.fixtureCatalog(),
fetchedAt: now.addingTimeInterval(-ModelsDevCache.ttlSeconds - 1),
cacheRoot: root)
let transport = TrackingTransport(result: .failure(MockError.failed))
let client = ModelsDevClient(transport: transport)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: now,
cacheRoot: root,
client: client)
let outcome = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["still-unknown"],
now: now,
cacheRoot: root,
client: client)
#expect(outcome == .unavailable)
#expect(transport.calls == 1)
}
@Test
func `failed unknown model refresh bounds a following ttl refresh within cooldown`() async throws {
let root = try Self.cacheRoot()
let now = Date(timeIntervalSince1970: 47000)
try ModelsDevCache.save(
catalog: Self.fixtureCatalog(),
fetchedAt: now.addingTimeInterval(-ModelsDevCache.ttlSeconds - 1),
cacheRoot: root)
let transport = TrackingTransport(result: .failure(MockError.failed))
let client = ModelsDevClient(transport: transport)
let outcome = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["still-unknown"],
now: now,
cacheRoot: root,
client: client)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: now,
cacheRoot: root,
client: client)
#expect(outcome == .unavailable)
#expect(transport.calls == 1)
}
@Test
func `ttl refresh rechecks cache freshness after coordination`() async throws {
let root = try Self.cacheRoot()
let now = Date(timeIntervalSince1970: 48000)
try ModelsDevCache.save(
catalog: Self.fixtureCatalog(),
fetchedAt: now.addingTimeInterval(-ModelsDevCache.ttlSeconds - 1),
cacheRoot: root)
#expect(ModelsDevCache.load(now: now, cacheRoot: root).isStale)
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: now, cacheRoot: root)
let transport = TrackingTransport(result: .failure(MockError.failed))
let cacheIsCurrent = await ModelsDevPricingPipeline.refreshStaleCache(
now: now,
cacheRoot: root,
client: ModelsDevClient(transport: transport))
#expect(cacheIsCurrent)
#expect(transport.calls == 0)
}
@Test
func `failed cache save does not report pricing available`() async {
let root = URL(fileURLWithPath: "/dev/null", isDirectory: true)
let now = Date(timeIntervalSince1970: 50000)
let refreshed = Data("""
{
"openai": {
"id": "openai",
"models": { "gpt-new": { "id": "gpt-new", "cost": { "input": 2, "output": 8 } } }
},
"anthropic": {
"id": "anthropic",
"models": { "claude-new": { "id": "claude-new", "cost": { "input": 3, "output": 15 } } }
}
}
""".utf8)
let outcome = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: "openai",
modelIDs: ["gpt-new"],
now: now,
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((refreshed, Self.response(status: 200))))))
#expect(outcome == .unavailable)
}
@Test
func `refresh accepts model churn and preserves removed pricing as fallback`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: old, cacheRoot: root)
let partialCatalog = Data("""
{
"openai": {
"id": "openai",
"models": {
"shared-model": {
"id": "shared-model",
"cost": { "input": 99, "output": 99 }
},
"provider-a-new": {
"id": "provider-a-new",
"cost": { "input": 7, "output": 8 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": {
"shared-model": {
"id": "shared-model",
"cost": { "input": 99, "output": 99 }
}
}
},
"google-vertex-anthropic": {
"id": "google-vertex-anthropic",
"models": {
"claude-sonnet-4-6": {
"id": "claude-sonnet-4-6",
"cost": { "input": 99, "output": 99 }
}
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((partialCatalog, Self.response(status: 200))))))
let oldLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "gpt-4o-mini",
cacheRoot: root))
let newLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "provider-a-new",
cacheRoot: root))
let updatedLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "shared-model",
cacheRoot: root))
#expect(oldLookup.pricing.inputCostPerToken == 0.15 / 1_000_000.0)
#expect(newLookup.pricing.inputCostPerToken == 7 / 1_000_000.0)
#expect(updatedLookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
}
@Test
func `accumulated fallback models do not freeze later refreshes`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
let cachedCatalog = try Self.catalog("""
{
"openai": {
"id": "openai",
"models": {
"provider-a-old": { "id": "provider-a-old", "cost": { "input": 1, "output": 2 } }
}
},
"anthropic": {
"id": "anthropic",
"models": {
"provider-b-old": { "id": "provider-b-old", "cost": { "input": 3, "output": 4 } }
}
},
"stale-a": {
"id": "stale-a",
"models": {
"model-a": { "id": "model-a", "cost": { "input": 5, "output": 6 } }
}
},
"stale-b": {
"id": "stale-b",
"models": {
"model-b": { "id": "model-b", "cost": { "input": 7, "output": 8 } }
}
},
"stale-c": {
"id": "stale-c",
"models": {
"model-c": { "id": "model-c", "cost": { "input": 9, "output": 10 } }
}
}
}
""")
ModelsDevCache.save(catalog: cachedCatalog, fetchedAt: old, cacheRoot: root)
let fetchedCatalog = Data("""
{
"openai": {
"id": "openai",
"models": {
"provider-a-new": { "id": "provider-a-new", "cost": { "input": 11, "output": 12 } }
}
},
"anthropic": {
"id": "anthropic",
"models": {
"provider-b-new": { "id": "provider-b-new", "cost": { "input": 13, "output": 14 } }
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((fetchedCatalog, Self.response(status: 200))))))
let newLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "provider-a-new",
cacheRoot: root))
let fallbackLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "stale-a",
modelID: "model-a",
cacheRoot: root))
#expect(newLookup.pricing.inputCostPerToken == 11 / 1_000_000.0)
#expect(fallbackLookup.pricing.inputCostPerToken == 5 / 1_000_000.0)
}
@Test
func `historical fallback does not overwrite a refreshed model that reuses its map key`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
let cachedCatalog = try Self.catalog("""
{
"openai": {
"id": "openai",
"models": {
"rolling": { "id": "provider-a-old", "cost": { "input": 1, "output": 2 } }
}
},
"anthropic": {
"id": "anthropic",
"models": {
"provider-b-anchor": { "id": "provider-b-anchor", "cost": { "input": 3, "output": 4 } }
}
}
}
""")
ModelsDevCache.save(catalog: cachedCatalog, fetchedAt: old, cacheRoot: root)
let fetchedCatalog = Data("""
{
"openai": {
"id": "openai",
"models": {
"rolling": { "id": "provider-a-new", "cost": { "input": 99, "output": 100 } }
}
},
"anthropic": {
"id": "anthropic",
"models": {
"provider-b-anchor": { "id": "provider-b-anchor", "cost": { "input": 3, "output": 4 } }
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((fetchedCatalog, Self.response(status: 200))))))
let freshLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "provider-a-new",
cacheRoot: root))
let fallbackLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "provider-a-old",
cacheRoot: root))
#expect(freshLookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
#expect(fallbackLookup.pricing.inputCostPerToken == 1 / 1_000_000.0)
}
@Test
func `refresh updates cache when fetched catalog renames model key but keeps id`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: old, cacheRoot: root)
let renamedCatalog = Data("""
{
"openai": {
"id": "openai",
"models": {
"renamed-model-key": {
"id": "gpt-4o-mini",
"cost": { "input": 99, "output": 99 }
},
"shared-model": {
"id": "shared-model",
"cost": { "input": 99, "output": 99 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": {
"claude-sonnet-4-6": {
"id": "claude-sonnet-4-6",
"cost": { "input": 99, "output": 99 }
},
"shared-model": {
"id": "shared-model",
"cost": { "input": 99, "output": 99 }
}
}
},
"google-vertex-anthropic": {
"id": "google-vertex-anthropic",
"models": {
"renamed-vertex-key": {
"id": "claude-sonnet-4-6",
"cost": { "input": 99, "output": 99 }
}
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((renamedCatalog, Self.response(status: 200))))))
let lookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "gpt-4o-mini",
cacheRoot: root))
#expect(lookup.normalizedModelID == "gpt-4o-mini")
#expect(lookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
}
@Test
func `refresh preserves cache when fetched matching model is not priceable`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: old, cacheRoot: root)
let partialCatalog = Data("""
{
"openai": {
"id": "openai",
"models": {
"gpt-4o-mini": {
"id": "gpt-4o-mini",
"cost": { "input": 99 }
},
"shared-model": {
"id": "shared-model",
"cost": { "input": 99, "output": 99 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": {
"claude-sonnet-4-6": {
"id": "claude-sonnet-4-6",
"cost": { "input": 99, "output": 99 }
},
"shared-model": {
"id": "shared-model",
"cost": { "input": 99, "output": 99 }
}
}
},
"google-vertex-anthropic": {
"id": "google-vertex-anthropic",
"models": {
"claude-sonnet-4-6": {
"id": "claude-sonnet-4-6",
"cost": { "input": 99, "output": 99 }
}
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((partialCatalog, Self.response(status: 200))))))
let lookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "gpt-4o-mini",
cacheRoot: root))
let updatedLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "shared-model",
cacheRoot: root))
#expect(lookup.pricing.inputCostPerToken == 0.15 / 1_000_000.0)
#expect(updatedLookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
}
@Test
func `refresh updates cache when fetched catalog canonicalizes alias model id`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
let cachedCatalog = try Self.catalog("""
{
"openai": {
"id": "openai",
"models": {
"gpt-4o-mini": {
"id": "gpt-4o-mini",
"cost": { "input": 0.15, "output": 0.6 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": {
"claude-sonnet-4-6": {
"id": "claude-sonnet-4-6",
"cost": { "input": 3, "output": 15 }
}
}
},
"google-vertex-anthropic": {
"id": "google-vertex-anthropic",
"models": {
"snapshot-model@20250101": {
"id": "snapshot-model@20250101",
"cost": { "input": 3.1, "output": 15.1 }
}
}
}
}
""")
ModelsDevCache.save(catalog: cachedCatalog, fetchedAt: old, cacheRoot: root)
let canonicalizedCatalog = Data("""
{
"openai": {
"id": "openai",
"models": {
"gpt-4o-mini": {
"id": "gpt-4o-mini",
"cost": { "input": 99, "output": 99 }
},
"shared-model": {
"id": "shared-model",
"cost": { "input": 99, "output": 99 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": {
"claude-sonnet-4-6": {
"id": "claude-sonnet-4-6",
"cost": { "input": 99, "output": 99 }
},
"shared-model": {
"id": "shared-model",
"cost": { "input": 99, "output": 99 }
}
}
},
"google-vertex-anthropic": {
"id": "google-vertex-anthropic",
"models": {
"snapshot-model-20250101": {
"id": "snapshot-model-20250101",
"cost": { "input": 99, "output": 99 }
}
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((canonicalizedCatalog, Self.response(status: 200))))))
let aliasLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "google-vertex-anthropic",
modelID: "snapshot-model@20250101",
cacheRoot: root))
let canonicalLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "google-vertex-anthropic",
modelID: "snapshot-model-20250101",
cacheRoot: root))
#expect(aliasLookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
#expect(canonicalLookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
}
@Test
func `fallback merge treats default alias as the canonical base model`() throws {
let cachedCatalog = try Self.catalog("""
{
"anthropic": {
"id": "anthropic",
"models": {
"base-model@default": {
"id": "base-model@default",
"cost": { "input": 3, "output": 15 }
}
}
}
}
""")
let refreshedCatalog = try Self.catalog("""
{
"anthropic": {
"id": "anthropic",
"models": {
"base-model": {
"id": "base-model",
"cost": { "input": 99, "output": 100 }
}
}
}
}
""")
let merged = refreshedCatalog.mergingFallbackPricing(from: cachedCatalog)
let aliasLookup = try #require(merged.pricing(
providerID: "anthropic",
modelID: "base-model@default"))
#expect(aliasLookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
#expect(merged.providers["anthropic"]?.models.count == 1)
}
@Test
func `fallback merge treats provider version alias as the canonical base model`() throws {
let cachedCatalog = try Self.catalog("""
{
"openai": {
"id": "openai",
"models": {
"openai/base-model-v1:0": {
"id": "openai/base-model-v1:0",
"cost": { "input": 3, "output": 15 }
}
}
}
}
""")
let refreshedCatalog = try Self.catalog("""
{
"openai": {
"id": "openai",
"models": {
"base-model": {
"id": "base-model",
"cost": { "input": 99, "output": 100 }
}
}
}
}
""")
let merged = refreshedCatalog.mergingFallbackPricing(from: cachedCatalog)
let aliasLookup = try #require(merged.pricing(
providerID: "openai",
modelID: "openai/base-model-v1:0"))
#expect(aliasLookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
#expect(merged.providers["openai"]?.models.count == 1)
}
@Test
func `refresh keeps historical pinned pricing while accepting a new snapshot`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
let cachedCatalog = try Self.catalog("""
{
"google-vertex-anthropic": {
"id": "google-vertex-anthropic",
"models": {
"snapshot-model@20250101": {
"id": "snapshot-model@20250101",
"cost": { "input": 3, "output": 15 }
}
}
}
}
""")
ModelsDevCache.save(catalog: cachedCatalog, fetchedAt: old, cacheRoot: root)
let fetchedCatalog = Data("""
{
"openai": {
"id": "openai",
"models": {
"provider-a-anchor": {
"id": "provider-a-anchor",
"cost": { "input": 1, "output": 2 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": {
"provider-b-anchor": {
"id": "provider-b-anchor",
"cost": { "input": 3, "output": 4 }
}
}
},
"google-vertex-anthropic": {
"id": "google-vertex-anthropic",
"models": {
"snapshot-model@20250201": {
"id": "snapshot-model@20250201",
"cost": { "input": 99, "output": 99 }
}
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((fetchedCatalog, Self.response(status: 200))))))
let oldLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "google-vertex-anthropic",
modelID: "snapshot-model@20250101",
cacheRoot: root))
let newLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "google-vertex-anthropic",
modelID: "snapshot-model@20250201",
cacheRoot: root))
#expect(oldLookup.pricing.inputCostPerToken == 3 / 1_000_000.0)
#expect(newLookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
}
@Test
func `refresh preserves dated snapshot when fetched catalog only keeps base model`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
let cachedCatalog = try Self.catalog("""
{
"openai": {
"id": "openai",
"models": {
"historical-map-key": {
"id": "snapshot-model-2025-01-01",
"cost": { "input": 3, "output": 15 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": {
"provider-b-anchor": {
"id": "provider-b-anchor",
"cost": { "input": 3, "output": 4 }
}
}
}
}
""")
ModelsDevCache.save(catalog: cachedCatalog, fetchedAt: old, cacheRoot: root)
let fetchedCatalog = Data("""
{
"openai": {
"id": "openai",
"models": {
"snapshot-model": {
"id": "snapshot-model",
"cost": { "input": 99, "output": 99 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": {
"provider-b-anchor": {
"id": "provider-b-anchor",
"cost": { "input": 3, "output": 4 }
}
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((fetchedCatalog, Self.response(status: 200))))))
let snapshotLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "snapshot-model-2025-01-01",
cacheRoot: root))
let baseLookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "snapshot-model",
cacheRoot: root))
#expect(snapshotLookup.pricing.inputCostPerToken == 3 / 1_000_000.0)
#expect(baseLookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
}
@Test
func `compact snapshot alias prefers snapshot pricing over base pricing`() throws {
let catalog = try Self.catalog("""
{
"openai": {
"id": "openai",
"models": {
"snapshot-model": {
"id": "snapshot-model",
"cost": { "input": 99, "output": 100 }
},
"snapshot-model-20250101": {
"id": "snapshot-model-20250101",
"cost": { "input": 3, "output": 15 }
}
}
}
}
""")
let lookup = try #require(catalog.pricing(
providerID: "openai",
modelID: "snapshot-model@20250101"))
#expect(lookup.pricing.inputCostPerToken == 3 / 1_000_000.0)
#expect(lookup.normalizedModelID == "snapshot-model-20250101")
}
@Test
func `refresh ignores unpriceable models in old cache continuity check`() async throws {
let root = try Self.cacheRoot()
let old = Date(timeIntervalSince1970: 1)
let cachedCatalog = try Self.catalog("""
{
"openai": {
"id": "openai",
"models": {
"gpt-4o-mini": {
"id": "gpt-4o-mini",
"cost": { "input": 0.15, "output": 0.6 }
},
"unpriced-model": {
"id": "unpriced-model"
}
}
}
}
""")
ModelsDevCache.save(catalog: cachedCatalog, fetchedAt: old, cacheRoot: root)
let fetchedCatalog = Data("""
{
"openai": {
"id": "openai",
"models": {
"gpt-4o-mini": {
"id": "gpt-4o-mini",
"cost": { "input": 99, "output": 99 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": {
"provider-b-anchor": {
"id": "provider-b-anchor",
"cost": { "input": 3, "output": 4 }
}
}
}
}
""".utf8)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(timeIntervalSince1970: 1 + ModelsDevCache.ttlSeconds + 1),
cacheRoot: root,
client: ModelsDevClient(transport: MockTransport(
result: .success((fetchedCatalog, Self.response(status: 200))))))
let lookup = try #require(ModelsDevPricingPipeline.lookup(
providerID: "openai",
modelID: "gpt-4o-mini",
cacheRoot: root))
#expect(lookup.pricing.inputCostPerToken == 99 / 1_000_000.0)
}
@Test
func `fresh cache does not refresh`() async throws {
let root = try Self.cacheRoot()
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: Date(), cacheRoot: root)
let transport = TrackingTransport(result: .failure(MockError.failed))
await ModelsDevPricingPipeline.refreshIfNeeded(
now: Date(),
cacheRoot: root,
client: ModelsDevClient(transport: transport))
#expect(transport.calls == 0)
}
@Test
func `corrupt cache is ignored safely`() throws {
let root = try Self.cacheRoot()
let url = ModelsDevCache.cacheFileURL(cacheRoot: root)
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
try Data("not json".utf8).write(to: url)
let load = ModelsDevCache.load(cacheRoot: root)
#expect(load.artifact == nil)
#expect(load.isStale)
#expect(load.error == .invalidJSON)
}
@Test
func `serves decoded catalog from memo while the file is unchanged`() throws {
let root = try Self.cacheRoot()
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: Date(), cacheRoot: root)
let url = ModelsDevCache.cacheFileURL(cacheRoot: root)
// Pin a whole-second modification date so the memo key (which compares modification dates) round-trips
// deterministically through the filesystem.
let pinnedDate = Date(timeIntervalSince1970: 1_700_000_000)
try FileManager.default.setAttributes([.modificationDate: pinnedDate], ofItemAtPath: url.path)
// Prime the in-memory memo with a successful decode.
let primed = ModelsDevCache.load(cacheRoot: root)
let cachedArtifact = try #require(primed.artifact)
// Corrupt the file contents while preserving its size and modification date, so the on-disk identity
// the memo keys on is unchanged. A re-decode would now fail; a memo hit returns the cached artifact.
let size = try #require(
try (FileManager.default.attributesOfItem(atPath: url.path)[.size]) as? NSNumber).intValue
try Data(repeating: 0, count: size).write(to: url)
try FileManager.default.setAttributes([.modificationDate: pinnedDate], ofItemAtPath: url.path)
let reloaded = ModelsDevCache.load(cacheRoot: root)
#expect(reloaded.error == nil)
#expect(reloaded.artifact == cachedArtifact)
}
@Test
func `saving a new catalog invalidates the memo`() throws {
let root = try Self.cacheRoot()
try ModelsDevCache.save(catalog: Self.fixtureCatalog(), fetchedAt: Date(), cacheRoot: root)
#expect(ModelsDevCache.load(cacheRoot: root).artifact?.catalog.providers["openai"] != nil)
// Overwriting the cache must drop the memo so the next load reflects the freshly written catalog.
ModelsDevCache.save(catalog: ModelsDevCatalog(providers: [:]), fetchedAt: Date(), cacheRoot: root)
let reloaded = ModelsDevCache.load(cacheRoot: root)
#expect(reloaded.error == nil)
#expect(reloaded.artifact?.catalog.providers.isEmpty == true)
}
@Test
func `serves a failed load from memo while the file is unchanged`() throws {
let root = try Self.cacheRoot()
let url = ModelsDevCache.cacheFileURL(cacheRoot: root)
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
let validData = try Self.encodedArtifactData()
// Write invalid JSON of the same size as a valid encoding, with a pinned modification date, then prime
// the memo with the resulting failure.
let pinnedDate = Date(timeIntervalSince1970: 1_700_000_000)
try Data(repeating: 0x7B, count: validData.count).write(to: url)
try FileManager.default.setAttributes([.modificationDate: pinnedDate], ofItemAtPath: url.path)
#expect(ModelsDevCache.load(cacheRoot: root).error == .invalidJSON)
// Replace the bytes with a valid encoding of identical size + modification date. A re-read would now
// succeed, so a returned failure proves the unchanged-identity file was not read and decoded again.
try validData.write(to: url)
try FileManager.default.setAttributes([.modificationDate: pinnedDate], ofItemAtPath: url.path)
let reloaded = ModelsDevCache.load(cacheRoot: root)
#expect(reloaded.error == .invalidJSON)
#expect(reloaded.artifact == nil)
}
@Test
func `client fetches with mock transport`() async throws {
let data = try Self.fixtureData()
let client = ModelsDevClient(transport: MockTransport(result: .success((data, Self.response(status: 200)))))
let catalog = try await client.fetchCatalog()
#expect(catalog.providers["google-vertex-anthropic"]?.models["claude-sonnet-4-6"]?.cost?.input == 3.1)
}
@Test
func `client reports http and json failures`() async throws {
let data = try Self.fixtureData()
let httpClient = ModelsDevClient(transport: MockTransport(result: .success((data, Self.response(status: 500)))))
let jsonClient = ModelsDevClient(transport: MockTransport(
result: .success((Data("not json".utf8), Self.response(status: 200)))))
await #expect(throws: ModelsDevClient.Error.httpStatus(500)) {
_ = try await httpClient.fetchCatalog()
}
await #expect(throws: ModelsDevClient.Error.invalidJSON) {
_ = try await jsonClient.fetchCatalog()
}
}
private static func fixtureData() throws -> Data {
let url = try #require(Bundle.module.url(
forResource: "models-dev-subset",
withExtension: "json",
subdirectory: "Fixtures"))
return try Data(contentsOf: url)
}
private static func fixtureCatalog() throws -> ModelsDevCatalog {
try JSONDecoder().decode(ModelsDevCatalog.self, from: self.fixtureData())
}
/// A valid `ModelsDevCacheArtifact` encoding, written the same way `ModelsDevCache.save` writes the file.
private static func encodedArtifactData() throws -> Data {
let artifact = try ModelsDevCacheArtifact(
version: ModelsDevCache.artifactVersion,
fetchedAt: Date(timeIntervalSince1970: 0),
catalog: self.fixtureCatalog())
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return try encoder.encode(artifact)
}
private static func catalog(_ json: String) throws -> ModelsDevCatalog {
try JSONDecoder().decode(ModelsDevCatalog.self, from: Data(json.utf8))
}
private static func cacheRoot() throws -> URL {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-modelsdev-tests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
return root
}
private static func response(status: Int) -> HTTPURLResponse {
HTTPURLResponse(
url: URL(string: "https://models.dev/api.json")!,
statusCode: status,
httpVersion: nil,
headerFields: nil)!
}
}
private enum MockError: Error {
case failed
}
private struct MockTransport: ModelsDevHTTPTransport {
let result: Result<(Data, URLResponse), Error>
func data(for _: URLRequest) async throws -> (Data, URLResponse) {
try self.result.get()
}
}
private final class TrackingTransport: ModelsDevHTTPTransport, @unchecked Sendable {
private let lock = NSLock()
private var callCount = 0
let result: Result<(Data, URLResponse), Error>
let delayNanoseconds: UInt64
var calls: Int {
self.lock.withLock { self.callCount }
}
init(result: Result<(Data, URLResponse), Error>, delayNanoseconds: UInt64 = 0) {
self.result = result
self.delayNanoseconds = delayNanoseconds
}
func data(for _: URLRequest) async throws -> (Data, URLResponse) {
self.lock.withLock { self.callCount += 1 }
if self.delayNanoseconds > 0 {
try await Task.sleep(nanoseconds: self.delayNanoseconds)
}
return try self.result.get()
}
}