From 26dd976fbe39b4d915d701bed85ea24ffd45d28c Mon Sep 17 00:00:00 2001 From: Ivan Miao Date: Fri, 17 Jul 2026 09:42:23 +0200 Subject: [PATCH] fix: align Gemini billing pricing with provider catalog --- agent/usage_pricing.py | 40 ++++++++++++++--- tests/agent/test_usage_pricing.py | 75 ++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index 84956b2dfff8..ccb3560a9768 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -518,7 +518,6 @@ _OFFICIAL_DOCS_PRICING: Dict[tuple[str, str], PricingEntry] = { input_cost_per_million=Decimal("1.50"), output_cost_per_million=Decimal("9.00"), cache_read_cost_per_million=Decimal("0.15"), - cache_write_cost_per_million=Decimal("1.50"), source="official_docs_snapshot", source_url="https://ai.google.dev/pricing", pricing_version="google-pricing-2026-07-07", @@ -530,7 +529,6 @@ _OFFICIAL_DOCS_PRICING: Dict[tuple[str, str], PricingEntry] = { input_cost_per_million=Decimal("2.00"), output_cost_per_million=Decimal("12.00"), cache_read_cost_per_million=Decimal("0.20"), - cache_write_cost_per_million=Decimal("2.00"), source="official_docs_snapshot", source_url="https://ai.google.dev/pricing", pricing_version="google-pricing-2026-07-07", @@ -542,7 +540,28 @@ _OFFICIAL_DOCS_PRICING: Dict[tuple[str, str], PricingEntry] = { input_cost_per_million=Decimal("0.25"), output_cost_per_million=Decimal("1.50"), cache_read_cost_per_million=Decimal("0.025"), - cache_write_cost_per_million=Decimal("0.25"), + source="official_docs_snapshot", + source_url="https://ai.google.dev/pricing", + pricing_version="google-pricing-2026-07-07", + ), + ( + "google", + "gemini-3-pro-preview", + ): PricingEntry( + input_cost_per_million=Decimal("2.00"), + output_cost_per_million=Decimal("12.00"), + cache_read_cost_per_million=Decimal("0.20"), + source="official_docs_snapshot", + source_url="https://ai.google.dev/pricing", + pricing_version="google-pricing-2026-07-07", + ), + ( + "google", + "gemini-3-flash-preview", + ): PricingEntry( + input_cost_per_million=Decimal("0.50"), + output_cost_per_million=Decimal("3.00"), + cache_read_cost_per_million=Decimal("0.05"), source="official_docs_snapshot", source_url="https://ai.google.dev/pricing", pricing_version="google-pricing-2026-07-07", @@ -554,7 +573,6 @@ _OFFICIAL_DOCS_PRICING: Dict[tuple[str, str], PricingEntry] = { input_cost_per_million=Decimal("1.25"), output_cost_per_million=Decimal("10.00"), cache_read_cost_per_million=Decimal("0.125"), - cache_write_cost_per_million=Decimal("1.25"), source="official_docs_snapshot", source_url="https://ai.google.dev/pricing", pricing_version="google-pricing-2026-07-07", @@ -566,7 +584,6 @@ _OFFICIAL_DOCS_PRICING: Dict[tuple[str, str], PricingEntry] = { input_cost_per_million=Decimal("0.15"), output_cost_per_million=Decimal("0.60"), cache_read_cost_per_million=Decimal("0.015"), - cache_write_cost_per_million=Decimal("0.15"), source="official_docs_snapshot", source_url="https://ai.google.dev/pricing", pricing_version="google-pricing-2026-07-07", @@ -578,7 +595,6 @@ _OFFICIAL_DOCS_PRICING: Dict[tuple[str, str], PricingEntry] = { input_cost_per_million=Decimal("0.10"), output_cost_per_million=Decimal("0.40"), cache_read_cost_per_million=Decimal("0.01"), - cache_write_cost_per_million=Decimal("0.10"), source="official_docs_snapshot", source_url="https://ai.google.dev/pricing", pricing_version="google-pricing-2026-07-07", @@ -920,6 +936,18 @@ for _base_56 in ("gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"): ] del _base_56 +# The direct Gemini provider currently exposes preview IDs for these two +# models. Keep the official snapshot keyed by both their documented stable +# names and the provider's emitted IDs so a catalog selection is billable. +for _alias, _canonical in { + "gemini-3.1-pro-preview": "gemini-3.1-pro", + "gemini-3.1-flash-lite-preview": "gemini-3.1-flash-lite", +}.items(): + _OFFICIAL_DOCS_PRICING[("google", _alias)] = _OFFICIAL_DOCS_PRICING[ + ("google", _canonical) + ] +del _alias, _canonical + def _to_decimal(value: Any) -> Optional[Decimal]: if value is None: diff --git a/tests/agent/test_usage_pricing.py b/tests/agent/test_usage_pricing.py index b17755247e97..ccab46d87f53 100644 --- a/tests/agent/test_usage_pricing.py +++ b/tests/agent/test_usage_pricing.py @@ -5,6 +5,7 @@ from agent.usage_pricing import ( estimate_usage_cost, get_pricing_entry, normalize_usage, + resolve_billing_route, ) @@ -638,27 +639,59 @@ def test_deepseek_v4_flash_estimate_usage_cost(): assert float(result.amount_usd) == 0.28 -def test_gemini_3_5_flash_pricing_resolved(): - """Ensure gemini-3.5-flash pricing exists and resolves correctly.""" - entry = get_pricing_entry("gemini-3.5-flash", provider="google") - assert entry is not None - assert float(entry.input_cost_per_million) == 1.50 - assert float(entry.output_cost_per_million) == 9.00 - assert float(entry.cache_read_cost_per_million) == 0.15 - assert float(entry.cache_write_cost_per_million) == 1.50 +def test_gemini_catalog_models_estimate_cached_usage(): + """Every direct-Gemini catalog model with official pricing can estimate a + session that includes a cache hit, rather than reporting ``unknown``. + """ + from hermes_cli.models import _PROVIDER_MODELS + + usage = CanonicalUsage(input_tokens=100, output_tokens=100, cache_read_tokens=100) + results = [ + estimate_usage_cost(model, usage, provider="gemini") + for model in _PROVIDER_MODELS["gemini"] + ] + + assert results + assert all(result.status == "estimated" for result in results) + assert all(result.amount_usd is not None and result.amount_usd > 0 for result in results) -def test_gemini_provider_maps_to_google(): - """Ensure using 'gemini' as provider resolves to 'google' pricing.""" - entry = get_pricing_entry("gemini-3.5-flash", provider="gemini") - assert entry is not None - assert float(entry.input_cost_per_million) == 1.50 - - # Also check base URL matching - entry_base = get_pricing_entry( - "gemini-3.5-flash", - provider="custom", - base_url="https://generativelanguage.googleapis.com/v1beta", +def test_google_and_vertex_routes_share_official_pricing_snapshot(): + """Direct Gemini, Vertex, and Vertex's OpenAI-compatible hostname must + all normalize to the Google official-pricing route. + """ + routes = ( + resolve_billing_route("model", provider="gemini"), + resolve_billing_route("google/model", provider="vertex"), + resolve_billing_route( + "google/model", + provider="custom", + base_url="https://aiplatform.googleapis.com/v1/projects/example", + ), ) - assert entry_base is not None - assert float(entry_base.input_cost_per_million) == 1.50 + + assert all(route.provider == "google" for route in routes) + assert all(route.billing_mode == "official_docs_snapshot" for route in routes) + + +def test_vertex_default_model_estimates_cached_usage(monkeypatch): + """The bundled Vertex profile's default auxiliary model must fall back to + Google snapshot pricing when the OpenAI-compatible endpoint has no model + metadata, including for cache-read accounting. + """ + from providers import get_provider_profile + + monkeypatch.setattr( + "agent.usage_pricing.fetch_endpoint_model_metadata", + lambda *_args, **_kwargs: {}, + ) + vertex = get_provider_profile("vertex") + result = estimate_usage_cost( + vertex.default_aux_model, + CanonicalUsage(input_tokens=100, output_tokens=100, cache_read_tokens=100), + provider=vertex.name, + base_url=vertex.base_url, + ) + + assert result.status == "estimated" + assert result.amount_usd is not None and result.amount_usd > 0