perf(proxy): bound upstream calls and hot-path costs (#2852)

Seven commits from one week of load testing: one hang, two request-path
correctness fixes, and four hot-path costs that only show up in
production.

## Reliability

**Bound every upstream call.** The litellm backend had no timeout at
all, so a
request the upstream never answered blocked its caller forever. Observed
under
load on 2026-08-07: four agent workers on ESTABLISHED connections for
36+
minutes while `/readyz` answered in 0.11s. No error, no retry, no log
line —
indistinguishable from slow work, which is the worst shape a failure can
take.

A float rather than an `httpx.Timeout`, deliberately: litellm expands a
float
across all four httpx phases, so on a streaming call it becomes the
maximum gap
*between chunks*, not a cap on total generation. A long answer streaming
steadily is never cut off; a stalled one dies. Default 600s via
`HEADROOM_UPSTREAM_TIMEOUT`; 0, negative, and junk fall back to the
default
rather than meaning "no timeout".

**Keep the consistency re-count off the event loop.** It ran
`tokenizer.count_messages` twice directly on the loop. Since Claude
counting
moved to a real BPE that is CPU-bound work stalling every other
in-flight
request — ~1s on a 2.3 MB body, with `/healthz` gaps tracking body size.
Offloaded via `asyncio.to_thread` on the same tokenizer instance, so
reported
values are unchanged. (#2810)

**Survive a re-parse MemoryError.** `MemoryError` is not a `ValueError`,
so on
1M-context payloads the byte-faithful forwarder's verification re-parse
escaped
the handler and aborted an otherwise-fine request — 14 aborts across 8
days of
reporter logs. (#2768)

## Performance

All four are measured, not guessed. Each degrades with something a short
benchmark does not vary: uptime, content shape, or process age.

| fix | before | after |
|---|---|---|
| Cost-record walk per request (at 100k records) | 13.6 ms | bounded by
model count |
| JSON-block scan, JS-style object logs (1200 lines) | 4643 ms | 183 ms
|
| JSON-block scan, truncated JSONL | 3737 ms | 116 ms |
| Lazy imports inside user requests | multi-second | paid at startup |
| `count_text` (80% of local CPU) | — | memoised |

Two worth calling out:

- **The cost walk degrades with proxy *uptime*, not load.** A freshly
started
proxy pays ~0.01 ms; a month-old one pays 4–13 ms on every request, on
the
event loop, holding the metrics lock. Deliberately not a TTL cache over
`stats()`: those values feed `check_budget()` when `--budget` is set,
and a
stale reading under-enforces the budget. The fix is to stop computing
what
  the caller discards.
- **The JSON-block memo is built only *after* a scan fails to balance.**
That
ordering is load-bearing, not an optimisation — caching from the start
made
pretty-printed JSON ~2x slower, since content that balances on the first
scan
  has nothing to reuse and just pays the per-line dict traffic. Still a
  constant-factor fix, not an asymptotic one.

## Tests

+1202 lines, 20 files. Each fix is pinned by a test that fails on the
unmodified code: the re-count test asserts no `count_messages` pass runs
with a
live event loop in its thread; the re-parse test drives a `MemoryError`
through
the real request path and expects a 200; `totals()` equality with
`stats()` is
asserted across model counts, request volumes, and both pricing
branches. The
timeout test is structural rather than a mock — the failure mode is a
dispatch
path someone adds later without a guard, which mocking the existing four
cannot
catch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tejas Chopra
2026-08-09 16:24:33 -07:00
committed by GitHub
parent e0870ef931
commit f624d3a00a
9 changed files with 845 additions and 9 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

+378
View File
@@ -0,0 +1,378 @@
# context-mode → Headroom: enterprise plugin & variant analysis
Analysis date: 2026-07-29. Sources: `/Users/tcms/demo/context-mode` @ v1.0.169, `/Users/tcms/demo/headroom` @ main.
---
## 1. Bottom line
context-mode and Headroom attack the same cost problem at **two different layers**, and they do not
overlap where it matters:
| | context-mode | Headroom |
|---|---|---|
| Interception point | agent **tool-call boundary** (host hooks + MCP) | model **API boundary** (proxy / SDK / MCP) |
| Position relative to context | **pre-context** — data never enters | **in-context** — data already entered, gets squeezed |
| Mechanism | admission control: block, redirect, sandbox, externalize | compression: crush, cache, retrieve |
| Touches the wire request | never | always |
| Loss | lossless (full content in FTS5, queryable) | lossy squeeze + hash rehydrate |
Headroom's own realignment doc identifies its correct compression target as the **live zone**:
"latest user message content + latest `tool_result` + latest `function_call_output` + latest
`local_shell_call_output`" (`REALIGNMENT/00-overview.md`, Phase B).
**That is precisely the payload context-mode intercepts one layer earlier.** Headroom Phase B is
building a Rust engine to compress the latest tool result *after* it hits the wire. context-mode
stops that tool result from being produced at all. These are complements, not competitors — and the
upstream position is strictly cheaper: nothing to compress, nothing to cache-invalidate, no
token-validation fallback needed.
Three strategic unlocks, in order of value:
1. **Cache safety.** Headroom's #1 identified bug class is prompt-cache busting from request
mutation (5 top-tier cache-killer bugs, `REALIGNMENT/00-overview.md`). context-mode has
*structurally zero* cache-bust risk because it never touches the request body.
2. **Subscription safety.** The realignment flags "fingerprint-class subscription-revocation
risks" from `X-Headroom-*` header leakage, `anthropic-beta` mutation and re-serialization on
OAuth/subscription CLIs. A hook-layer product carries none of this — it is invisible to the
upstream. This is a *deployable-where-the-proxy-can't-go* capability.
3. **Proxy-free deployment.** Headroom's value today requires being in the API path
(`127.0.0.1:8787`). Verified live this session: with the proxy down, `headroom_stats` returns all
zeros and `headroom_compress` no-ops. Enterprises that cannot reroute model traffic (TLS trust,
egress policy, subscription auth) currently get nothing. context-mode's hook+MCP model needs no
interposition.
Zero references to context-mode exist in the Headroom tree today — clean slate.
---
## 2. context-mode: portable IP inventory
41,617 lines of TypeScript, 11 MCP tools, 18 host adapters, npm-distributed
(`context-mode@1.0.169`, 8 runtime deps, esbuild-bundled).
Ranked by *how hard it would be for Headroom to rebuild*:
### Tier 1 — genuinely hard, no Headroom equivalent
**1. Cross-host hook adapter layer**`src/adapters/**` (~10K LOC), `src/adapters/types.ts`,
`src/adapters/detect.ts` (737 lines), `configs/` (18 hosts).
Normalizes three incompatible paradigms — `json-stdio` (Claude Code, Gemini/Qwen, Copilot, Codex,
Kimi, Cursor, Kiro, Antigravity), `ts-plugin` (OpenCode, KiloCode, OpenClaw), `mcp-only` (Zed, Pi,
OMP) — behind one contract: normalized `PreToolUse` / `PostToolUse` / `PreCompact` /
`SessionStart` events, a `PlatformCapabilities` matrix, and a 5-way decision
(`allow | deny | modify | context | ask`). Per-host install, config-format, and self-heal machinery
included (`hooks/heal-partial-install.mjs`, `scripts/plugin-cache-integrity.mjs`).
*Why hard to rebuild:* the value is entirely in the accumulated per-host quirks. There is no spec to
implement against.
**2. Tool-boundary policy engine**`src/security.ts` (889 lines).
A real policy decision point, not a regex list: glob→regex compilation, chained-command splitting
(`&&`/`;`/`|` with escape awareness), subshell extraction, deny/ask pattern ingestion from host
settings files, project-boundary containment (`evaluateProjectContainment` — Issue #852: an approved
`ctx_execute_file` cannot escape the repo via a path the user couldn't see), and a
**shell-escape scanner** (`SHELL_ESCAPE_PATTERNS`, `extractShellCommands`) that detects
`execSync`/`subprocess`/etc. embedded inside sandboxed *non-shell* code and re-evaluates the escaped
command against policy.
*Why hard to rebuild:* this is the sandbox-escape prevention layer. Getting it wrong is a CVE.
**3. Multi-language sandbox executor**`src/executor.ts` (785), `src/runPool.ts`,
`src/exit-classify.ts`, `src/truncate.ts`.
12 languages, stdout-only egress, timeouts, background detach, output caps, exit classification.
Enforces the "Think in Code" contract: the agent programs the analysis, only the answer enters
context.
**4. Lossless externalization store**`src/store.ts` (2,071 lines).
Dual SQLite FTS5 index — a tokenized `chunks` table *plus* a `chunks_trigram` table for
substring/identifier search where BM25 tokenization fails on code — with a `vocabulary` table and
schema migration path. Auto-externalizes any output >100 KB into FTS5 and returns a pointer.
Nothing is discarded; the model queries on demand.
### Tier 2 — valuable, but partially duplicated in Headroom
**5. Counterfactual savings accounting**`src/session/analytics.ts` (3,085 lines),
`src/session/project-attribution.ts`, `src/session/db.ts` (1,726).
`ContextSavings`, `ThinkInCodeComparison`, `RealBytesStats`, `MultiAdapterLifetimeStats`,
`enumerateAdapterDirs()`. Measures *what would have entered context but didn't* — a different and
harder quantity than Headroom's `savings_ledger.py`, which records actual compression deltas.
Session event ledger + `tool_calls` + resume + per-project attribution.
**6. Multi-vendor pricing catalog**`src/session/pricing.ts` + `model-prices.json`.
61 curated models × 4 rate buckets (input / output / cache-read / cache-write), refreshed from
litellm, unknown model → `null` rather than a silently wrong Claude rate.
**Overlaps `headroom/pricing/*` heavily. Do not port.**
### Tier 3 — do not port
Compression heuristics, memory/graph/relevance, telemetry transport, dashboard, install UX,
update-check. Headroom has all of these, more mature, and Phase B/H is actively consolidating them.
---
## 3. Headroom's actual extension seams
Verified entry-point groups (all `importlib.metadata`-discovered, all opt-in):
| Seam | Group | Contract | Source |
|---|---|---|---|
| Proxy extension | `headroom.proxy_extension` | `install(app: FastAPI, config: ProxyConfig) -> None` | `headroom/proxy/extensions.py:52` |
| Pipeline extension | `headroom.pipeline_extension` | `on_pipeline_event(PipelineEvent) -> PipelineEvent \| None` over 11 stages | `headroom/pipeline.py:13,68` |
| Learn plugin | `headroom.learn_plugin` | — | `headroom/learn/registry.py:44` |
| Memory text store | `headroom.memory_text` | — | `headroom/memory/config.py:41`, `factory.py:57` |
| Memory vector store | `headroom.memory_vector` | — | `headroom/memory/config.py:34` |
| Memory store | `headroom.memory_store` | — | `headroom/memory/config.py:25` |
| CCR backend | `headroom.ccr_backend` | — | `headroom/cache/compression_store.py:981` |
| Compression hooks | (subclass, not entry point) | `pre_compress` / `compute_biases` / `post_compress` | `headroom/hooks.py:1-31` |
Two things worth noting:
- `headroom/proxy/extensions.py:32` states an explicit **stability contract**: changing
`install(app, config)` or the group name requires a deprecation cycle. This is a supported public
seam, not an accident.
- `headroom/hooks.py:16` says outright: *"Headroom SaaS implements position-aware compression and
cross-turn deduplication via these hooks."* The open-core split is already designed in.
**The exemplar to copy:** `plugins/headroom-oauth2/` — own `pyproject.toml`, own `LICENSE`, own
`SPEC.md`, registers on `headroom.proxy_extension`, dormant until `--proxy-extension oauth2`,
all config via env, "zero core changes." That is the enterprise plugin template.
**The precedent to copy:** `headroom/lean_ctx/installer.py` and `headroom/rtk/installer.py`
Headroom already ships thin installers that adopt sibling products. `plugins/headroom-agent-hooks`
already installs startup hooks into Claude Code and Copilot CLI. The socket exists.
**The gap:** Headroom has *no tool-boundary interception anywhere*. It sees `tool_use`/`tool_result`
only as message content after the fact (`headroom/parser.py`, `headroom/tokenizers/*`). Its
`PipelineStage` enum has no tool-result stage. Everything context-mode does is upstream of
Headroom's earliest hook.
---
## 4. Proposed plugins & variants
Ranked by value ÷ effort.
### P1 — `headroom-recall`: FTS5+trigram lossless store as `headroom.memory_text`
**What:** port `src/store.ts` behind the existing `headroom.memory_text` seam.
**Why this first:** it is the smallest diff onto an *already-existing* contract, and it fixes a real
product limitation. Today `headroom_retrieve(hash)` requires you to *know the hash* — the tool
description literally says "hash comes from compression markers like `[N items compressed... hash=abc123]`".
With an FTS5-backed store you get `retrieve-by-query`: "what did that build log say about OOM"
instead of "paste hash abc123". The trigram index matters specifically because BM25 tokenization
loses identifiers and stack frames.
Composes rather than replaces: `compress` → return squeezed text + hash → store the *original* in
FTS5 → rehydrate by hash **or** by query. Also a natural `headroom.ccr_backend` implementation —
the realignment wants "CCR hardens: persistent backend" (Phase B), and this is one.
**Enterprise variant:** shared team store, retention/TTL policy, per-project scoping (context-mode
already has `project-attribution.ts`), audit of every retrieval.
**Effort:** medium. Reimplement in Python/Rust against Headroom's memory interface, or ship the
node store as a sidecar. Do not port the MCP tool surface — only the store.
### P2 — `headroom-admission`: tool-boundary admission control across 18 hosts
**What:** context-mode's adapter + hook layer, distributed the way `plugins/openclaw` and
`plugins/opencode` already are (TS package under `plugins/`), reporting savings into Headroom's
`savings_ledger.py` JSONL and emitting Headroom pipeline events.
**Why:** this is the strategic piece. It gives Headroom:
- a **pre-wire** enforcement point, upstream of Phase B's live-zone engine, with no cache-bust and
no token-validation fallback required;
- coverage of **18 agent hosts** — the realignment's Phase G wants to "extend wrap CLIs (cline,
continue, goose, openhands)"; this is that work already done, and then some;
- a deployment mode that works under **subscription auth**, where the proxy is a revocation risk.
**Enterprise value — this is the DLP story Headroom cannot currently tell.** A `curl` inside a Bash
tool call never touches the proxy, so Headroom is blind to it. context-mode blocks
`curl`/`wget`/`WebFetch`/inline `fetch()`/`requests.get` at the tool boundary and forces network
egress through `ctx_fetch_and_index`. That converts a token-savings feature into an
**egress-control** feature — a different budget line and a different buyer.
**Effort:** high, but it's mostly packaging + a reporting bridge, not a rewrite. Keep it TypeScript;
Phase H retires Python *proxy* code but explicitly preserves "CLI wrappers, RTK installer" — the
installer layer is the surviving Python, and it can shell out.
### P3 — `headroom-policy` (Enterprise, license-gated): the PDP
**What:** `src/security.ts` as a policy decision point, plus centrally-managed org rulesets.
Two attach points: the hook layer from P2 (tool-level `allow/deny/ask`), and
`headroom.pipeline_extension` at `PRE_SEND` (prompt-level policy). Feeds `headroom/audit/`.
**Enterprise features that only make sense paid:** central policy service, org-wide allow/deny
rulesets, project-boundary containment enforcement, shell-escape detection inside sandboxed code,
tamper-evident audit trail, per-team reporting. Gate it with the ELv2 license key (see §6).
**Effort:** medium. The engine exists and is tested (`tests/security/`, `src/security.ts` 889 lines);
the work is the control plane.
### P4 — `headroom-sandbox`: Think-in-Code execution
**What:** `executor.ts` exposed as a Headroom MCP tool (`headroom_execute`), 12 languages,
stdout-only.
**Why:** this is the mechanism behind context-mode's largest measured savings —
`ctx_execute_file` returns 98% savings across 315 KB of real fixtures (`BENCHMARK.md` Part 1),
versus 82% for index+search (Part 2). Programming the analysis beats compressing the output.
Must ship *with* P3: the shell-escape scanner is what stops the sandbox being an escape hatch.
**Effort:** medium-high. Runtime isolation is the hard part; `headroom` already has a `sandbox` extra
in `pyproject.toml` to build on.
### P5 — `headroom-attribution`: counterfactual savings + per-project cost
**What:** port the *methodology* from `session/analytics.ts``RealBytesStats`,
`ThinkInCodeComparison`, `enumerateAdapterDirs`, `project-attribution.ts` — into Headroom's
`savings_ledger` / `reporting` / `dashboard`.
**Why:** Headroom measures compression deltas (what it squeezed). context-mode measures the
counterfactual (what never entered). Enterprise buyers want the second number, sliced by team and
repo. Do **not** port `pricing.ts``headroom/pricing/*` already does this with litellm resolution.
**Merge, don't port.** `headroom/audit/reads.py` is already a counterfactual measurement tool over
the same Claude Code transcript corpus (see §8). It has the better mechanism taxonomy — identical
repeat, subset containment, write-readback, stale, line-number scaffolding, context residency,
cache-death windows. `analytics.ts` has the multi-host coverage and per-project attribution it
lacks. Combine the two rather than adding a third implementation.
**Effort:** low-medium, mostly a metrics-definition merge.
### Variants (packaging, not code)
- **Headroom No-Proxy Edition** — P1+P2 only, zero API interposition. Sells to buyers who cannot
reroute model traffic and to every subscription-auth user. Removes the single biggest deployment
blocker Headroom has.
- **Headroom Admission Control (Enterprise)** — P2+P3+P4 with a central policy plane and fleet
enrollment across 18 hosts. Positioned as AI-agent DLP/governance, not token savings.
- **Headroom Fleet** — P5 + `enumerateAdapterDirs` for org-wide rollout state and cost reporting.
---
## 5. Evidence base
context-mode's `BENCHMARK.md`: 21 scenarios, 376 KB raw → 16.5 KB context, **96% overall**, all
fixtures captured from real tool invocations (Context7, Playwright, `gh`, vitest, tsc, nginx logs,
`git log`, analytics CSV) rather than synthetic. Honest about its weak cases — 13% on a 0.4 KB
Playwright network dump, and Part 2 openly explains why index+search only reaches 50-93% (it returns
exact code blocks rather than summaries, by design).
Test suite: 125 tests across executor/store/MCP-integration/ecosystem, plus 45 test dirs in `tests/`
covering adapters, security, session, hooks, analytics.
That's a defensible enough evidence base to reuse in Headroom's own materials, and the fixture corpus
itself is reusable for Headroom's `benchmarks/`.
---
## 6. Blockers — resolve these before writing code
**1. License incompatibility (hard blocker).**
context-mode is **Elastic License 2.0**, "Copyright 2026 Mert Koseoglu". Headroom is
**Apache-2.0**, "Copyright 2025 Headroom Contributors".
- ELv2 code **cannot** be merged into the Apache-2.0 core. Not a technicality — it would relicense
Headroom's core.
- ELv2 forbids providing the software "to third parties as a hosted or managed service." That
directly constrains `headroom-managed/`.
- Different copyright holders means this needs an **IP arrangement between entities**, not an
engineering decision.
The good news: Headroom's plugin architecture is exactly the boundary that makes this tractable.
A separate package with its own `pyproject.toml` and its own `LICENSE`, registered on an entry
point — the `plugins/headroom-oauth2/` shape — can carry ELv2 while core stays Apache-2.0. ELv2 is
also the *right* license for a license-key-gated enterprise tier; it explicitly contemplates one.
Recommendation: any context-mode-derived code ships as separately-licensed plugin packages under
`plugins/`, never vendored into `headroom/`. Get the IP arrangement in writing first.
**2. Realignment collision.**
Phases AI are ~40 PRs / 813 weeks and include deleting ~25K LOC. Do not open a new integration
front mid-Phase-B. P1 (`headroom.memory_text` / `ccr_backend`) is the exception — it *serves* Phase
B's "CCR hardens: persistent backend" goal rather than competing with it.
**3. Phase H direction.**
Python proxy code is being retired. Write nothing new in `headroom/proxy/`. Target the surviving
layers: installers, memory writers, CLI wrappers, and Rust.
---
## 7. Sequencing
| Order | Item | Gate |
|---|---|---|
| 0 | IP/licensing arrangement | before any code |
| 1 | P1 `headroom-recall` — FTS5 store on `memory_text`/`ccr_backend` | lands inside Phase B, serves it |
| 2 | P2 `headroom-admission` — 18-host hook layer under `plugins/` | after Phase A stabilizes |
| 3 | Variant: **No-Proxy Edition** = P1+P2 | as soon as P2 works on 3+ hosts |
| 4 | P3 `headroom-policy` (Enterprise, ELv2, key-gated) | after P2 |
| 5 | P4 `headroom-sandbox` | with P3, never before |
| 6 | P5 `headroom-attribution` | opportunistic |
---
## 8. Follow-up verification
All four items flagged as open in the first pass are now resolved.
**`headroom-managed/` is the SaaS arm, and it is unlicensed.**
`headroom-managed/pyproject.toml`: `name = "headroom-managed"`, `description = "Headroom SaaS
Platform - Managed context window optimization"`, `version = 0.1.0`. It has `app/auth.py`,
`app/middleware/`, `app/routes/`, `app/services/`, `app/models.py`, alembic migrations, and a
`pilot/`. There is **no `license` field and no LICENSE file** — i.e. proprietary by default.
This *sharpens* the §6 blocker rather than easing it. ELv2 forbids providing the software "to third
parties as a hosted or managed service." The product whose name is literally *Managed* is the one
place context-mode-derived code cannot go without an explicit commercial grant from the copyright
holder. Plan the plugin boundary so that `headroom-managed` consumes only Apache-2.0 core
interfaces, never ELv2 implementations.
**`headroom/audit/reads.py` does not overlap P3 — and it independently validates the whole thesis.**
It is a *measurement* tool, not an audit trail: it streams Claude Code `*.jsonl` transcripts to size
"the addressable bytes for each Read compression mechanism... so defaults are set from traffic, not
theory." No policy, no tamper-evidence. P3's audit trail remains a gap.
Two lines in its docstring are the most useful corroboration in either repo:
- *"context residency — how many assistant turns each Read stays in context (the multiplier on its
prefix-cache read cost; **the case for compress-before-cache-entry**)"* — Headroom is already
arguing, from its own traffic, for moving earlier in the pipeline. context-mode is the terminus of
that argument: compress before **context** entry, not merely before cache entry.
- *"identical repeat — a dedup mechanism for this was prototyped and removed: it measured 0.1% of
Read bytes on real traffic."* — Headroom has already empirically established that
message-history-level dedup is worthless. The addressable bytes are at the tool boundary, not in
history. That is the same conclusion the realignment reached from the cache side, arrived at
independently from the traffic side.
It *does* overlap **P5**`audit/reads.py` and context-mode's `session/analytics.ts` are two
independent implementations of counterfactual measurement over the same transcript corpus. Merge
them rather than porting; `audit/reads.py` has the better mechanism taxonomy, `analytics.ts` has
multi-host coverage and per-project attribution.
**No plugin-authoring docs exist.** `docs/` is a Next.js site (`app/`, `content/`, `components/`);
`wiki/` has nothing on extension authoring (only `macos-deployment.md` matched). `plugins/headroom-oauth2/SPEC.md`
remains the de-facto authoring reference — which means whichever plugin lands first sets the house
style. Worth writing the authoring doc as part of P1.
**Headroom publishes no benchmark results.** `benchmarks/` is 29 runner scripts with no committed
results artifacts, so no like-for-like number exists to compare against context-mode's 96%. The
comparison has to be run. The harness is there and is unusually strong on exactly the axis that
matters: `prefix_cache_benchmark.py`, `cache_bust_trace_report.py`, `cache_validation_bundle.py`,
`synthetic_token_cache_bust_report.py`, `proxy_mode_benchmark.py`, `agent_cost_benchmark.py`,
`real_world_agent_benchmark.py`. Use it to *prove* the §1 cache-safety claim empirically rather than
asserting it — a measured "zero cache-bust events" result is the strongest possible artifact for the
No-Proxy Edition.
**Bonus finding — the platform axes are orthogonal.**
`docs/platform-feature-matrix.json` (schema v1, updated 2026-07-06) tracks coverage across
`["linux", "macos", "windows"]` — Headroom's platform axis is **operating system**. context-mode's
platform axis is **agent host** (18 of them). Headroom tracks no host-coverage matrix at all. P2
therefore fills a dimension that does not currently exist in Headroom's own feature accounting,
which also means it needs a second matrix rather than new rows in this one.
*Process note:* six subagents were dispatched across this analysis and all six stalled at the
600-second watchdog; one reported "Bash is temporarily unavailable" before dying, so the failures
were tool-layer, not analytical. Every finding in this document was verified directly.
+46
View File
@@ -413,6 +413,40 @@ PROVIDER_REGISTRY: dict[str, ProviderConfig] = {
}
# How long an upstream call may go silent before we give up on it.
#
# WHY THIS EXISTS. There was no timeout here at all, so a request the upstream
# never answered blocked its caller forever. Observed 2026-08-07 under load:
# four agent workers sat on ESTABLISHED connections for 36+ minutes while this
# proxy answered /readyz in 0.11s. No error, no retry, no log line -- the
# client just stops. That is the worst shape a failure can take, because it is
# indistinguishable from slow work and no supervisor can tell the difference.
#
# A float, not an httpx.Timeout, on purpose: litellm expands a float into all
# four httpx phases, so for a STREAMING call this becomes the maximum gap
# BETWEEN CHUNKS rather than a cap on total generation time. A long answer
# streaming steadily is never cut off; a stalled one dies. That is the
# semantic we want, and it falls out of the simpler type.
#
# 600s is deliberately generous -- long enough that no healthy call is at
# risk, short enough that a hang surfaces within a coffee break instead of
# never.
UPSTREAM_TIMEOUT_ENV = "HEADROOM_UPSTREAM_TIMEOUT"
DEFAULT_UPSTREAM_TIMEOUT = 600.0
def _upstream_timeout() -> float:
"""Seconds. Never raises; a junk env value must not disable the timeout."""
import os
try:
v = float(os.getenv(UPSTREAM_TIMEOUT_ENV, DEFAULT_UPSTREAM_TIMEOUT))
except (TypeError, ValueError):
return DEFAULT_UPSTREAM_TIMEOUT
# 0 or negative would mean "no timeout" to httpx, which is the bug.
return v if v > 0 else DEFAULT_UPSTREAM_TIMEOUT
def get_provider_config(provider: str) -> ProviderConfig:
"""Get provider config, with fallback for unknown providers."""
if provider in PROVIDER_REGISTRY:
@@ -925,6 +959,9 @@ class LiteLLMBackend(Backend):
logger.debug(f"LiteLLM request: model={litellm_model}")
# Make the call
# Bounded, always: an upstream that never answers must not
# block the caller forever. setdefault so an explicit value wins.
kwargs.setdefault("timeout", _upstream_timeout())
response = await acompletion(**kwargs)
# Convert to Anthropic format
@@ -1055,6 +1092,9 @@ class LiteLLMBackend(Backend):
kwargs["stream_options"] = {"include_usage": True}
# Stream content — blocks emitted dynamically based on response
# Bounded, always: an upstream that never answers must not
# block the caller forever. setdefault so an explicit value wins.
kwargs.setdefault("timeout", _upstream_timeout())
response = await acompletion(**kwargs)
output_tokens = 0
current_block_index = -1
@@ -1283,6 +1323,9 @@ class LiteLLMBackend(Backend):
logger.debug(f"LiteLLM OpenAI request: model={litellm_model}")
# Make the call
# Bounded, always: an upstream that never answers must not
# block the caller forever. setdefault so an explicit value wins.
kwargs.setdefault("timeout", _upstream_timeout())
response = await acompletion(**kwargs)
# Build the usage block. LiteLLM normalizes prompt-cache stats from
@@ -1452,6 +1495,9 @@ class LiteLLMBackend(Backend):
elif headers.get("x-api-key"):
kwargs["api_key"] = headers["x-api-key"]
# Bounded, always: an upstream that never answers must not
# block the caller forever. setdefault so an explicit value wins.
kwargs.setdefault("timeout", _upstream_timeout())
response = await acompletion(**kwargs)
async for chunk in response:
+13 -3
View File
@@ -2562,8 +2562,14 @@ class AnthropicHandlerMixin:
# turn-hook fold (optimized_messages is post-hook). Runs unconditionally.
try:
_orig_snapshot = original_client_messages # noqa: F821 (bound at request start)
original_tokens = tokenizer.count_messages(_orig_snapshot)
optimized_tokens = tokenizer.count_messages(optimized_messages)
# Off the event loop (#2810): both passes are CPU-bound real BPE
# since #2543, and running them inline stalled every other
# in-flight request on the same process (~1s on a 2.3 MB body).
# Same tokenizer instance, so the reported values are unchanged.
original_tokens = await asyncio.to_thread(tokenizer.count_messages, _orig_snapshot)
optimized_tokens = await asyncio.to_thread(
tokenizer.count_messages, optimized_messages
)
# Fold the tool-schema/desc compaction delta into BOTH endpoints so
# tok_before - tok_after == tok_saved stays coherent in the PERF line
# (count_messages never sees tool bytes). Same shape as the OpenAI chat
@@ -2657,7 +2663,11 @@ class AnthropicHandlerMixin:
parsed_original = json.loads(original_body_bytes)
if parsed_original != body:
body_mutation_tracker.mark_mutated("structural_diff_vs_original")
except (json.JSONDecodeError, ValueError):
# MemoryError is not a ValueError, so a re-parse spike on 1M-context
# bodies used to escape and abort an otherwise-fine request (#2768).
# This block is a safety net; marking mutated is already the safe
# outcome (it forces canonical re-serialization).
except (json.JSONDecodeError, ValueError, MemoryError, RecursionError):
body_mutation_tracker.mark_mutated("original_unparseable")
if (
+5 -2
View File
@@ -3721,8 +3721,11 @@ class OpenAIHandlerMixin:
# (result.tokens_before), which mismatches optimized_tokens (provider tokenizer)
# and yields impossible tok_after>tok_before. Recount original from the
# pre-compression snapshot so the message delta is on one scale.
original_tokens = tokenizer.count_messages(original_client_messages)
optimized_tokens = tokenizer.count_messages(body["messages"])
# Off the event loop (#2810): see the matching note in the Anthropic handler.
original_tokens = await asyncio.to_thread(
tokenizer.count_messages, original_client_messages
)
optimized_tokens = await asyncio.to_thread(tokenizer.count_messages, body["messages"])
if tool_tokens_before_compaction > 0:
try:
tool_tokens_after_compaction = tokenizer.count_text(_json_debug_dumps(tools))
+4 -4
View File
@@ -417,14 +417,14 @@ class PrometheusMetrics:
return total_input_tokens, total_input_cost_usd
try:
cost_stats = self.cost_tracker.stats()
# totals() rather than stats(): identical numbers, without the
# 31-day cost-record walk that stats()["budget_basis"] performs and
# this caller throws away. See CostTracker.totals.
tracked_input_tokens, tracked_input_cost_usd = self.cost_tracker.totals()
except Exception:
logger.debug("Failed to read cost tracker totals for savings history", exc_info=True)
return total_input_tokens, total_input_cost_usd
tracked_input_tokens = cost_stats.get("total_input_tokens")
tracked_input_cost_usd = cost_stats.get("total_input_cost_usd")
if tracked_input_tokens is not None:
try:
total_input_tokens = self._savings_tracker_input_tokens_offset + max(
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# run-all-plugins.sh — install + configure + run the Headroom proxy with ALL 5
# enterprise plugins, the coding savings-profile, and ML compression offloaded
# to the Kompress-v2 Modal endpoint. Then confirm everything loaded.
#
# Plugins : lossless_guard, skill_search, observability, tier_router, tool_search
# Extra : headroom-ai[sandbox] (torch-free proxy; ML offloaded to Modal)
# Profile : coding (HEADROOM_SAVINGS_PROFILE) + cache mode (prefix-cache safe)
#
# Secrets are SOURCED from ~/env.txt and ~/.headroom/plugins.env — never inlined.
# Re-runnable: install is skipped when already satisfied (FORCE_INSTALL=1 forces).
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
HR=/Users/tcms/demo/headroom
VENV="$HR/.venv"
PORT="${HEADROOM_PORT:-8787}"
ENV_TXT="${ENV_TXT:-$HOME/env.txt}"
PLUGINS_ENV="$HOME/.headroom/plugins.env"
LOG="${HEADROOM_LOG:-$HOME/.headroom/logs/proxy-all-plugins.log}"
mkdir -p "$(dirname "$LOG")"
# ── 1. venv ──────────────────────────────────────────────────────────────────
# `python`/`pip`/`uv` are broken system-wide on this box — always use the venv,
# and `python -m pip` (the .venv/bin/pip shim is broken too).
# shellcheck disable=SC1091
source "$VENV/bin/activate"
PY="$VENV/bin/python"
# ── 2. install (guarded) ──────────────────────────────────────────────────────
# headroom-ai[sandbox] pulls proxy,code,relevance,reports,otel,html,mcp,spreadsheet
# (all torch-free — heavy ML is offloaded to the Modal Kompress endpoint below).
# The 5 plugins install --no-deps so pip won't drag PyPI's headroom-ai over the
# local editable one; headroom-license is their shared Ed25519 verifier.
need_install=1
if [ "${FORCE_INSTALL:-0}" != "1" ]; then
n=$("$PY" -c 'import opentelemetry; from headroom.proxy.extensions import discover; print(len(list(discover())))' 2>/dev/null || echo 0)
[ "$n" = "5" ] && need_install=0
fi
if [ "$need_install" = "1" ]; then
avail=$(df -g "$HR" 2>/dev/null | awk 'NR==2{print $4}')
echo "▶ disk: ${avail:-?}Gi free before install"
if [ -n "$avail" ] && [ "$avail" -lt 2 ]; then
echo "!! <2Gi free — aborting before heavy install (free space, then re-run)"; exit 1
fi
echo "▶ installing pip+maturin, then headroom-ai[sandbox] + license + 5 plugins (editable)…"
"$PY" -m pip install -U pip maturin
# litellm >=1.92 ships an sdist-only Rust bridge whose AWS-SDK crates need rustc>=1.94.1;
# the default rustup toolchain here is older (pip builds litellm in a temp dir that misses
# the repo's 1.95 pin), so pin to the last pure-Python wheel line (1.91.4). Satisfies
# headroom's litellm>=1.86.2,<2.0 and skips the Rust build entirely.
"$PY" -m pip install "litellm<1.92"
"$PY" -m pip install -e "${HR}[sandbox]" "litellm<1.92"
"$PY" -m pip install -e /Users/tcms/demo/headroom-license
for p in lossless-guard skill-search observability tier-router tool-search; do
"$PY" -m pip install -e "/Users/tcms/demo/headroom-${p}" --no-deps
done
else
echo "▶ install satisfied (5 extensions discovered) — skipping (FORCE_INSTALL=1 to force)"
fi
# ── 3. secrets from ~/env.txt ─────────────────────────────────────────────────
# Provides: OPENAI_API_KEY, ANTHROPIC_API_KEY, FIREWORKS_API_KEY (upstream creds);
# LANGFUSE_{PUBLIC,SECRET}_KEY + LANGFUSE_BASE_URL (observability sink);
# HEADROOM_KOMPRESS_ENDPOINT + _TOKEN (Modal ML offload).
[ -f "$ENV_TXT" ] || { echo "!! $ENV_TXT not found"; exit 1; }
set -a; # shellcheck disable=SC1090
source "$ENV_TXT"; set +a
# ── 4. plugin license (Ed25519, offline, wildcard) ────────────────────────────
# HEADROOM_LICENSE + HEADROOM_LICENSE_PUBKEY. This is SEPARATE from the OSS cloud
# key (HEADROOM_LICENSE_KEY) — the banner will still say "OSS (no license key)",
# but each plugin prints "license accepted". Fallback: skip verification entirely.
if [ -f "$PLUGINS_ENV" ]; then
set -a; # shellcheck disable=SC1090
source "$PLUGINS_ENV"; set +a
else
echo "$PLUGINS_ENV missing — using dev license bypass"
export HEADROOM_LICENSE_DEV=1
fi
# ── 5. Kompress ML offload → Modal ────────────────────────────────────────────
# Setting HEADROOM_KOMPRESS_ENDPOINT (+_TOKEN) alone routes Kompress inference to
# the Modal endpoint (content_router._get_kompress_remote). No other flag needed;
# HEADROOM_COMPRESS_ALLOW_REMOTE is a different thing (remote upstreams, not this).
: "${HEADROOM_KOMPRESS_ENDPOINT:?must be set in $ENV_TXT}"
export HEADROOM_KOMPRESS_ENDPOINT_TOKEN="${HEADROOM_KOMPRESS_ENDPOINT_TOKEN:-}"
# ── 6. observability sink → Langfuse + spend attribution ──────────────────────
# HEADROOM_LANGFUSE_ENABLED must be explicitly truthy (LANGFUSE_* creds come from
# env.txt). Traces (agent.turn / llm.turn spans with gen_ai.usage.cost) land in
# Langfuse. Spend is opt-in: HEADROOM_MODEL_PRICES is {model-substr:{in,out}} in
# USD per 1K tokens. (Per-request identity — org/team/user/session — is supplied
# by the CLIENT via x-headroom-* headers, not settable here.)
export HEADROOM_LANGFUSE_ENABLED=1
export HEADROOM_LANGFUSE_SERVICE_NAME=headroom-proxy
export HEADROOM_MODEL_PRICES='{"claude-opus":{"in":0.015,"out":0.075},"claude-sonnet":{"in":0.003,"out":0.015},"gpt-5":{"in":0.00125,"out":0.01},"gpt-4":{"in":0.003,"out":0.012}}'
# Metrics (counters) need a separate OTLP endpoint — none in env.txt, so left off:
# export HEADROOM_OTEL_METRICS_ENABLED=1 HEADROOM_OTEL_METRICS_ENDPOINT=http://localhost:4318
# ── 7. tier_router ─────────────────────────────────────────────────────────────
# Only stamps service_tier on the wire (no token delta). OpenAI 'flex' is only
# auto-selected for models declared eligible here. Anthropic tiers are a no-op by
# default. Clients force a tier with x-headroom-tier / x-headroom-background: 1.
export HEADROOM_TIER_FLEX_MODELS="${HEADROOM_TIER_FLEX_MODELS:-gpt-5,gpt-4.1,o4-mini}"
# ── 8. plugin tuning (defaults shown; override as needed) ─────────────────────
# skill_search fires on Anthropic w/ >=min skills; tool_search on synthetic-tier
# providers w/ >=min tools; lossless_guard lossy tier is opt-in (kept OFF).
export HEADROOM_SKILL_SEARCH_MIN_SKILLS="${HEADROOM_SKILL_SEARCH_MIN_SKILLS:-8}"
export HEADROOM_TOOL_SEARCH_MIN_TOOLS="${HEADROOM_TOOL_SEARCH_MIN_TOOLS:-5}"
# export HEADROOM_LOSSLESS_GUARD_LOSSY=1 # opt-in irreversible Bash-noise drop
# ── 9. coding profile + mode ───────────────────────────────────────────────────
# savings_profile=coding tunes the pipeline for coding-agent traffic; cache mode
# freezes prior turns to preserve the provider prefix-cache (what coding wants).
export HEADROOM_SAVINGS_PROFILE=coding
# ── 10. run + confirm ──────────────────────────────────────────────────────────
cleanup() { [ -n "${PROXY_PID:-}" ] && kill "$PROXY_PID" 2>/dev/null || true; }
trap cleanup INT TERM EXIT
echo "▶ starting proxy on :$PORT (profile=coding, mode=cache, all 5 extensions)…"
headroom proxy --port "$PORT" --mode cache --proxy-extension '*' > "$LOG" 2>&1 &
PROXY_PID=$!
# wait for readiness (no foreground sleep on this harness)
curl -s --retry 40 --retry-delay 1 --retry-all-errors --max-time 60 \
"http://127.0.0.1:$PORT/health" >/dev/null 2>&1 || true
echo
echo "══════════════════ CONFIRMATION ══════════════════"
echo "── extensions loaded (from $LOG) ──"
grep -iE "Extensions:|license accepted|installed \(" "$LOG" | sed 's/^/ /' || true
echo "── Modal Kompress endpoint reachable? ──"
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 "$HEADROOM_KOMPRESS_ENDPOINT" || echo "unreachable")
echo " $HEADROOM_KOMPRESS_ENDPOINT -> HTTP $code (any response = up; offload runs on real traffic)"
echo "── /stats surfaces (empty until traffic flows) ──"
curl -s --max-time 5 "http://127.0.0.1:$PORT/stats" | "$PY" -c '
import sys,json
d=json.load(sys.stdin)
print(" extension_savings :", d.get("extension_savings"))
print(" by_layer :", list(d.get("savings",{}).get("by_layer",{})))
print(" tokens_saved_by_strat:", d.get("tokens_saved_by_strategy"))
print(" otel.enabled :", d.get("otel",{}).get("enabled"))
print(" langfuse.enabled :", d.get("langfuse",{}).get("enabled"))
' 2>/dev/null || echo " (stats not ready)"
cat <<EOF
── where each effect shows up ──
lossless_guard -> dashboard (compression layer) + /stats.tokens_saved_by_strategy
skill_search -> /stats.extension_savings (NOT dashboard) — Anthropic client, >=8 skills
tool_search -> /stats.extension_savings (NOT dashboard) — synthetic-tier client, >=5 tools
observability -> Langfuse UI (spans + gen_ai.usage.cost) — send x-headroom-org/user/session
tier_router -> service_tier on the wire / provider bill (no token delta)
── drive traffic (two clients — they exercise different plugins) ──
Claude Code : ANTHROPIC_BASE_URL=http://localhost:$PORT claude # lossless_guard + skill_search
OpenAI/opencode: OPENAI_BASE_URL=http://localhost:$PORT/v1 <client> # tool_search
Dashboard : headroom dashboard (http://127.0.0.1:$PORT/dashboard)
Raw stats : curl -s localhost:$PORT/stats | python3 -m json.tool
Proxy is running (pid $PROXY_PID). Ctrl-C to stop. Logs: $LOG
═══════════════════════════════════════════════════
EOF
wait "$PROXY_PID" || true
+70
View File
@@ -0,0 +1,70 @@
"""Every upstream call must be bounded.
There was no timeout in this backend at all. Observed 2026-08-07 under load:
four agent workers blocked on ESTABLISHED connections for 36+ minutes while
the proxy answered /readyz in 0.11s. No error, no retry, no log line -- the
caller simply stops, forever, and that is indistinguishable from slow work.
"""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
from headroom.backends.litellm import (
DEFAULT_UPSTREAM_TIMEOUT,
UPSTREAM_TIMEOUT_ENV,
_upstream_timeout,
)
_SRC = Path(__file__).resolve().parents[1] / "headroom" / "backends" / "litellm.py"
def test_every_acompletion_call_is_bounded():
"""A new dispatch path added without a timeout reintroduces the hang.
Checked structurally rather than by mocking, because the failure mode is a
call site someone ADDS later -- which no mock of the existing paths sees.
"""
tree = ast.parse(_SRC.read_text())
calls, guards = 0, 0
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
fn = node.func
if isinstance(fn, ast.Name) and fn.id == "acompletion":
calls += 1
if (
isinstance(fn, ast.Attribute)
and fn.attr == "setdefault"
and node.args
and isinstance(node.args[0], ast.Constant)
and node.args[0].value == "timeout"
):
guards += 1
assert calls > 0, "no acompletion call sites found -- test is stale"
assert guards >= calls, (
f"{calls} acompletion call site(s) but only {guards} timeout guard(s); "
"an unbounded upstream call blocks its caller forever"
)
def test_a_junk_env_value_cannot_disable_the_timeout(monkeypatch):
"""`0` means 'no timeout' to httpx, i.e. exactly the bug. So does junk."""
for bad in ("", "0", "-1", "nonsense", "None"):
monkeypatch.setenv(UPSTREAM_TIMEOUT_ENV, bad)
assert _upstream_timeout() == DEFAULT_UPSTREAM_TIMEOUT, bad
def test_an_operator_can_still_tune_it(monkeypatch):
monkeypatch.setenv(UPSTREAM_TIMEOUT_ENV, "42.5")
assert _upstream_timeout() == pytest.approx(42.5)
def test_the_default_is_generous_enough_for_real_work():
"""Streaming: litellm expands a float across all httpx phases, so this is
the max gap BETWEEN CHUNKS, not a cap on total generation. A steady long
answer is never cut off."""
assert 60.0 <= DEFAULT_UPSTREAM_TIMEOUT <= 1800.0
@@ -0,0 +1,158 @@
"""Two request-path safety nets in ``handle_anthropic_messages``.
1. #2810 — the consistency re-count runs ``count_messages`` twice. Both passes
are CPU-bound real BPE (since #2543) and used to run directly on the event
loop, stalling every other in-flight request on the process (~1s on a 2.3 MB
body). They must run off the loop.
2. #2768 — the byte-faithful forwarder's verification re-parse of the original
body is best-effort, but ``MemoryError`` is not a ``ValueError``, so on
1M-context payloads it escaped and aborted an otherwise-fine request. The
block must never be able to fail the request.
"""
from __future__ import annotations
import asyncio
import json
from unittest.mock import AsyncMock, MagicMock
import pytest
fastapi = pytest.importorskip("fastapi")
from fastapi.testclient import TestClient # noqa: E402
from headroom.proxy.server import ProxyConfig, create_app # noqa: E402
MESSAGES = "/v1/messages"
MODEL = "claude-sonnet-4-6"
# Only ever present in the PRE-compression snapshot, never in the outbound body.
# Long enough to clear the handler's min-token floors.
SENTINEL = "presnapshot-sentinel " * 500
def _config(**overrides) -> ProxyConfig:
base = {
"optimize": True,
"cache_enabled": False,
"rate_limit_enabled": False,
"cost_tracking_enabled": False,
"mode": "token",
}
base.update(overrides)
return ProxyConfig(**base)
def _upstream_200() -> MagicMock:
payload = {
"id": "msg_1",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "ok"}],
"model": MODEL,
"usage": {"input_tokens": 10, "output_tokens": 2},
}
resp = MagicMock()
resp.status_code = 200
resp.headers = {"content-type": "application/json"}
resp.content = json.dumps(payload).encode()
resp.text = json.dumps(payload)
resp.json.return_value = payload
return resp
def test_consistency_recount_runs_off_the_event_loop(monkeypatch):
"""No ``count_messages`` pass over the pre-compression snapshot may run on
the loop thread. The snapshot is identified by SENTINEL, which the pipeline
strips, so this pins the re-count specifically: the already-offloaded count
at request start also sees the sentinel and passes either way, while the
two re-count passes ran inline before #2810 and would fail here.
"""
import headroom.tokenizers as tokenizers_mod
seen: list[bool] = [] # one entry per snapshot count: True == ran on the loop
# Patch the class, not the cached instance, so pytest restores it for us.
tokenizer_cls = type(tokenizers_mod.get_tokenizer(MODEL))
real_count = tokenizer_cls.count_messages
def counting(self, messages): # noqa: ANN001, ANN202
if SENTINEL in json.dumps(messages, default=str):
try:
asyncio.get_running_loop()
except RuntimeError:
seen.append(False) # worker thread — no running loop here
else:
seen.append(True) # blocking the event loop
return real_count(self, messages)
monkeypatch.setattr(tokenizer_cls, "count_messages", counting)
def stripping_apply(**kwargs): # noqa: ANN003, ANN202
"""Return genuinely-changed messages with the sentinel removed."""
from types import SimpleNamespace
compressed = [{**m, "content": "compressed"} for m in kwargs["messages"]]
return SimpleNamespace(
messages=compressed,
transforms_applied=["test_strip"],
timing={},
tokens_before=100,
tokens_after=80,
waste_signals=None,
)
app = create_app(_config())
with TestClient(app) as client:
proxy = client.app.state.proxy
proxy.anthropic_pipeline.apply = MagicMock(side_effect=stripping_apply)
proxy._retry_request = AsyncMock(return_value=_upstream_200())
r = client.post(
MESSAGES,
json={
"model": MODEL,
"max_tokens": 16,
"messages": [{"role": "user", "content": SENTINEL}],
},
)
assert r.status_code == 200, r.text
assert seen, "no count_messages pass saw the snapshot; test is not exercising #2810"
assert not any(seen), f"{sum(seen)}/{len(seen)} snapshot counts blocked the event loop"
def test_memoryerror_in_verification_reparse_does_not_abort_the_request(monkeypatch):
"""A ``MemoryError`` from the best-effort original-body re-parse must be
swallowed (the safe fallback marks the body mutated, forcing canonical
re-serialization) rather than escaping and killing the request.
"""
import headroom.proxy.handlers.anthropic as anthropic_mod
real_loads = json.loads
raised = {"n": 0}
def exploding_loads(s, *args, **kwargs): # noqa: ANN001, ANN002, ANN003, ANN202
# Only the verification re-parse passes the raw original body bytes.
if isinstance(s, (bytes, bytearray)) and b"reparse-bomb" in s:
raised["n"] += 1
raise MemoryError("simulated re-parse spike")
return real_loads(s, *args, **kwargs)
monkeypatch.setattr(anthropic_mod.json, "loads", exploding_loads)
app = create_app(_config(optimize=False))
with TestClient(app) as client:
proxy = client.app.state.proxy
proxy._retry_request = AsyncMock(return_value=_upstream_200())
r = client.post(
MESSAGES,
json={
"model": MODEL,
"max_tokens": 16,
"messages": [{"role": "user", "content": "reparse-bomb"}],
},
)
assert raised["n"] > 0, "the verification re-parse never ran; test is not exercising #2768"
assert r.status_code == 200, r.text