Account for additive cache counts in the total_tokens guard - #23
Open
anassg-lago wants to merge 1 commit into
Open
Account for additive cache counts in the total_tokens guard#23anassg-lago wants to merge 1 commit into
anassg-lago wants to merge 1 commit into
Conversation
An OpenAI-compatible wire says nothing about the token CONVENTION behind it, and
the consistency guard assumed it did.
The guard folds any positive `total_tokens - (input + output + reasoning)`
remainder into `output`, on the premise that cache counts always sit INSIDE
`prompt_tokens` and so can never appear in that remainder. That held for all
three surfaces this adapter served when it was written: native OpenAI (zero
deltas across every capture), Databricks (112 of 112 rows with
total == input + output), and Cloudflare (cache outside `input`, but outside
`total_tokens` too, so it never inflated the delta).
Snowflake Cortex breaks it. Cortex answers on an OpenAI wire —
/api/v2/cortex/v1/chat/completions, byte-for-byte the OpenAI payload shape —
with Anthropic's ADDITIVE convention. Measured live 2026-08-25:
prompt_tokens 7
prompt_tokens_details.cached_tokens 4805
completion_tokens 6
total_tokens 4818 = 7 + 4805 + 6
The cached block sits outside `prompt_tokens` and inside `total_tokens`, so
4,805 tokens looked unaccounted and were added to `output`: 4,811 reported for a
call that generated 6, while the same 4,805 also shipped as cache_read. On the
first capture that was 17,503 tokens billed for 8,758 consumed (2.0x), with the
output line inflated ~800x. Not Snowflake-specific — any OpenAI-compatible proxy
with additive caching hits it.
The reconciliation now subtracts cache_read and the raw
prompt_tokens_details.cache_write_tokens as well.
cache_write_tokens is read straight off the payload rather than through a
canonical field because it is deliberately NOT mapped to
CanonicalUsage.cache_write — for OpenAI it sits inside prompt_tokens and billing
it separately over-charges 2.24x — so it has no other route into the arithmetic,
and an additive cache WRITE would inflate `output` exactly the way the read did.
Subtracting cannot suppress a genuine fold: on a subtractive surface those counts
are already inside `input`, so removing them again only drives the delta further
negative, where the `> 0` guard already no-ops. Re-verified across every captured
OpenAI, Databricks and Cloudflare fixture — all still 0 — and a payload carrying
both an additive cache block and hidden thinking tokens still folds the thinking
remainder alone.
Deliberately NOT gated on the details objects being empty, which was the other
candidate fix: that suppresses genuine folds on any proxy that reports a details
block alongside unreported tokens, reintroducing the silent under-bill the guard
exists for.
This hazard was raised in review on #14 (2026-08-17) with the exact payload
shape, and answered with a three-surface census returning unaccounted = 0
everywhere. That census was correct; it expired when a surface with additive
caching arrived. It is now pinned by live fixtures rather than by an argument:
11_snowflake_cortex_plain_chat.json and 12_snowflake_cortex_cache_chat.json,
captured by capture_snowflake_cortex.py and byte-identical to the JS port's
copies. Reverting the subtraction fails three tests in each repo.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
An OpenAI-compatible wire says nothing about the token convention behind it. The
total_tokensconsistency guard assumed it did, and folds any positivetotal_tokens - (input + output + reasoning)remainder intooutput.That premise held for all three surfaces this adapter served when it was written — native OpenAI (zero deltas across every capture), Databricks (112/112 rows with
total == input + output), Cloudflare (cache outsideinput, but outsidetotal_tokenstoo, so it never inflated the delta).Snowflake Cortex breaks it. Cortex answers on an OpenAI wire —
/api/v2/cortex/v1/chat/completions, byte-for-byte the OpenAI payload shape — with Anthropic's additive convention. Measured live against the real endpoint, 2026-08-25:The cached block sits outside
prompt_tokensand insidetotal_tokens, so those 4,805 tokens looked unaccounted and were folded intooutput. Run through the real adapter:inputoutputcache_readThe same 4,805 tokens billed twice, the second time at the output rate. On the first capture that was 17,503 tokens for 8,758 consumed — 2.0x on the call, ~800x on the output line. This is live on
maintoday for anyone pointing the OpenAI client at Cortex, and it is not Snowflake-specific: any OpenAI-compatible proxy with additive caching hits it.What changed
extract_openai_nativenow subtractscache_readand the rawprompt_tokens_details.cache_write_tokensalongsidereasoning.cache_write_tokensis accounted for without being mapped. It is deliberately absent fromCanonicalUsage.cache_write— for OpenAI it sits insideprompt_tokensand billing it separately over-charges 2.24x — so it has no canonical route into the arithmetic and is read straight off the payload. Without that, an additive cache write inflatesoutputthe same way the read did.input, so removing them again only drives the delta further negative, where the> 0guard already no-ops. Re-verified across every captured OpenAI, Databricks and Cloudflare fixture — all still 0.Tests
Three new assertions, each verified to fail on revert:
output == 6, not 4811cache_writepayload from the review thread below, assertingoutput == 4Fixtures are captured from the live endpoint by a new
capture_snowflake_cortex.py, byte-identical to the JS port's copies (sha256 verified). They sit underopenai_native/because that is the adapter Cortex goes through. The script documents the two Cortex quirks that make them un-hand-writable: caching needs an explicitcache_controlpart, andmax_tokensis rejected formax_completion_tokens.Green locally:
ruff check,ruff format --check,mypy src(strict), 606 unit tests, coverage 91.9% against the 80% floor.Provenance
@ancorcruz raised this class of hazard on #14 (2026-08-17), with the payload shape almost exactly right — he named
cache_write_tokens; Cortex does it withcached_tokens. It was answered with a three-surface census returningunaccounted = 0everywhere and closed as unreachable. That census was correct — it expired when a surface with additive caching arrived. His suggested gate would have caught this too; I went the other way for the false-negative reason above. The census is now pinned by a live payload instead of an argument.Rebase notes
Shared symbols touched, for the connector branches rebasing after this:
src/lago_agent_sdk/adapters/openai_native.py— thedeclared_totalreconciliation block only. No signature change;extract_openai_native(response, model_id=…, provider_hint=…)is untouched.CanonicalUsage,_MAPPED_DETAIL_FIELDS, the drift contract,pricing.py, or any wrapper.tests/unit/adapters/fixtures/— no renumbering of existing fixtures.Twin PR: getlago/lago-agent-sdk-js#38