Skip to content

feat(lean): keep Haiku coding sessions cached and under the 5x long-prompt price - #97

Merged
ALikesToCode merged 5 commits into
mainfrom
feat/lean-endpoint
Oct 26, 2026
Merged

ALikesToCode merged 5 commits into
mainfrom
feat/lean-endpoint

Conversation

@ALikesToCode

@ALikesToCode ALikesToCode commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Haiku 5.5 is cheap ($0.10 / $0.50 per million tokens) only while a request's prompt stays at or under 100,000 tokens. One token over, the whole request costs 5x, cached tokens included. Coding harnesses grow their prompts long, and Chat Completions and Responses traffic reaching the Anthropic API today gets no prompt caching at all, because the gateway never adds cache markers and translation drops them.

This adds a /lean base path. Point a harness at /lean/v1/messages, /lean/v1/chat/completions or /lean/v1/responses and, for anthropic: models, the upstream request gets:

  • Prompt caching: an automatic top-level breakpoint plus one after the system prompt, unless the caller placed its own.
  • Server-side clearing of old tool results (clear_tool_uses_20250919) from 80K tokens. The harness keeps its full history, and Anthropic documents that this never invalidates thinking blocks.
  • Shell output condensing: ANSI escapes, progress redraws, repeated lines and blank runs. File reads keep their exact bytes so edit tools still match. Oversized results keep their head and tail.
  • A price guard: near the limit the prompt is counted with count_tokens using the key that will send it. Context editing is included and server tools are left out with an allowance. Prompts that can't be counted are judged by a content-aware estimate. Above 99K (a 1K margin, since the count is an estimate) the request is refused with the harness's own overflow error: Anthropic's prompt is too long, OpenAI's context_length_exceeded, and a streamed response.failed for Codex. The harness compacts with its own summarizer instead of paying 5x.
    Anthropic doesn't document whether a prompt shortened by clearing is priced by its size before or after clearing. So by default the limit applies to the size before; LEAN_PRICE_LIMIT_BASIS=cleared switches it once the bill confirms.
  • An opt-in concise-answer instruction (LEAN_STYLE=concise or X-MultiLLM-Lean-Style). Off by default: published trials found small output savings, with no measured quality loss.

Model, effort, thinking and max_tokens are never changed. Every transform depends only on the request, so turn N+1 repeats turn N's bytes. That matters on these accounts: they reject a replayed thinking block whose earlier conversation changed.

Checked against the live Anthropic API (local app, a few Haiku calls):

Check Result
Caching Request 1 wrote a 2,006-token prefix and request 2 read all of it, with 4 uncached input tokens
Clearing applied_edits reported 4 tool uses / 3,839 tokens cleared
Exact count and refusal Real count_tokens returned 5,042 tokens against a 5,000 test limit; Messages and Chat Completions refusals came back in the right shapes
Thinking replay A block written through /lean was accepted when turn 2 replayed it through /lean, and rejected ("bound to a different conversation") when the same turn went to /v1 with raw shell output. So a conversation must stay on one base path, as docs/lean.md says

Design inputs: Anthropic's context-editing, compaction, caching, token-counting and preserved-thinking docs, and the compaction code of Codex (90% trigger, context_length_exceeded handling), opencode, Cline, Roo, Aider and Gemini CLI. The research ruled out proxy-side summarizing and age-based pruning: both edit history behind the harness, which breaks caching and thinking replay.

A second model reviewed the code. It found that documents, failed counts and pooled keys could slip past the guard, and that a schema property named cache_control disabled caching; all four are fixed with regression tests. Tests: tests/test_lean_mode.py covers the transforms, the cross-turn byte-stability invariant, caller-owned cache and context settings, the count body, and the three refusal shapes. The related Python suites (445 tests) and the Worker tests (79) pass.

…r the long-prompt price

/lean/v1/messages, /lean/v1/chat/completions and /lean/v1/responses serve
the unified routes. For Anthropic API models the upstream body gains
automatic prompt caching, server-side clearing of old tool results,
deterministic shell-output condensing and head-and-tail clipping of
oversized results. A prompt counted above 100,000 tokens is refused with
each protocol's own context-overflow error, so the harness compacts
instead of paying the long-prompt price. Every change depends only on the
request, so later turns repeat earlier bytes and replayed thinking blocks
stay valid.
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 675bb3f6-3271-4629-8d82-35ac6eec3843
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-26T16:28:45.808256Z 58c58d6 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…ault

Anthropic's pricing page does not say whether a prompt shortened by
server-side context editing is priced by its size before or after
clearing. count_tokens reports both, so the guard now refuses on the
larger size unless LEAN_PRICE_LIMIT_BASIS=cleared, and reports the size
after clearing in its own header. The docs note that a harness stops
after repeated failed compactions when one message alone is too large.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 288bd62e80

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread services/lean_mode.py Outdated
Comment on lines +381 to +383
if value.get("type") in {"image", "document"}:
media += 1
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count document contents before bypassing the exact token count

For Messages requests containing a large document, this early return ignores all document contents and assigns only 1,600 tokens regardless of size. For example, a text-source document containing hello world repeated 100,000 times produces an estimate of 1,601 tokens and never calls count_claude_prompt_tokens, even though its actual prompt exceeds the 100,000-token price threshold. Large PDFs have the same unbounded underestimate. Count text/content sources recursively and require an exact count for documents whose token size cannot be bounded locally, so these requests cannot silently bypass the price guard.

Useful? React with 👍 / 👎.

Comment thread middleware/lean_path.py
Comment on lines +41 to +42
environ["PATH_INFO"] = path[len(LEAN_PREFIX):]
environ[LEAN_ENVIRON_KEY] = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Isolate lean requests in the response-cache identity

When a caller opts into response caching with X-MultiLLM-Cache: on and sends a cacheable request such as temperature: 0, rewriting the path makes /lean/v1/chat/completions share its cache identity with /v1/chat/completions. routes/chat_cache.py keys entries using request.path and the original payload, without the lean marker or style header. Consequently, an answer generated with lean's concise system instruction can be returned to an ordinary /v1 request, or an ordinary cached answer can bypass the requested concise instruction entirely. Preserve lean mode and the effective style in the response-cache identity, or bypass that cache for lean requests.

Useful? React with 👍 / 👎.

Comment thread services/native_token_count.py Outdated
Comment on lines +146 to +147
count_body = {key: value for key, value in body.items() if key in CLAUDE_COUNT_FIELDS}
return _post_count("anthropic", url, CLAUDE_COUNT_PATH, credential, incoming, count_body)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply the secret firewall before sending the count request

For a lean prompt large enough to trigger exact counting, secrets are sent to Anthropic before the configured secret policy runs. _post_count submits this body directly through provider_http, which enforces credentials and data policy but does not apply the secret firewall; generation requests only reach protect_body later in ProxyService._make_base_request. With the default redaction policy, an embedded API key therefore leaves the gateway unredacted, and with secret_scan_mode: block, it is transmitted before generation is refused. Apply the secret policy before this outbound count and reuse the protected body for generation so both requests obey the same policy.

Useful? React with 👍 / 👎.

Comment thread services/lean_mode.py Outdated
def walk(value: Any) -> None:
nonlocal characters, media
if isinstance(value, dict):
if value.get("type") in {"image", "document"}:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle non-string type values when walking nested JSON

For valid tool schemas containing a union such as "type": ["string", "null"], this set-membership check raises TypeError: unhashable type: 'list'. The estimator recursively visits every dictionary in tool schemas and tool inputs, so an ordinary tool argument named type with an array or object value also triggers the failure. Because estimation runs on every lean Anthropic request, these requests fail before generation even when the prompt is tiny. Check that the discriminator is a string before testing membership, while continuing to traverse arbitrary nested JSON.

Useful? React with 👍 / 👎.

…ompts and pooled keys

- Text documents and base64 PDFs are estimated by their contents instead
  of a flat size, so a large document is always counted.
- URL and file sources, which count_tokens rejects, are judged by the
  estimate, and so is any prompt whose count fails: an estimate above the
  limit is refused rather than sent.
- Server tools are left out of the count and added back at a fixed size,
  so requests with web search can still be counted.
- The count uses the key that will send the request. Thinking blocks from
  another account are dropped before counting, so any other key could
  undercount.
- Only block-level cache_control counts as a caller breakpoint; a tool
  schema property of that name no longer turns caching off.
- The default limit is 99,000 tokens, since count_tokens is an estimate.
Bandit read the shell= keyword of the condensing helpers as a subprocess
call; the parameter is now terminal_output. Narrow the estimate's source
and tools lookups for mypy.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0a3397f49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread services/lean_mode.py
Comment on lines +436 to +440
elif isinstance(value, list):
for item in value:
walk(item)
elif isinstance(value, str):
characters += len(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include numeric tool inputs in the prompt estimate

For lean requests containing large numeric arrays in historical tool_use.input blocks, the estimator ignores every numeric element because the walker only accounts for strings. A Messages body with 120,000 instances of 123456789 in a tool input is approximately 1.3 MB but produces an estimate of just 34 tokens. This bypasses exact counting entirely, allowing a prompt well above the price limit through the guard. Account for numeric primitives and JSON structure, or require provider counting when their size cannot be bounded by the local estimate.

Useful? React with 👍 / 👎.

Comment thread services/lean_mode.py
count_body, allowance = _count_body(decision.body)
beta_header = merged_beta_value(_header(request_headers, "anthropic-beta"), decision.betas)
try:
after, before = count_claude_prompt_tokens(count_body, beta_header, base_urls, credential=credential)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the generation deadline to the token-count request

For a near-limit lean request with X-MultiLLM-Deadline-Ms, this synchronous count can continue well beyond the caller's generation budget. The count transport in services/native_token_count.py uses fixed (5, 10) timeouts and never registers its response with the deadline's cancellation owner; provider_http does not enforce deadlines either. Consequently, a request with a 100 ms deadline can remain blocked on a stalled count for roughly 10 seconds before reaching the deadline check in _send_native_request. Bound the count transport by the remaining generation budget and attach its response to deadline cancellation so counting cannot hold the request open after expiry.

Useful? React with 👍 / 👎.

…le refused keys

count_tokens runs the same thinking-block prefix check as generation, so
a body that replays thinking blocks is now counted with its tools
unchanged instead of without its server tools. The advisor tool, which
count_tokens accepts, is no longer stripped. A count that refuses the key
(401, 402, 403) no longer decides the request: it goes out so rotation
can rest that key and count again with the next. Prompts with a PDF are
always counted, since file size does not bound the text inside. The docs
say plainly when the limit is an estimate rather than a guarantee.
@ALikesToCode
ALikesToCode merged commit 58c58d6 into main Oct 26, 2026
6 checks passed
@ALikesToCode
ALikesToCode deleted the feat/lean-endpoint branch October 27, 2026 18:52

This branch had an error being deployed

1 failed deployment
production — 58c58d6a Deployed Oct 26, 2026 by ALikesToCode via Deploy to Cloudflare #54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant