diff --git a/tests/contracts/test_model_pricing.py b/tests/contracts/test_model_pricing.py index c2ebf1b..5038adf 100644 --- a/tests/contracts/test_model_pricing.py +++ b/tests/contracts/test_model_pricing.py @@ -6,6 +6,7 @@ BUILTIN_MODEL_PRICE_HISTORY, BUILTIN_PRICE_SOURCES, BUILTIN_PRICE_TABLES, + CLAUDE_CONTEXT_WINDOWS, GEMINI_4_ARGON_PRICE_UPDATE_AT, GEMINI_FLASH_PRICE_UPDATE_AT, GPT_56_PRICE_UPDATE_AT, @@ -13,6 +14,7 @@ canonical_model_provider, ) from token_meter.models.pricing import long_context_multipliers, quote_for +from token_meter.models.tiers import model_tier class ModelPricingBoundaryTests(unittest.TestCase): @@ -47,6 +49,23 @@ def test_fable_and_mythos_5_1_use_their_published_cache_read_price(self): self.assertEqual(quote.cache_read_per_million, 0.25) self.assertEqual(quote.matched_rule, model_id) + def test_fable_and_mythos_5_2_use_their_published_prices(self): + for model_id in ("claude-fable-5-2", "claude-mythos-5-2"): + with self.subTest(model_id=model_id): + quote = quote_for(PriceQuery(ModelRef("anthropic", model_id))) + + self.assertTrue(quote.available) + self.assertEqual(quote.input_per_million, 8.0) + self.assertEqual(quote.output_per_million, 40.0) + self.assertEqual(quote.cache_write_per_million, 10.0) + self.assertEqual(quote.cache_read_per_million, 0.20) + self.assertEqual(quote.matched_rule, model_id) + self.assertEqual(model_tier(model_id), "frontier") + + # The models overview lists Fable 5.2's window; Mythos 5.2's is unpublished, so it stays unknown. + self.assertEqual(CLAUDE_CONTEXT_WINDOWS["claude-fable-5-2"], 1_000_000) + self.assertNotIn("claude-mythos-5-2", CLAUDE_CONTEXT_WINDOWS) + def test_unknown_provider_is_explicitly_unavailable_without_cross_fallback(self): unknown_provider = quote_for(PriceQuery(ModelRef("unknown", "gpt-5.6"))) wrong_provider = quote_for(PriceQuery(ModelRef("anthropic", "gpt-5.6"))) diff --git a/tests/test_claude_cost_correctness.py b/tests/test_claude_cost_correctness.py index c6a6e0c..2b880a6 100644 --- a/tests/test_claude_cost_correctness.py +++ b/tests/test_claude_cost_correctness.py @@ -156,6 +156,48 @@ def test_opus_5_5_fast_us_pricing_uses_published_multipliers(self): for component, value in expected.items(): self.assertAlmostEqual(cost[component], value) + def test_fable_and_mythos_5_2_fast_us_pricing_uses_published_multipliers(self): + # Fast $16/$80 (2x), then US-only inference 1.1x; 1h writes are 2x input. + expected = { + "input": 17.6, + "cache_write": 57.2, + "cache_read": 0.44, + "output": 88.0, + "server_tools": 0.0, + } + for model in ("claude-fable-5-2", "claude-mythos-5-2"): + with self.subTest(model=model): + cost = meter.cost_of( + claude_usage( + input_tokens=1_000_000, + output_tokens=1_000_000, + cache_read=1_000_000, + cache_write_5m=1_000_000, + cache_write_1h=1_000_000, + speed="fast", + inference_geo="us", + ), + model, + "claude", + ) + for component, value in expected.items(): + self.assertAlmostEqual(cost[component], value) + + def test_fable_5_2_standard_pricing_and_dated_snapshot(self): + usage = claude_usage( + input_tokens=1_000_000, + output_tokens=1_000_000, + cache_read=1_000_000, + cache_write_5m=1_000_000, + cache_write_1h=1_000_000, + ) + expected = {"input": 8.0, "cache_write": 26.0, "cache_read": 0.2, "output": 40.0} + for model in ("claude-fable-5-2", "claude-fable-5-2-20261110"): + with self.subTest(model=model): + cost = meter.cost_of(usage, model, "claude") + for component, value in expected.items(): + self.assertAlmostEqual(cost[component], value) + def test_five_minute_and_one_hour_cache_writes_use_distinct_rates(self): cost = meter.cost_of( claude_usage( diff --git a/tests/test_pricing.py b/tests/test_pricing.py index 07f07d1..0820273 100644 --- a/tests/test_pricing.py +++ b/tests/test_pricing.py @@ -116,7 +116,7 @@ def test_reading_settings_never_rewrites_obsolete_terms(self): class PricingTests(unittest.TestCase): def test_builtin_pricing_exposes_reviewed_primary_sources(self): pricing = meter.model_pricing_settings() - self.assertEqual(pricing["reviewed_on"], "2026-09-30") + self.assertEqual(pricing["reviewed_on"], "2026-11-11") self.assertEqual( [source["provider"] for source in pricing["sources"]], ["anthropic", "openai", "cursor", "google"], diff --git a/token_meter/app_helpers/claude_traces.py b/token_meter/app_helpers/claude_traces.py index 193c295..af48077 100644 --- a/token_meter/app_helpers/claude_traces.py +++ b/token_meter/app_helpers/claude_traces.py @@ -50,8 +50,8 @@ def _claude_effective_duration(group, duration_s, timing_basis): _CLAUDE_INFERENCE_GEO_RULES = frozenset(( - "claude-mythos-5", "claude-mythos-5-1", - "claude-fable-5", "claude-fable-5-1", + "claude-mythos-5", "claude-mythos-5-1", "claude-mythos-5-2", + "claude-fable-5", "claude-fable-5-1", "claude-fable-5-2", "claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-5-5", "claude-sonnet-5", "claude-sonnet-4-6", "claude-haiku-5-5", diff --git a/token_meter/app_helpers/common.py b/token_meter/app_helpers/common.py index fb6ea55..1c63a3f 100644 --- a/token_meter/app_helpers/common.py +++ b/token_meter/app_helpers/common.py @@ -991,6 +991,7 @@ def _cached_price_query(model_provider, model_id, variant, timestamp): _CLAUDE_FAST_PREMIUM_RULES = frozenset(( + "claude-fable-5-2", "claude-mythos-5-2", "claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", )) diff --git a/token_meter/models/catalog.py b/token_meter/models/catalog.py index daf5fe2..a80751a 100644 --- a/token_meter/models/catalog.py +++ b/token_meter/models/catalog.py @@ -20,7 +20,7 @@ "google": "google", } MODEL_PRICE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+-]{0,159}$") -BUILTIN_PRICE_REVIEWED_ON = "2026-09-30" +BUILTIN_PRICE_REVIEWED_ON = "2026-11-11" BUILTIN_PRICE_SOURCES = ( { "provider": "anthropic", @@ -80,6 +80,13 @@ "claude-mythos-5-1": { "input": 10.0, "output": 50.0, "cache_write": 12.50, "cache_read": 0.25, }, + # Fable 5.2 and Mythos 5.2 pricing page, checked 2026-11-11: cache hits are 0.025x input. + "claude-fable-5-2": { + "input": 8.0, "output": 40.0, "cache_write": 10.0, "cache_read": 0.20, + }, + "claude-mythos-5-2": { + "input": 8.0, "output": 40.0, "cache_write": 10.0, "cache_read": 0.20, + }, # Current published standard pricing is $2 input / $10 output per 1M tokens. "claude-sonnet-5": { "input": 2.0, "output": 10.0, "cache_write": 2.50, "cache_read": 0.20, @@ -339,9 +346,10 @@ # Documented context windows for the current Claude lineup. Older or unlisted # model ids are absent on purpose: their window stays unknown, never guessed. -CLAUDE_CONTEXT_WINDOWS_REVIEWED_ON = "2026-10-30" +CLAUDE_CONTEXT_WINDOWS_REVIEWED_ON = "2026-11-11" CLAUDE_CONTEXT_WINDOWS_SOURCE = "https://platform.claude.com/docs/en/about-claude/models/overview" CLAUDE_CONTEXT_WINDOWS = { + "claude-fable-5-2": 1_000_000, "claude-fable-5-1": 1_000_000, "claude-opus-5-5": 1_000_000, "claude-sonnet-5-5": 1_000_000, diff --git a/token_meter/models/tiers.py b/token_meter/models/tiers.py index 3d11f57..c101265 100644 --- a/token_meter/models/tiers.py +++ b/token_meter/models/tiers.py @@ -11,6 +11,8 @@ # https://developers.openai.com/api/docs/models TIER_MODELS = { "anthropic": { + "claude-mythos-5-2": "frontier", + "claude-fable-5-2": "frontier", "claude-mythos-5-1": "frontier", "claude-mythos-5": "frontier", "claude-fable-5-1": "frontier",