From b395f66c031e5c09cd2e2b89192da9482bcce08f Mon Sep 17 00:00:00 2001 From: olegbrok Date: Fri, 10 Jul 2026 14:31:41 -0700 Subject: [PATCH 1/4] =?UTF-8?q?fix(analytics):=20codex=20tmux=20turns=20pr?= =?UTF-8?q?iced=20correctly=20=E2=80=94=20provider=20attribution,=20usage?= =?UTF-8?q?=20normalization,=20gpt-5.6-sol=20rates=20(#860)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Source changes (tests follow in next commit): - TmuxSession._ANALYTICS_PROVIDER class attr (anthropic) used at the log_turn_usage call instead of a hardcoded "anthropic"; CodexTmuxSession overrides with "codex_cli" (SDK-path default, aliased onto openai rates by _provider_alias). - _normalize_turn_usage hook: identity on the Claude transport; CodexTmuxSession converts the codex schema (inclusive input_tokens + cached_input_tokens) to the daemon's disjoint convention ONCE, before any consumer reads the dict — mirrors CodexSession.uncached_input_tokens. - pricing.RATE_TABLE: OpenAI family (gpt-5.6-sol, gpt-5.5 at 5/30/0.50; gpt-5.3-codex at 1.75/14/0.175); no cache-write billing. - analytics seeds: gpt-5.6-sol row; _migrate_codex_provider_attribution heals deployed anthropic/gpt-% rows to codex_cli (idempotent, narrow). - agent_registry: gpt-5.6-sol catalog seed; gpt-5.5 catalog price realigned to official 5/30/0.50 via _PRICE_CORRECTIONS. Co-Authored-By: Claude Fable 5 --- src/pinky_daemon/agent_registry.py | 7 ++++- src/pinky_daemon/analytics_store.py | 33 +++++++++++++++++++++++ src/pinky_daemon/codex_tmux_session.py | 37 ++++++++++++++++++++++++++ src/pinky_daemon/pricing.py | 28 +++++++++++++++++++ src/pinky_daemon/tmux_session.py | 30 ++++++++++++++++++++- 5 files changed, 133 insertions(+), 2 deletions(-) diff --git a/src/pinky_daemon/agent_registry.py b/src/pinky_daemon/agent_registry.py index 22cf1d28..c5a09442 100644 --- a/src/pinky_daemon/agent_registry.py +++ b/src/pinky_daemon/agent_registry.py @@ -3966,7 +3966,8 @@ def delete_bot_token(self, token_id: str) -> bool: ("anthropic", "claude-opus-4-5", "Claude Opus 4.5", "Previous-gen Opus.", "opus", 200_000, 0, 5.0, 25.0, 0.5, 1, 40), ("anthropic", "claude-sonnet-4-5", "Claude Sonnet 4.5", "Previous-gen Sonnet.", "sonnet", 200_000, 0, 3.0, 15.0, 0.3, 1, 50), # OpenAI / Codex CLI - ("openai", "gpt-5.5", "GPT-5.5", "Newest frontier. Coding + reasoning. Codex sign-in auth only (API pending).", "flagship", 200_000, 0, 1.75, 14.0, 0.175, 0, 55), + ("openai", "gpt-5.6-sol", "GPT-5.6 Sol", "Current codex fleet model (2026-07). Frontier coding + reasoning. Codex sign-in auth only (API pending).", "flagship", 200_000, 0, 5.0, 30.0, 0.5, 0, 54), + ("openai", "gpt-5.5", "GPT-5.5", "Previous frontier. Coding + reasoning. Codex sign-in auth only (API pending).", "flagship", 200_000, 0, 5.0, 30.0, 0.5, 0, 55), ("openai", "gpt-5.4", "GPT-5.4", "Flagship. Complex reasoning & coding.", "flagship", 200_000, 0, 1.75, 14.0, 0.175, 0, 60), ("openai", "gpt-5.4-mini", "GPT-5.4 Mini", "Fast + capable. Daily driver.", "mid", 200_000, 0, 0.25, 2.0, 0.025, 0, 70), ("openai", "gpt-5.4-nano", "GPT-5.4 Nano", "Cheapest. High-volume tasks.", "low", 200_000, 0, 0.05, 0.4, 0.005, 0, 80), @@ -3986,6 +3987,10 @@ def delete_bot_token(self, token_id: str) -> bool: ("anthropic/claude-opus-4-6", (15.0, 75.0, 1.5), (5.0, 25.0, 0.5)), ("anthropic/claude-opus-4-5", (15.0, 75.0, 1.5), (5.0, 25.0, 0.5)), ("anthropic/claude-haiku-4-5", (0.8, 4.0, 0.08), (1.0, 5.0, 0.1)), + # #860: gpt-5.5 was seeded at the gpt-5.2-tier $1.75/$14 while the + # official rate is $5/$30 (cached $0.50) — analytics_store and + # pricing.RATE_TABLE always had it right; this realigns the catalog. + ("openai/gpt-5.5", (1.75, 14.0, 0.175), (5.0, 30.0, 0.5)), ] def _seed_models(self) -> None: diff --git a/src/pinky_daemon/analytics_store.py b/src/pinky_daemon/analytics_store.py index 576f64a2..75f3c05e 100644 --- a/src/pinky_daemon/analytics_store.py +++ b/src/pinky_daemon/analytics_store.py @@ -221,6 +221,7 @@ def _init_db(self) -> None: self._migrate_opus_haiku_seed_pricing(conn) self._migrate_legacy_dotted_model_ids(conn) self._ensure_pricing_rows(conn) + self._migrate_codex_provider_attribution(conn) # Schema migration: add user_message_snippet to turn_usage cols = { r[1] for r in conn.execute("PRAGMA table_info(analytics_turn_usage)").fetchall() @@ -258,6 +259,11 @@ def _init_db(self) -> None: _LATEST_PRICING_ADDITIONS = [ ("openai", "gpt-5.5", "2020-01-01T00:00:00Z", None, 5.00, 30.00, 0.50, "seed"), ("openai", "gpt-5.3-codex", "2020-01-01T00:00:00Z", None, 1.75, 14.00, 0.175, "seed"), + # gpt-5.6-sol (#860): codex fleet model since 2026-07-09. Official + # rates identical to gpt-5.5 (developers.openai.com, verified + # 2026-07-10). Mirrors pricing.RATE_TABLE (_GPT_FRONTIER), parity-pinned + # by test_seed_pricing_matches_rate_table. + ("openai", "gpt-5.6-sol", "2020-01-01T00:00:00Z", None, 5.00, 30.00, 0.50, "seed"), # Sonnet 5 (2026-06): the current Sonnet tier. Standard $3/$15 (cached # $0.30) — mirrors pinky_daemon.pricing.RATE_TABLE (_SONNET), pinned by # test_seed_pricing_matches_rate_table. Introductory $2/$10 runs through @@ -470,6 +476,33 @@ def _ensure_pricing_rows(self, conn) -> None: (*r, r[0], r[1]), ) + def _migrate_codex_provider_attribution(self, conn) -> None: + """Reattribute codex turns mislogged as ``anthropic`` (#860). + + ``TmuxSession._log_turn_cost_and_analytics`` hardcoded + ``provider="anthropic"``, so every CodexTmuxSession turn landed as + ``anthropic/gpt-*`` — a (provider, model) pair the pricing lookup can + never match (``_provider_alias`` never crosses providers), pricing all + codex history at $0. Heal deployed rows to ``codex_cli`` — the value + new codex turns record and the SDK path's default — which aliases onto + the openai rate rows at read time. + + Follows the ``_migrate_opus_haiku_seed_pricing`` precedent: idempotent + (the predicate matches only the misattributed pairs, so a second run is + a no-op) and narrow (a ``gpt-*`` model under ``anthropic`` cannot be + legitimate — Anthropic serves no gpt model — so no honest row can + match). Touches turn usage AND session facts so per-session rollups + group consistently. + """ + for table in ("analytics_turn_usage", "analytics_session_facts"): + conn.execute( + f""" + UPDATE {table} + SET provider = 'codex_cli' + WHERE provider = 'anthropic' AND model LIKE 'gpt-%' + """ + ) + def ensure_session_fact( self, *, diff --git a/src/pinky_daemon/codex_tmux_session.py b/src/pinky_daemon/codex_tmux_session.py index 38f7a83d..db8ba471 100644 --- a/src/pinky_daemon/codex_tmux_session.py +++ b/src/pinky_daemon/codex_tmux_session.py @@ -86,6 +86,43 @@ async def paste_text( class CodexTmuxSession(TmuxSession): """Interactive codex-CLI REPL in a detached tmux pane (see module docstring).""" + # #860: analytics rows must not claim "anthropic" for codex turns — the + # pricing lookup keys on (provider, model) and never crosses providers, so + # anthropic/gpt-* priced every turn at $0. "codex_cli" matches the SDK + # path's default (CodexSession._analytics_log_turn_usage) and + # analytics_store._provider_alias maps it onto the openai rate rows. + _ANALYTICS_PROVIDER = "codex_cli" + + @staticmethod + def _normalize_turn_usage(u: dict) -> dict: + """Codex usage schema → the daemon's disjoint convention (#860). + + Codex reports ``input_tokens`` INCLUSIVE of the cached prefix, with the + cached span under ``cached_input_tokens`` — while every consumer here + (SessionUsage accumulation, ``compute_cost_from_usage``, analytics + ``log_turn_usage``, the context gauge's window reconstruction) follows + the Anthropic convention: ``input_tokens`` is the uncached remainder + and the cached span rides ``cache_read_input_tokens``. Mirrors + ``CodexSession.uncached_input_tokens`` on the SDK path; pricing the + inclusive number at the full input rate would overstate review-heavy + sessions roughly 10x. The ambiguous source key is consumed, not + forwarded. A dict without ``cached_input_tokens`` (or with malformed + counts) passes through untouched — fail toward the visible undercount, + never a silent double-bill. + """ + if "cached_input_tokens" not in u: + return u + try: + cached = max(0, int(u.get("cached_input_tokens") or 0)) + inclusive = max(0, int(u.get("input_tokens") or 0)) + except (TypeError, ValueError): + return u + out = dict(u) + out.pop("cached_input_tokens", None) + out["input_tokens"] = max(0, inclusive - cached) + out["cache_read_input_tokens"] = cached + return out + def __init__( self, config: StreamingSessionConfig, diff --git a/src/pinky_daemon/pricing.py b/src/pinky_daemon/pricing.py index 0582912f..13daedb6 100644 --- a/src/pinky_daemon/pricing.py +++ b/src/pinky_daemon/pricing.py @@ -93,6 +93,28 @@ "cache_write_5m": 1.00, "cache_write_1h": 1.60, } +# OpenAI / Codex family (#860): powers the live tmux cost path for codex +# agents (CodexTmuxSession rides the same _log_turn_cost_and_analytics as +# Claude tmux). Official OpenAI API rates (developers.openai.com, verified +# 2026-07-10); codex agents run on sign-in subscription, so as with CC +# subscription agents these are notional usage-value figures (#648). OpenAI +# bills no cache WRITE — write rates are 0; cached reads use the published +# cached-input rate. Must mirror the analytics openai seeds +# (test_seed_pricing_matches_rate_table, #669). +_GPT_FRONTIER = { # gpt-5.5 / gpt-5.6-sol tier + "input": 5.00, + "output": 30.00, + "cache_read": 0.50, + "cache_write_5m": 0.0, + "cache_write_1h": 0.0, +} +_GPT_53_CODEX = { + "input": 1.75, + "output": 14.00, + "cache_read": 0.175, + "cache_write_5m": 0.0, + "cache_write_1h": 0.0, +} # Bare-model-id → rate dict. Add new model ids here on each release. RATE_TABLE: dict[str, dict[str, float]] = { @@ -117,6 +139,12 @@ "claude-sonnet-4": _SONNET, "claude-haiku-4-5": _HAIKU_45, "claude-haiku-3-5": _HAIKU_35, + # Codex fleet models (#860). gpt-5.6-sol is the current fleet model + # (2026-07-09→); gpt-5.5 the previous one; gpt-5.3-codex kept because the + # analytics seed carries it (parity-pinned). + "gpt-5.6-sol": _GPT_FRONTIER, + "gpt-5.5": _GPT_FRONTIER, + "gpt-5.3-codex": _GPT_53_CODEX, } diff --git a/src/pinky_daemon/tmux_session.py b/src/pinky_daemon/tmux_session.py index 0131fe3c..853fd2a5 100644 --- a/src/pinky_daemon/tmux_session.py +++ b/src/pinky_daemon/tmux_session.py @@ -3804,6 +3804,27 @@ def _soft_nudge_threshold_pct(self) -> float: pass return DEFAULT_CONTEXT_NUDGE_THRESHOLD_PCT + # Provider recorded on analytics_turn_usage rows (#860). Class-level so + # transport subclasses that ride this same cost/analytics path attribute + # their turns truthfully — CodexTmuxSession overrides with "codex_cli" + # (analytics_store._provider_alias maps it to the openai rate rows). + _ANALYTICS_PROVIDER: str = "anthropic" + + @staticmethod + def _normalize_turn_usage(u: dict) -> dict: + """Transport-specific usage normalization hook (#860). + + The base (Claude) transport already emits the daemon's disjoint + convention, so this is the identity. CodexTmuxSession overrides it to + convert the codex schema (``input_tokens`` INCLUSIVE of the cached + prefix, cached span under ``cached_input_tokens``) before any + consumer — usage accumulation, pricing, analytics, context gauge — + reads the dict. ONE conversion point by design: a partial conversion + spread across consumers is how the cached split got silently dropped + in the first place. + """ + return u + def _record_turn_usage(self, response: TurnResponse) -> None: """Fold a turn's usage block into ``self.usage`` (SessionUsage). @@ -3929,7 +3950,10 @@ def _log_turn_cost_and_analytics(self, response: TurnResponse) -> None: session_id=self.id, agent_name=self.agent_name, turn_seq=turn_seq, - provider="anthropic", + # #860: was hardcoded "anthropic", which mispriced every + # CodexTmuxSession turn to $0 (anthropic/gpt-* never + # matches the openai rate rows). + provider=self._ANALYTICS_PROVIDER, model=model, input_tokens=input_tokens, output_tokens=output_tokens, @@ -4380,6 +4404,10 @@ async def _handle_turn_complete(self, response: TurnResponse) -> None: # tailer already pulled the usage dict out of each assistant # entry's ``usage`` block; we just need to fold it into the # session-level dataclass and emit it. + # #860: normalize transport-specific usage schemas ONCE, before any + # consumer (accumulation, pricing, analytics, context gauge) reads it. + if isinstance(response.usage, dict): + response.usage = self._normalize_turn_usage(response.usage) self._record_turn_usage(response) # #648 — forward per-turn usage to analytics + cost tracking so # tmux agents reach live Analytics / lifetime-cost parity with the From e99c9577e9337b7fb0e54b22731bc97eb4c2145c Mon Sep 17 00:00:00 2001 From: olegbrok Date: Fri, 10 Jul 2026 14:55:03 -0700 Subject: [PATCH 2/4] test(analytics): #860 coverage + migration reprices history at the cached rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests for every #860 seam: - tmux_session: provider comes from _ANALYTICS_PROVIDER (subclass-driven), base _normalize_turn_usage is identity, hook runs BEFORE all consumers (accumulation, pricing, analytics see the same normalized dict). - codex_tmux_session: _ANALYTICS_PROVIDER=codex_cli; normalization unit matrix (convert/pass-through/malformed/clamp/no-mutate); end-to-end turn-complete pricing a codex-schema usage dict at openai rates with the disjoint split. - pricing: gpt-5.6-sol/gpt-5.5 ($5/$30/$0.50), gpt-5.3-codex ($1.75/$14/$0.175), cache writes bill $0. - analytics_store: seed/RATE_TABLE parity guard restructured to be provider-aware (gpt rows must sit under openai); migration tests (heal, reprice, narrow, idempotent). - agent_registry: gpt-5.6-sol catalog seed, openai seed/RATE_TABLE parity, gpt-5.5 stale-price correction + operator-override safety. Migration amendment (src): the provider flip alone would over-bill history ~7x — pre-#860 rows logged codex's INCLUSIVE input with the cached span unrecorded (fleet rollouts measure 97.5% cached). Rows carrying that signature (cached_input_tokens=0) move the whole count to the cached column: error bounded ~-20%, failing toward the visible undercount, same direction as _normalize_turn_usage. Verified against a copy of the prod DB: 779 mislogged turns, $0.00 -> $33.43 (naive flip would have shown ~$320). Co-Authored-By: Claude Fable 5 --- src/pinky_daemon/analytics_store.py | 34 +++++-- tests/test_agent_registry.py | 62 ++++++++++++ tests/test_analytics_store.py | 151 ++++++++++++++++++++++++++-- tests/test_codex_tmux_session.py | 138 ++++++++++++++++++++++++- tests/test_pricing.py | 53 ++++++++++ tests/test_tmux_session.py | 77 ++++++++++++++ 6 files changed, 501 insertions(+), 14 deletions(-) diff --git a/src/pinky_daemon/analytics_store.py b/src/pinky_daemon/analytics_store.py index 75f3c05e..4faac696 100644 --- a/src/pinky_daemon/analytics_store.py +++ b/src/pinky_daemon/analytics_store.py @@ -477,7 +477,7 @@ def _ensure_pricing_rows(self, conn) -> None: ) def _migrate_codex_provider_attribution(self, conn) -> None: - """Reattribute codex turns mislogged as ``anthropic`` (#860). + """Reattribute + reprice codex turns mislogged as ``anthropic`` (#860). ``TmuxSession._log_turn_cost_and_analytics`` hardcoded ``provider="anthropic"``, so every CodexTmuxSession turn landed as @@ -487,13 +487,35 @@ def _migrate_codex_provider_attribution(self, conn) -> None: new codex turns record and the SDK path's default — which aliases onto the openai rate rows at read time. + The provider flip alone would over-bill history: pre-#860 rows also + predate ``_normalize_turn_usage``, so their ``input_tokens`` is + codex's INCLUSIVE count with the cached span invisible + (``cached_input_tokens=0`` — the old extraction read only the + Anthropic cache keys). Repricing that at the full input rate + overstates ~7x (measured across the fleet's 2026-07 rollouts: 97.5% + of codex input is cached reads). The per-turn split is unrecoverable + from the DB, so rows carrying that signature move the whole count to + the cached column: error bounded at roughly -20% (the true-uncached + share priced at 10x the cached rate), failing toward the visible + undercount — the same direction ``_normalize_turn_usage`` chooses. + Follows the ``_migrate_opus_haiku_seed_pricing`` precedent: idempotent - (the predicate matches only the misattributed pairs, so a second run is - a no-op) and narrow (a ``gpt-*`` model under ``anthropic`` cannot be - legitimate — Anthropic serves no gpt model — so no honest row can - match). Touches turn usage AND session facts so per-session rollups - group consistently. + (both predicates match only misattributed ``anthropic`` pairs, gone + after the first run) and narrow (a ``gpt-*`` model under ``anthropic`` + cannot be legitimate — Anthropic serves no gpt model — so no honest + row can match). Touches turn usage AND session facts so per-session + rollups group consistently. """ + conn.execute( + """ + UPDATE analytics_turn_usage + SET provider = 'codex_cli', + cached_input_tokens = input_tokens, + input_tokens = 0 + WHERE provider = 'anthropic' AND model LIKE 'gpt-%' + AND cached_input_tokens = 0 + """ + ) for table in ("analytics_turn_usage", "analytics_session_facts"): conn.execute( f""" diff --git a/tests/test_agent_registry.py b/tests/test_agent_registry.py index 6e567e93..3fc5e85e 100644 --- a/tests/test_agent_registry.py +++ b/tests/test_agent_registry.py @@ -995,3 +995,65 @@ def test_operator_customized_prices_survive_reseed(self, registry): if m["id"] == "anthropic/claude-opus-4-8" ) assert opus["input_price"] == 12.34 + + def test_gpt_56_sol_seeded_at_frontier_rates(self, registry): + """#860: gpt-5.6-sol (the codex fleet model since 2026-07) must be in + the catalog at the official $5/$30 (cached $0.50).""" + models = { + m["model_id"]: m + for m in registry.list_models(provider="openai", active_only=False) + } + assert "gpt-5.6-sol" in models + sol = models["gpt-5.6-sol"] + assert (sol["input_price"], sol["output_price"], + sol["cached_input_price"]) == (5.0, 30.0, 0.5) + assert sol["tier"] == "flagship" + + def test_openai_seed_prices_match_pricing_rate_table(self, registry): + """#860 extends the #741 invariant to the OpenAI family: catalog + display prices must agree with pricing.py (the actual cost engine) + for every openai model both tables know.""" + from pinky_daemon.pricing import RATE_TABLE + + checked = 0 + for m in registry.list_models(provider="openai", active_only=False): + rate = RATE_TABLE.get(m["model_id"]) + if rate is None: + continue + assert m["input_price"] == rate["input"], m["model_id"] + assert m["output_price"] == rate["output"], m["model_id"] + assert m["cached_input_price"] == rate["cache_read"], m["model_id"] + checked += 1 + # Guard the guard: gpt-5.6-sol + gpt-5.5 must both be intersecting. + assert checked >= 2 + + def test_stale_gpt55_price_corrected_on_existing_rows(self, registry): + """#860: deployed DBs seeded gpt-5.5 at the gpt-5.2-tier $1.75/$14; + the next seed pass realigns existing rows to the official $5/$30 + (INSERT OR IGNORE alone never reaches them).""" + registry._db.execute( + "UPDATE models SET input_price=1.75, output_price=14.0," + " cached_input_price=0.175 WHERE id='openai/gpt-5.5'" + ) + registry._db.commit() + registry._seed_models() + gpt = next( + m for m in registry.list_models(provider="openai", active_only=False) + if m["id"] == "openai/gpt-5.5" + ) + assert (gpt["input_price"], gpt["output_price"], + gpt["cached_input_price"]) == (5.0, 30.0, 0.5) + + def test_operator_customized_gpt55_price_survives_reseed(self, registry): + """Same exact-stale-triple gate as the Anthropic corrections — an + operator-priced gpt-5.5 row must not be clobbered.""" + registry._db.execute( + "UPDATE models SET input_price=9.99 WHERE id='openai/gpt-5.5'" + ) + registry._db.commit() + registry._seed_models() + gpt = next( + m for m in registry.list_models(provider="openai", active_only=False) + if m["id"] == "openai/gpt-5.5" + ) + assert gpt["input_price"] == 9.99 diff --git a/tests/test_analytics_store.py b/tests/test_analytics_store.py index 17c578ca..82fbc3b0 100644 --- a/tests/test_analytics_store.py +++ b/tests/test_analytics_store.py @@ -456,25 +456,38 @@ class TestSeedRateTableParity: per-turn). They must agree for the same model, or the same usage reports different dollar figures. This pins them together as a drift guard.""" + # RATE_TABLE keys are bare model ids; the Analytics seed keys on + # (provider, model). #860 added the OpenAI/codex family to RATE_TABLE, so + # the guard is provider-aware: each family must be seeded under the RIGHT + # provider (a gpt row seeded as anthropic would be unpriceable at read + # time — _provider_alias never crosses providers). + @staticmethod + def _expected_provider(model: str) -> str: + return "anthropic" if model.startswith("claude-") else "openai" + def test_seed_pricing_matches_rate_table(self, tmp_path): store = _store(tmp_path) with store._connect() as conn: rows = conn.execute( - "SELECT model, input_usd_per_mtok, output_usd_per_mtok, " - "cached_input_usd_per_mtok FROM analytics_model_pricing " - "WHERE provider = 'anthropic'" + "SELECT provider, model, input_usd_per_mtok, output_usd_per_mtok, " + "cached_input_usd_per_mtok FROM analytics_model_pricing" ).fetchall() # Seed ids are canonical (hyphenated) since #759, so they line up with # RATE_TABLE directly — no normalization. A dotted-id regression would # drop the matching RATE_TABLE key out of `seeded` and fail the check. - seeded = {r["model"]: r for r in rows} + seeded = {(r["provider"], r["model"]): r for r in rows} - missing = [m for m in RATE_TABLE if m not in seeded] - assert not missing, f"models in RATE_TABLE but absent from Analytics seed: {missing}" + missing = [ + m for m in RATE_TABLE if (self._expected_provider(m), m) not in seeded + ] + assert not missing, ( + f"models in RATE_TABLE but absent from the Analytics seed " + f"(under their expected provider): {missing}" + ) mismatches = [] for model, rates in RATE_TABLE.items(): - row = seeded[model] + row = seeded[(self._expected_provider(model), model)] if ( row["input_usd_per_mtok"] != rates["input"] or row["output_usd_per_mtok"] != rates["output"] @@ -588,6 +601,130 @@ def test_migration_preserves_operator_overrides(self, tmp_path): assert row is not None and row[0] == 15.00 +class TestCodexProviderAttributionMigration: + """#860: TmuxSession hardcoded provider="anthropic" at the log_turn_usage + call, so every CodexTmuxSession turn landed as anthropic/gpt-* — a + (provider, model) pair the pricing lookup can never match, costing all + codex tmux history at $0. _migrate_codex_provider_attribution heals + deployed rows to codex_cli on init (turn usage AND session facts), and + moves the pre-normalization INCLUSIVE input count to the cached column + (pricing it at the full input rate would overstate ~7x; see migration + docstring).""" + + def _log_mislogged_codex_turn(self, store: AnalyticsStore) -> None: + """One turn exactly as the pre-#860 tmux path wrote it: inclusive + input count, cached span invisible (cached_input_tokens=0).""" + store.ensure_session_fact( + session_id="codex1", agent_name="murzik", session_label="test", + provider="anthropic", model="gpt-5.5", + ) + store.log_turn_usage( + session_id="codex1", agent_name="murzik", turn_seq=1, + provider="anthropic", model="gpt-5.5", + input_tokens=1_000_000, output_tokens=0, cached_input_tokens=0, + ) + + def test_heals_both_tables_and_restores_pricing(self, tmp_path): + db = str(tmp_path / "analytics.db") + store = AnalyticsStore(db) + self._log_mislogged_codex_turn(store) + # The bug as deployed: anthropic/gpt-5.5 matches no rate row → $0. + overview = store.get_overview(range_name="7d") + assert overview["totals"]["cost_usd"] == pytest.approx(0.0) + + store2 = AnalyticsStore(db) # init runs the migration + with store2._connect() as conn: + tu = conn.execute( + "SELECT provider, input_tokens, cached_input_tokens " + "FROM analytics_turn_usage WHERE session_id='codex1'" + ).fetchall() + sf = conn.execute( + "SELECT provider FROM analytics_session_facts WHERE session_id='codex1'" + ).fetchall() + assert [r["provider"] for r in tu] == ["codex_cli"] + assert [r["provider"] for r in sf] == ["codex_cli"] + # The inclusive input count moved to the cached column. + assert tu[0]["input_tokens"] == 0 + assert tu[0]["cached_input_tokens"] == 1_000_000 + # History is repriced, not just relabeled: 1M cached @ $0.50/Mtok. + overview = store2.get_overview(range_name="7d") + assert overview["totals"]["cost_usd"] == pytest.approx(0.5) + + def test_row_with_cached_split_gets_provider_flip_only(self, tmp_path): + """A mislogged row that somehow carries a real cached split doesn't + match the pre-normalization signature — its tokens are already + disjoint, so only the provider is healed.""" + db = str(tmp_path / "analytics.db") + store = AnalyticsStore(db) + store.ensure_session_fact( + session_id="codex2", agent_name="murzik", session_label="test", + provider="anthropic", model="gpt-5.5", + ) + store.log_turn_usage( + session_id="codex2", agent_name="murzik", turn_seq=1, + provider="anthropic", model="gpt-5.5", + input_tokens=10_000, output_tokens=0, cached_input_tokens=90_000, + ) + store2 = AnalyticsStore(db) + with store2._connect() as conn: + row = conn.execute( + "SELECT provider, input_tokens, cached_input_tokens " + "FROM analytics_turn_usage WHERE session_id='codex2'" + ).fetchone() + assert row["provider"] == "codex_cli" + assert row["input_tokens"] == 10_000 + assert row["cached_input_tokens"] == 90_000 + + def test_narrow_leaves_honest_rows_alone(self, tmp_path): + """A gpt-* model under anthropic cannot be legitimate, but every + honest (provider, model) combination must survive untouched.""" + db = str(tmp_path / "analytics.db") + store = AnalyticsStore(db) + _seed_session(store) + honest = [ + ("a1", "anthropic", "claude-opus-4-8"), + ("c1", "codex_cli", "gpt-5.5"), + ("o1", "openai", "gpt-5.5"), + ] + for i, (sid, provider, model) in enumerate(honest, start=1): + store.log_turn_usage( + session_id=sid, agent_name="barsik", turn_seq=i, + provider=provider, model=model, + input_tokens=100, output_tokens=10, cached_input_tokens=0, + ) + store2 = AnalyticsStore(db) + with store2._connect() as conn: + rows = conn.execute( + "SELECT session_id, provider FROM analytics_turn_usage" + ).fetchall() + providers = {r["session_id"]: r["provider"] for r in rows} + assert providers == {"a1": "anthropic", "c1": "codex_cli", "o1": "openai"} + # The seeded session fact (anthropic/claude-sonnet-4) is honest too. + with store2._connect() as conn: + fact = conn.execute( + "SELECT provider FROM analytics_session_facts WHERE session_id='sess1'" + ).fetchone() + assert fact["provider"] == "anthropic" + + def test_idempotent_on_reopen(self, tmp_path): + """Both heal predicates key on provider='anthropic', gone after the + first run — a second init must not re-shift tokens or duplicate.""" + db = str(tmp_path / "analytics.db") + store = AnalyticsStore(db) + self._log_mislogged_codex_turn(store) + AnalyticsStore(db) # heals + store3 = AnalyticsStore(db) # second run must be a no-op + with store3._connect() as conn: + rows = conn.execute( + "SELECT provider, model, input_tokens, cached_input_tokens " + "FROM analytics_turn_usage WHERE session_id='codex1'" + ).fetchall() + assert len(rows) == 1 + assert (rows[0]["provider"], rows[0]["model"]) == ("codex_cli", "gpt-5.5") + assert rows[0]["input_tokens"] == 0 + assert rows[0]["cached_input_tokens"] == 1_000_000 + + class TestLegacyDottedIdMigration: """#759: deployed DBs seeded before the canonicalization keep dotted legacy ids. _migrate_legacy_dotted_model_ids renames them to the hyphenated form on diff --git a/tests/test_codex_tmux_session.py b/tests/test_codex_tmux_session.py index 073d40d3..5993088b 100644 --- a/tests/test_codex_tmux_session.py +++ b/tests/test_codex_tmux_session.py @@ -13,6 +13,7 @@ """ import asyncio import os +import time from unittest.mock import AsyncMock, MagicMock import pytest @@ -24,7 +25,15 @@ ) from pinky_daemon.codex_tmux_transcript import CodexTmuxTranscriptTailer from pinky_daemon.streaming_session import StreamingSessionConfig -from pinky_daemon.tmux_session import TmuxCommandResult, _TmuxControl +from pinky_daemon.tmux_session import ( + TmuxCommandResult, + TmuxSession, + _InflightMeta, + _QueuedTurn, + _TmuxControl, +) +from pinky_daemon.tmux_transcript import TurnResponse +from pinky_daemon.transport_state import SessionState def _ok(stdout: str = "") -> TmuxCommandResult: @@ -371,6 +380,133 @@ def test_container_isolation_transport_checked_before_runtime(): assert status == 400 +# ── #860 — cost attribution + usage normalization ─────────────────────────── +# The codex transport shares TmuxSession._log_turn_cost_and_analytics; its two +# seams are _ANALYTICS_PROVIDER (truthful provider on analytics rows) and +# _normalize_turn_usage (codex's inclusive input_tokens + cached_input_tokens +# → the daemon's disjoint convention, once, before any consumer). + + +def test_analytics_provider_is_codex_cli(): + """Codex turns must not claim "anthropic": the pricing lookup keys on + (provider, model) and _provider_alias never crosses providers, so + anthropic/gpt-* rows price at $0. "codex_cli" matches the SDK path's + default and aliases onto the openai rate rows.""" + assert CodexTmuxSession._ANALYTICS_PROVIDER == "codex_cli" + assert TmuxSession._ANALYTICS_PROVIDER == "anthropic" + + +def test_normalize_converts_codex_schema_to_disjoint(): + """Codex reports input_tokens INCLUSIVE of the cached prefix; the daemon + convention is disjoint (input = uncached remainder, cached span under + cache_read_input_tokens). The ambiguous source key is consumed.""" + u = {"input_tokens": 100_000, "cached_input_tokens": 90_000, + "output_tokens": 500} + out = CodexTmuxSession._normalize_turn_usage(u) + assert out["input_tokens"] == 10_000 + assert out["cache_read_input_tokens"] == 90_000 + assert "cached_input_tokens" not in out + assert out["output_tokens"] == 500 + # The source dict is copied, never mutated in place. + assert u["input_tokens"] == 100_000 + assert u["cached_input_tokens"] == 90_000 + + +def test_normalize_without_cached_key_passes_through(): + """No cached_input_tokens ⇒ nothing to convert; the dict passes through + untouched (fail toward the visible undercount, never a double-bill).""" + u = {"input_tokens": 5_000, "output_tokens": 100} + assert CodexTmuxSession._normalize_turn_usage(u) is u + + +def test_normalize_malformed_counts_pass_through(): + u = {"input_tokens": 5_000, "cached_input_tokens": "garbage"} + assert CodexTmuxSession._normalize_turn_usage(u) is u + + +def test_normalize_clamps_cached_larger_than_inclusive(): + """cached > inclusive shouldn't happen, but must clamp to 0 rather than + log a negative input count.""" + u = {"input_tokens": 1_000, "cached_input_tokens": 2_000} + out = CodexTmuxSession._normalize_turn_usage(u) + assert out["input_tokens"] == 0 + assert out["cache_read_input_tokens"] == 2_000 + + +def _seed_inflight(ss: CodexTmuxSession) -> None: + """Minimal _InflightMeta seed so _handle_turn_complete has a head entry + (mirrors tests/test_tmux_session.py's helper).""" + ss._inflight_metas.append( + _InflightMeta( + meta={}, + completion_event=None, + internal=False, + dispatched_at=time.time(), + turn=_QueuedTurn(prompt=""), + ) + ) + ss._head_started_at = time.time() + + +@pytest.mark.asyncio +async def test_turn_complete_prices_codex_turn_correctly(): + """#860 end-to-end: a codex-schema turn (usage exactly as the tailer + snapshots token_count.info.last_token_usage) must log provider=codex_cli, + the DISJOINT token split, and the openai-rate dollar figure. Pre-#860 this + row logged as anthropic/gpt-* at $0 — and had a rate row existed, the + inclusive input count would have over-billed the cached span ~10x on + review-heavy sessions.""" + analytics = MagicMock() + cost_cb = MagicMock() + cfg = StreamingSessionConfig( + agent_name="murzik", + working_dir="/tmp/codex-tmux-test", + model="gpt-5.6-sol", + provider_key="sk-test", + ) + ss = CodexTmuxSession( + cfg, + tmux_control=_mock_tmux(), + analytics_store=analytics, + cost_callback=cost_cb, + ) + ss._skip_wake_prompt_for_tests = True + ss._state_machine._state = SessionState.CONNECTED + _seed_inflight(ss) + + await ss._handle_turn_complete( + TurnResponse( + text="ok", + stop_reason="end_turn", + model="gpt-5.6-sol", + usage={ + "input_tokens": 100_000, # inclusive of cached (codex) + "cached_input_tokens": 90_000, + "output_tokens": 1_000, + }, + duration_ms=100, + assistant_entry_count=1, + tool_uses=[], + ) + ) + + analytics.log_turn_usage.assert_called_once() + kwargs = analytics.log_turn_usage.call_args.kwargs + assert kwargs["provider"] == "codex_cli" + assert kwargs["model"] == "gpt-5.6-sol" + assert kwargs["input_tokens"] == 10_000 # uncached remainder + assert kwargs["cached_input_tokens"] == 90_000 # cache-READ column + assert kwargs["output_tokens"] == 1_000 + + # 10k uncached @ $5 + 1k out @ $30 + 90k cached @ $0.50 (per Mtok). + expected = 10_000 / 1e6 * 5 + 1_000 / 1e6 * 30 + 90_000 / 1e6 * 0.5 + assert cost_cb.call_args.args[1] == pytest.approx(expected) + assert ss.usage.total_cost_usd == pytest.approx(expected) + # Accumulation saw the disjoint split too (context gauge correctness). + assert ss.usage.input_tokens == 10_000 + assert ss.usage.cache_read_tokens == 90_000 + + # ── gated integration smoke (the make-or-break; opt-in) ───────────────────── @pytest.mark.asyncio @pytest.mark.skipif( diff --git a/tests/test_pricing.py b/tests/test_pricing.py index 8efb087f..86c24d26 100644 --- a/tests/test_pricing.py +++ b/tests/test_pricing.py @@ -183,3 +183,56 @@ def test_cost_from_usage_tolerates_malformed() -> None: def test_zero_usage_is_zero_cost() -> None: assert compute_cost_from_usage("claude-opus-4-8", {}) == 0.0 + + +# ── OpenAI / Codex family (#860) ─────────────────────────────────────────── +# Powers the live tmux cost path for codex agents (CodexTmuxSession rides the +# same _log_turn_cost_and_analytics as Claude tmux). Official API rates: +# gpt-5.6-sol / gpt-5.5 at $5/$30 (cached input $0.50), gpt-5.3-codex at +# $1.75/$14 (cached $0.175). OpenAI bills no cache WRITE. + + +def test_gpt_frontier_rates() -> None: + # 1M input @ $5 + 1M output @ $30 + 1M cached-read @ $0.50 = $35.50 + for model in ("gpt-5.6-sol", "gpt-5.5"): + cost = compute_turn_cost_usd( + model, + input_tokens=1_000_000, + output_tokens=1_000_000, + cache_read_tokens=1_000_000, + cache_creation_5m_tokens=0, + cache_creation_1h_tokens=0, + ) + assert cost == pytest.approx(35.5), model + assert lookup_rate("gpt-5.6-sol") is lookup_rate("gpt-5.5") + + +def test_gpt_53_codex_rates() -> None: + # 1M input @ $1.75 + 1M output @ $14 + 1M cached-read @ $0.175 = $15.925 + cost = compute_turn_cost_usd( + "gpt-5.3-codex", + input_tokens=1_000_000, + output_tokens=1_000_000, + cache_read_tokens=1_000_000, + cache_creation_5m_tokens=0, + cache_creation_1h_tokens=0, + ) + assert cost == pytest.approx(15.925) + + +def test_gpt_cache_write_bills_zero() -> None: + """OpenAI has no cache-write charge — cache_creation tokens on a gpt + model must contribute $0 at both the 5m and 1h positions.""" + cost = compute_turn_cost_usd( + "gpt-5.6-sol", + input_tokens=0, + output_tokens=0, + cache_read_tokens=0, + cache_creation_5m_tokens=1_000_000, + cache_creation_1h_tokens=1_000_000, + ) + assert cost == 0.0 + # And through the usage-dict entry point (1h fallback path). + usage = {"input_tokens": 0, "output_tokens": 0, + "cache_creation_input_tokens": 1_000_000} + assert compute_cost_from_usage("gpt-5.6-sol", usage) == 0.0 diff --git a/tests/test_tmux_session.py b/tests/test_tmux_session.py index c014de58..acda94c8 100644 --- a/tests/test_tmux_session.py +++ b/tests/test_tmux_session.py @@ -5419,6 +5419,83 @@ async def test_cost_callback_failure_is_swallowed() -> None: assert ss.usage.total_turns == 1 +# ── #860 — transport-truthful provider attribution + usage normalization ── + + +def _make_usage_session_of( + cls, *, analytics=None, cost_cb=None, model: str = "claude-opus-4-8" +) -> TmuxSession: + """_make_usage_session for a TmuxSession SUBCLASS (transport variants).""" + cfg = StreamingSessionConfig( + agent_name="dymok", + working_dir="/tmp/tmux-session-test", + model=model, + ) + ss = cls( + cfg, + tmux_control=_make_mock_tmux(), + analytics_store=analytics, + cost_callback=cost_cb, + ) + ss._skip_wake_prompt_for_tests = True + ss._state_machine._state = SessionState.CONNECTED + return ss + + +@pytest.mark.asyncio +async def test_analytics_provider_comes_from_class_attr() -> None: + """#860: the provider on analytics rows must read _ANALYTICS_PROVIDER — + not a hardcoded literal — so transport subclasses (CodexTmuxSession) + attribute their turns truthfully. The hardcoded "anthropic" landed every + codex turn as anthropic/gpt-*, which no pricing row can match ($0).""" + + class _OtherTransport(TmuxSession): + _ANALYTICS_PROVIDER = "other_provider" + + analytics = MagicMock() + ss = _make_usage_session_of(_OtherTransport, analytics=analytics) + _seed_inflight(ss) + await ss._handle_turn_complete(_usage_turn_response()) + assert analytics.log_turn_usage.call_args.kwargs["provider"] == "other_provider" + + +def test_base_normalize_turn_usage_is_identity() -> None: + """The Claude transport already emits the daemon's disjoint convention, + so the base hook must pass the dict through untouched (same object).""" + u = {"input_tokens": 10_000, "cache_read_input_tokens": 2_000} + assert TmuxSession._normalize_turn_usage(u) is u + + +@pytest.mark.asyncio +async def test_normalize_hook_runs_before_all_consumers() -> None: + """#860: _normalize_turn_usage must rewrite response.usage ONCE, before + ANY consumer — accumulation (SessionUsage), pricing, and analytics all + see the normalized dict. A partial conversion spread across consumers is + how the cached split got silently dropped in the first place.""" + + class _NormalizingTransport(TmuxSession): + @staticmethod + def _normalize_turn_usage(u: dict) -> dict: + out = dict(u) + out["input_tokens"] = 7_777 + return out + + analytics = MagicMock() + cost_cb = MagicMock() + ss = _make_usage_session_of( + _NormalizingTransport, analytics=analytics, cost_cb=cost_cb + ) + _seed_inflight(ss) + await ss._handle_turn_complete(_usage_turn_response(input_tokens=999)) + # Analytics row: normalized, not the raw 999. + assert analytics.log_turn_usage.call_args.kwargs["input_tokens"] == 7_777 + # SessionUsage accumulation: normalized. + assert ss.usage.input_tokens == 7_777 + # Cost: priced off the normalized count (opus $5/Mtok input). + expected = 7_777 / 1e6 * 5 + 500 / 1e6 * 25 + 2_000 / 1e6 * 0.5 + assert cost_cb.call_args.args[1] == pytest.approx(expected) + + @pytest.mark.asyncio async def test_emits_context_usage_event_with_sdk_shape(monkeypatch) -> None: """``context_usage`` SSE event must carry the SDK-compatible fields From 6bbbd1513515ca84f9c0c9dc7db3005f5510f3cb Mon Sep 17 00:00:00 2001 From: olegbrok Date: Fri, 10 Jul 2026 15:01:28 -0700 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20#860=20before/after=20evidence=20?= =?UTF-8?q?=E2=80=94=20prod=20DB=20copy=20through=20the=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- specs/img/860-codex-cost-before-after.png | Bin 0 -> 75612 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 specs/img/860-codex-cost-before-after.png diff --git a/specs/img/860-codex-cost-before-after.png b/specs/img/860-codex-cost-before-after.png new file mode 100644 index 0000000000000000000000000000000000000000..f5b709874bb4e59a52186261cb9c743fc163ce80 GIT binary patch literal 75612 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 BRk#2E literal 0 HcmV?d00001 From b8057b737155522e68fdc058d04e92a3d6fb84c5 Mon Sep 17 00:00:00 2001 From: olegbrok Date: Fri, 10 Jul 2026 16:17:16 -0700 Subject: [PATCH 4/4] fix(pricing): gpt-5.6-sol cache writes billed at 1.25x input (murzik #861 P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sol model page (developers.openai.com, verified 2026-07-10) documents cache writes at 1.25x the uncached input rate ($6.25/Mtok); _GPT_FRONTIER hardcoded $0 for the whole family and the regression locked it. Split gpt-5.6-sol into its own rate dict with 6.25 in both write positions (OpenAI has no TTL split); gpt-5.5 and gpt-5.3-codex keep $0 — their pages list no write tariff. Regressions updated: sol write-billing pinned through both compute entry points; the shared-dict identity assertion dropped. Latent-only today (codex rollout usage carries no cache-creation fields), so the #860 migration evidence is unaffected. Co-Authored-By: Claude Fable 5 --- src/pinky_daemon/pricing.py | 25 +++++++++++++++------ tests/test_pricing.py | 43 ++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/pinky_daemon/pricing.py b/src/pinky_daemon/pricing.py index 13daedb6..8d85bd4e 100644 --- a/src/pinky_daemon/pricing.py +++ b/src/pinky_daemon/pricing.py @@ -97,11 +97,22 @@ # agents (CodexTmuxSession rides the same _log_turn_cost_and_analytics as # Claude tmux). Official OpenAI API rates (developers.openai.com, verified # 2026-07-10); codex agents run on sign-in subscription, so as with CC -# subscription agents these are notional usage-value figures (#648). OpenAI -# bills no cache WRITE — write rates are 0; cached reads use the published -# cached-input rate. Must mirror the analytics openai seeds -# (test_seed_pricing_matches_rate_table, #669). -_GPT_FRONTIER = { # gpt-5.5 / gpt-5.6-sol tier +# subscription agents these are notional usage-value figures (#648). +# Cache-write billing DIFFERS per model page: gpt-5.6-sol documents cache +# writes at 1.25x the uncached input rate ($6.25/Mtok); the gpt-5.5 and +# gpt-5.3-codex pages list no write tariff, so those bill $0. OpenAI has no +# 5m/1h TTL split — the documented tariff goes in both positions. Cached +# reads use the published cached-input rate. Must mirror the analytics +# openai seeds (test_seed_pricing_matches_rate_table, #669 — that guard +# pins input/output/cache_read; the seed table has no write column). +_GPT_56_SOL = { + "input": 5.00, + "output": 30.00, + "cache_read": 0.50, + "cache_write_5m": 6.25, # 1.25x input per the sol model page + "cache_write_1h": 6.25, +} +_GPT_55 = { "input": 5.00, "output": 30.00, "cache_read": 0.50, @@ -142,8 +153,8 @@ # Codex fleet models (#860). gpt-5.6-sol is the current fleet model # (2026-07-09→); gpt-5.5 the previous one; gpt-5.3-codex kept because the # analytics seed carries it (parity-pinned). - "gpt-5.6-sol": _GPT_FRONTIER, - "gpt-5.5": _GPT_FRONTIER, + "gpt-5.6-sol": _GPT_56_SOL, + "gpt-5.5": _GPT_55, "gpt-5.3-codex": _GPT_53_CODEX, } diff --git a/tests/test_pricing.py b/tests/test_pricing.py index 86c24d26..69866d43 100644 --- a/tests/test_pricing.py +++ b/tests/test_pricing.py @@ -187,9 +187,11 @@ def test_zero_usage_is_zero_cost() -> None: # ── OpenAI / Codex family (#860) ─────────────────────────────────────────── # Powers the live tmux cost path for codex agents (CodexTmuxSession rides the -# same _log_turn_cost_and_analytics as Claude tmux). Official API rates: -# gpt-5.6-sol / gpt-5.5 at $5/$30 (cached input $0.50), gpt-5.3-codex at -# $1.75/$14 (cached $0.175). OpenAI bills no cache WRITE. +# same _log_turn_cost_and_analytics as Claude tmux). Official API rates +# (developers.openai.com, verified 2026-07-10): gpt-5.6-sol / gpt-5.5 at +# $5/$30 (cached input $0.50), gpt-5.3-codex at $1.75/$14 (cached $0.175). +# Cache-write tariffs differ per model page: sol bills 1.25x input ($6.25); +# the gpt-5.5 and gpt-5.3-codex pages list no write tariff (murzik #861 P2). def test_gpt_frontier_rates() -> None: @@ -204,7 +206,6 @@ def test_gpt_frontier_rates() -> None: cache_creation_1h_tokens=0, ) assert cost == pytest.approx(35.5), model - assert lookup_rate("gpt-5.6-sol") is lookup_rate("gpt-5.5") def test_gpt_53_codex_rates() -> None: @@ -220,9 +221,13 @@ def test_gpt_53_codex_rates() -> None: assert cost == pytest.approx(15.925) -def test_gpt_cache_write_bills_zero() -> None: - """OpenAI has no cache-write charge — cache_creation tokens on a gpt - model must contribute $0 at both the 5m and 1h positions.""" +def test_gpt_56_sol_cache_write_billed_at_1_25x_input() -> None: + """The sol model page documents cache writes at 1.25x the uncached input + rate ($6.25/Mtok). OpenAI has no 5m/1h TTL split, so both positions carry + the same tariff (murzik #861 P2 — was wrongly $0).""" + rate = lookup_rate("gpt-5.6-sol") + assert rate["cache_write_5m"] == 6.25 + assert rate["cache_write_1h"] == 6.25 cost = compute_turn_cost_usd( "gpt-5.6-sol", input_tokens=0, @@ -231,8 +236,26 @@ def test_gpt_cache_write_bills_zero() -> None: cache_creation_5m_tokens=1_000_000, cache_creation_1h_tokens=1_000_000, ) - assert cost == 0.0 - # And through the usage-dict entry point (1h fallback path). + assert cost == pytest.approx(12.5) + # Usage-dict entry point (aggregate falls back to the 1h position). usage = {"input_tokens": 0, "output_tokens": 0, "cache_creation_input_tokens": 1_000_000} - assert compute_cost_from_usage("gpt-5.6-sol", usage) == 0.0 + assert compute_cost_from_usage("gpt-5.6-sol", usage) == pytest.approx(6.25) + + +def test_gpt_55_and_codex_cache_write_bills_zero() -> None: + """The gpt-5.5 and gpt-5.3-codex model pages list no cache-write tariff — + cache_creation tokens contribute $0 at both positions.""" + for model in ("gpt-5.5", "gpt-5.3-codex"): + cost = compute_turn_cost_usd( + model, + input_tokens=0, + output_tokens=0, + cache_read_tokens=0, + cache_creation_5m_tokens=1_000_000, + cache_creation_1h_tokens=1_000_000, + ) + assert cost == 0.0, model + usage = {"input_tokens": 0, "output_tokens": 0, + "cache_creation_input_tokens": 1_000_000} + assert compute_cost_from_usage(model, usage) == 0.0, model