-
Notifications
You must be signed in to change notification settings - Fork 0
Account for additive cache counts in the total_tokens guard #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -254,10 +254,36 @@ def extract_openai_native(response: Any, model_id: str = "", provider_hint: str | |
| # breakdown at all (measured: prompt 57, completion 47, total 1253) — still | ||
| # recovers its 1,149 tokens, because reasoning is 0 there. | ||
| # | ||
| # The cache counts are subtracted for the SAME reason as reasoning, and this was | ||
| # the half that was missing. The guard assumed every OpenAI-shaped surface reports | ||
| # cache_read INSIDE prompt_tokens, which held for all three surfaces that existed | ||
| # when it was written (native OpenAI: zero deltas; Databricks: 112/112 rows with | ||
| # total == input + output; Cloudflare: cache outside `input` but outside `total` | ||
| # too, so it never inflated the delta). Snowflake Cortex is the surface that broke | ||
| # it — an OpenAI-WIRE endpoint with Anthropic's ADDITIVE convention: measured | ||
| # 2026-08-25, prompt_tokens=7, cached_tokens=4805, completion_tokens=6, | ||
| # total_tokens=4818, i.e. the cached block sits OUTSIDE prompt_tokens and INSIDE | ||
| # total_tokens. Accounting for only input+output+reasoning made those 4,805 cached | ||
| # tokens look unaccounted, so they were folded into `output` — 4,811 reported for a | ||
| # call that generated 6, while the same tokens also shipped as cache_read. 2.0x on | ||
| # the call, 800x on the output line. See 12_snowflake_cortex_cache_chat.json. | ||
| # | ||
| # `cache_write_tokens` is read straight from the payload rather than from 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 — see _MAPPED_DETAIL_FIELDS). It still has to be accounted for here, or an | ||
| # additive cache WRITE would inflate `output` exactly the way the read did. | ||
| # | ||
| # Subtracting them cannot suppress a genuine fold on a subtractive surface: there | ||
| # the cache counts are already inside `input`, so removing them again only drives | ||
| # the delta further negative, where the `> 0` guard already no-ops. Verified against | ||
| # every captured OpenAI, Databricks and Cloudflare fixture — all still 0. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On this line specifically: I don't think the Databricks and Cloudflare halves of that check can have run. That leaves the 10 OpenAI fixtures, none of which pairs a cache count with a positive delta — the one combination I'm asking about above. Worth narrowing the claim to what was actually exercised? The CHANGELOG entry repeats it too. |
||
| # | ||
| # A no-op for real OpenAI either way: total always equals prompt + completion. | ||
| declared_total = _safe_int(usage.get("total_tokens")) | ||
| if declared_total: | ||
| unaccounted = declared_total - (input_tokens + output_tokens + reasoning) | ||
| cache_write = _safe_int(_safe_dict(usage.get("prompt_tokens_details")).get("cache_write_tokens")) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one I think may point the wrong way. The NOTE at lines 79-87 measures the OpenAI case as Taking that documented shape and putting it behind a proxy that under-reports — The additive cache-write case the new test at line 353 pins is hand-written — fixture 12 has There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this pick the details container based on which branch ran? On the Responses path the container is
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small thing, related to the above: the chat branch already builds this same dict as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How confident are we that The drift sweep at lines 223-226 already walks every unmapped nested key — could the accounting be derived from that rather than from a name match, so a new spelling is covered on arrival? If a name list is the pragmatic call for now, maybe worth a line in the comment saying that's a known limit. |
||
| unaccounted = declared_total - (input_tokens + output_tokens + reasoning + cache_read + cache_write) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to make sure I've followed the reasoning in the comment above about why this can't suppress a genuine fold. The argument holds when the cache count is the only thing making the delta positive — but what about a subtractive surface carrying a large cached block and a real remainder? Concretely, Gemini through Google's own OpenAI-compat layer — the surface the comment at lines 234-239 says this guard was written for, and which So the subtraction wants to be conditional rather than unconditional. I tried two ways of deciding it from the payload and neither is sound, which seems worth writing down before someone reaches for one:
The payload on its own doesn't carry the convention — the only thing here that knows it is the table this repo already has. Would keying the subtraction off Entirely possible there's a reason that shape can't reach here — is there one? |
||
| if unaccounted > 0: | ||
| output_tokens += unaccounted | ||
| extras["unaccounted_output_tokens"] = unaccounted | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| """Capture Snowflake Cortex responses off the OpenAI-compatible endpoint. | ||
|
|
||
| Saves to: | ||
| tests/unit/adapters/fixtures/openai_native/11_snowflake_cortex_plain_chat.json | ||
| tests/unit/adapters/fixtures/openai_native/12_snowflake_cortex_cache_chat.json | ||
|
|
||
| These live under `openai_native/` on purpose: Cortex answers on an OpenAI-wire | ||
| endpoint, so `extract_openai_native` / `extractOpenAINative` is the adapter that | ||
| serves them. They are the surface that proves the `total_tokens` reconciliation | ||
| cannot assume OpenAI's subtractive cache convention — on Cortex, `cached_tokens` | ||
| sits OUTSIDE `prompt_tokens` and INSIDE `total_tokens`. | ||
|
|
||
| Two things about Cortex that this script encodes, both measured 2026-08-25: | ||
|
|
||
| * Caching only happens with an explicit Anthropic-style `cache_control` part. | ||
| The same 4,800-token prompt sent twice WITHOUT it reports `cached_tokens: 0` | ||
| both times, so the "call1 then call2" pattern the OpenAI cache fixtures use | ||
| captures nothing here. | ||
| * `max_tokens` is rejected outright ("deprecated in favor of | ||
| max_completion_tokens"), unlike OpenAI which still accepts it. | ||
|
|
||
| Requires a Snowflake account with the Cortex REST endpoint entitled — it returns | ||
| 403 `003001` otherwise, which is an account-level grant no config can work around. | ||
|
|
||
| SNOWFLAKE_HOST=<org>-<account>.snowflakecomputing.com \ | ||
| SNOWFLAKE_PAT=<programmatic access token> \ | ||
| python3 capture_snowflake_cortex.py | ||
|
|
||
| Idempotent: skips files that already exist. Re-run after deleting one to refresh it. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import os | ||
| import pathlib | ||
| import sys | ||
|
|
||
| import requests | ||
|
|
||
| MODEL = "claude-sonnet-4-5" | ||
| OUT = pathlib.Path(__file__).parent / "openai_native" | ||
|
|
||
| # Long enough to clear Anthropic's minimum cacheable prefix. Deliberately dull, | ||
| # fixed text: the fixture must not carry anything account- or person-identifying. | ||
| CACHEABLE_PREFIX = "Reference notes on widget calibration tolerances, revision seven. " * 400 | ||
|
|
||
|
|
||
| def call(host: str, pat: str, body: dict) -> dict: | ||
| r = requests.post( | ||
| f"https://{host}/api/v2/cortex/v1/chat/completions", | ||
| headers={ | ||
| "Authorization": f"Bearer {pat}", | ||
| "X-Snowflake-Authorization-Token-Type": "PROGRAMMATIC_ACCESS_TOKEN", | ||
| "Content-Type": "application/json", | ||
| "Accept": "application/json", | ||
| }, | ||
| json=body, | ||
| timeout=120, | ||
| ) | ||
| if r.status_code != 200: | ||
| sys.exit(f"Cortex returned {r.status_code}: {r.text[:300]}") | ||
| return r.json() | ||
|
|
||
|
|
||
| def save(name: str, response: dict) -> None: | ||
| path = OUT / name | ||
| if path.exists(): | ||
| print(f"skip {name} (exists)") | ||
| return | ||
| path.write_text(json.dumps({"_model_id": MODEL, "_response": response}, indent=2) + "\n") | ||
| print(f"wrote {name}") | ||
|
|
||
|
|
||
| def main() -> None: | ||
| host = os.environ.get("SNOWFLAKE_HOST") | ||
| pat = os.environ.get("SNOWFLAKE_PAT") | ||
| if not host or not pat: | ||
| sys.exit("set SNOWFLAKE_HOST and SNOWFLAKE_PAT") | ||
|
|
||
| save( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the existence check doesn't get a chance to run here: The existing |
||
| "11_snowflake_cortex_plain_chat.json", | ||
| call( | ||
| host, | ||
| pat, | ||
| { | ||
| "model": MODEL, | ||
| "messages": [{"role": "user", "content": "What is 2 + 2? Answer in one word."}], | ||
| "max_completion_tokens": 32, | ||
| }, | ||
| ), | ||
| ) | ||
|
|
||
| # The regression fixture. `cache_control` is what makes Cortex report a cached | ||
| # block at all, and the resulting payload is the one that used to inflate | ||
| # `output` by the whole cached count. | ||
| save( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does a single call against a cold cache actually produce If that's the case the test would fail on |
||
| "12_snowflake_cortex_cache_chat.json", | ||
| call( | ||
| host, | ||
| pat, | ||
| { | ||
| "model": MODEL, | ||
| "messages": [ | ||
| { | ||
| "role": "user", | ||
| "content": [ | ||
| { | ||
| "type": "text", | ||
| "text": CACHEABLE_PREFIX, | ||
| "cache_control": {"type": "ephemeral"}, | ||
| }, | ||
| {"type": "text", "text": "Reply with one word."}, | ||
| ], | ||
| } | ||
| ], | ||
| "max_completion_tokens": 32, | ||
| }, | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| { | ||
| "_model_id": "claude-sonnet-4-5", | ||
| "_response": { | ||
| "choices": [ | ||
| { | ||
| "finish_reason": "", | ||
| "index": 0, | ||
| "message": { | ||
| "annotations": null, | ||
| "audio": { | ||
| "data": "", | ||
| "expires_at": 0, | ||
| "id": "", | ||
| "transcript": "" | ||
| }, | ||
| "content": "Four", | ||
| "function_call": { | ||
| "arguments": "", | ||
| "name": "" | ||
| }, | ||
| "refusal": "", | ||
| "role": "assistant", | ||
| "tool_calls": null | ||
| } | ||
| } | ||
| ], | ||
| "created": 1787679484, | ||
| "id": "", | ||
| "model": "claude-sonnet-4-5", | ||
| "object": "chat.completion", | ||
| "service_tier": "", | ||
| "system_fingerprint": "", | ||
| "usage": { | ||
| "completion_tokens": 4, | ||
| "completion_tokens_details": { | ||
| "accepted_prediction_tokens": 0, | ||
| "audio_tokens": 0, | ||
| "reasoning_tokens": 0, | ||
| "rejected_prediction_tokens": 0 | ||
| }, | ||
| "prompt_tokens": 21, | ||
| "prompt_tokens_details": { | ||
| "audio_tokens": 0, | ||
| "cache_write_tokens": 0, | ||
| "cached_tokens": 0 | ||
| }, | ||
| "total_tokens": 25 | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| { | ||
| "_model_id": "claude-sonnet-4-5", | ||
| "_response": { | ||
| "choices": [ | ||
| { | ||
| "finish_reason": "", | ||
| "index": 0, | ||
| "message": { | ||
| "annotations": null, | ||
| "audio": { | ||
| "data": "", | ||
| "expires_at": 0, | ||
| "id": "", | ||
| "transcript": "" | ||
| }, | ||
| "content": "Acknowledged.", | ||
| "function_call": { | ||
| "arguments": "", | ||
| "name": "" | ||
| }, | ||
| "refusal": "", | ||
| "role": "assistant", | ||
| "tool_calls": null | ||
| } | ||
| } | ||
| ], | ||
| "created": 1787679488, | ||
| "id": "", | ||
| "model": "claude-sonnet-4-5", | ||
| "object": "chat.completion", | ||
| "service_tier": "", | ||
| "system_fingerprint": "", | ||
| "usage": { | ||
| "completion_tokens": 6, | ||
| "completion_tokens_details": { | ||
| "accepted_prediction_tokens": 0, | ||
| "audio_tokens": 0, | ||
| "reasoning_tokens": 0, | ||
| "rejected_prediction_tokens": 0 | ||
| }, | ||
| "prompt_tokens": 7, | ||
| "prompt_tokens_details": { | ||
| "audio_tokens": 0, | ||
| "cache_write_tokens": 0, | ||
| "cached_tokens": 4805 | ||
| }, | ||
| "total_tokens": 4818 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this PR effectively blesses
snowflakeas a provider through this adapter, should it also land in_OUTPUT_INCLUDES_REASONINGinpricing.py? Cortex serves Anthropic models, whose thinking tokens sit insideoutput_tokensand are re-reported on the wire ascompletion_tokens_details.reasoning_tokens— both new fixtures carry the field, at 0.The table is
frozenset({"openai", "workers-ai"})atpricing.py:111, plus_OPENAI_SHAPED_APIS = {"databricks_gateway"}at 137, andprovider="snowflake", api="chat_completions"matches neither — so_token_semanticsreturnsoutput_includes_reasoning=Falseand a call withcompletion_tokens=1000, reasoning_tokens=800would bill 1,800 output-rate tokens for 1,000 generated.Same class of convention-table gap this PR is closing, one field over — fold it in here, or leave it for a follow-up once someone can capture a thinking-enabled Cortex call?