Skip to content

perf(extract_llm): economic gate, global result cache, and the honest verdict (#28, rebased) - #51

Merged
OsherElhadad merged 5 commits into
mainfrom
feat/i28-extract-llm-rebased
Aug 10, 2026
Merged

perf(extract_llm): economic gate, global result cache, and the honest verdict (#28, rebased)#51
OsherElhadad merged 5 commits into
mainfrom
feat/i28-extract-llm-rebased

Conversation

@OsherElhadad

Copy link
Copy Markdown
Collaborator

Closes #28. Supersedes #34 (rebased; force-push blocked by branch protection).

extract_llm is the only component that spends money to save money, and on Terminal-Bench it lost: 271 calls, $3.26, 1,592,467 ms of added latency against 197,548 unique tokens saved — 82× underwater once those tokens are priced at the rate they would actually have been billed. ~93% of its realized value came from the replay cache, not the LLM.

The cause is arithmetic. A request to a caching backend is ~99.95% cached, so a token removed from a cached region saves the cache-read rate ($0.30/MTok), not the fresh-input rate ($3/MTok) — a 10× haircut.

What's implemented

A. Prompt-cache the preamble — and the measurement that matters. The ~1,463-token invariant contract now goes in a stable system block with cache_control (a leading system message on the OpenAI backend, which has no explicit breakpoints). Measured against the gateway, this is INERT on claude-haiku-4-5, whose minimum cacheable prefix is 4096 tokens — a sub-minimum breakpoint is silently ignored:

Prefix Model Result
~1.5k claude-haiku-4-5 write=0 read=0inert
~4.5k claude-haiku-4-5 write=5401 then read=5401
~1.5k claude-sonnet-5 write=2653 then read=2653

Shipped because it is free and correct where it wins, but /stats exposes prompt_cache_read_tokens so nobody infers a cache win from placement.

B. Reusing the agent's cached prefix — PROTOTYPED AND REJECTED, with numbers. It works mechanically (a 103,019-token prefix read from cache, no write, no invalidation) but cache-read is cheap, not free:

Prefix size Cost of one extraction vs a dedicated call ($0.004)
103,019 tok $0.03398 8.5×
500,000 tok $0.15307 38.3×
1,700,000 tok $0.51307 128.3×

At ~1.7M contexts with 4 concurrent calls/turn that is ~$2.04/turn vs ~$0.016. Plus: risks a cache-write on the agent's own prefix (11.5× a read), and couples the compaction model to the agent model.

C. Global content-hash result cache. Re-keyed on sha256(content + prompt version + model + config fingerprint) with no session prefix — 82 of 103 unique contents recurred across sessions. A version/model/config change misses rather than serving stale. Old entries inert once (noted); a session-scoped entry is honored as a migration read. Contrast #27's deliberately session-scoped index: reference vs derived result.

D. Economic gate (on by default). expected_saving = removed × (1 + expected replays) × per-token value vs the analytic size-aware cost of a call. Per-token value from Ctx.CacheAware; ratio learned from results; cost from real pricing (CHEAP_MODEL_PRICE_*), never a constant. Every decision records a reason.

E. Derived triggering. Context pressure + growth rate replace min_tokens, which stays honored when set explicitly. A merely growing context no longer fires every step.

F. Metrics. /stats gains an extract block: calls, calls avoided, calls suppressed, prompt-cache behavior, cost, gross value, and NET value after cost — the honest headline, previously impossible to see because cost lived in a field separate from savings. Purely additive; every existing key keeps its name so deploy/harbor/*.py keeps parsing.

Two deadlocks found by benchmarking

Both the same shape — a pessimistic estimate suppressing every call, so nothing was observed, so the estimate could never correct:

  1. Flat per-call cost priced everything at the $0.012 average (~5× true cost on small outputs) and suppressed everything. Actual: $0.0024/call. Cost is now analytic and size-aware, correct on the first call with zero observations.
  2. Compression ratio defaulted to 0.45; measured reality is ~0.12 (31–254 tokens removed per call on 400–2,000-token outputs). Corrected — and a bounded 3-call exploration budget now lets the tracker learn whether a workload actually compresses.

Also fixed: /compact hard-coded the context window as unknown, silently disabling every fraction-based trigger and the new pressure logic on that path, so offline eval measured a different component than ships.

Benchmark results

Replay of real captures, aws/claude-haiku-4-5. forced = pre-#28, gated = new default.

Terminal-Bench (20 requests):

Arm Backend Calls Cost Saved NET Avg latency
forced caching 4 $0.0086 1,220 −$0.0082 9,726 ms
gated caching 3 $0.0065 0 −$0.0065 6,966 ms
forced non-caching 6 $0.0109 6,664 +$0.0091 10,287 ms
gated non-caching 7 $0.0232 17,286 +$0.0287 4,654 ms

SWE-bench (19 requests):

Arm Backend Calls Cost Saved NET Avg latency
forced caching 2 $0.0098 793 −$0.0095 8,173 ms
gated caching 2 $0.0092 806 −$0.0089 5,060 ms
forced non-caching 13 $0.0593 726 −$0.0571 5,150 ms
gated non-caching 2 $0.0090 2,451 −$0.0016 3,754 ms

The verdict — stated plainly

On a non-caching backend the gate is a strict improvement, but not a profit. The tables above were produced before the H3 attribution fix, when /stats credited the whole pipeline's savings to this one component. With extract-scoped attribution the best available result is break-even, not the reported +$0.0287. What survives the correction is real and worth having: 68% less waste on Terminal-Bench, 26 calls → 1 on SWE-bench, latency roughly halved. A genuine positive result needs outputs above ~1,800 tokens on non-caching traffic, which neither capture contains.

That attribution error is the third benchmark result in this PR traced to a measurement artifact, alongside the flat $0.012/call cost and the 0.45 compression-ratio prior. All three pointed the same direction — making the component look better than it is.

On a caching backend extract_llm is still net-negative even after #28. The gate reduces the loss and cuts latency ~30%, but it cannot make the component pay when saved tokens bill at the cache-read rate and break-even is ~30,500 tokens/output against a largest-observed output of 2,053. Recommendation: disable it by default on caching backends — prefer codesafe (no LLM pass) or drop extract_llm from codesmart for caching traffic. This is documented as the verdict rather than dressed up as a win.

Break-even figures are pinned by test, so drift fails CI instead of silently invalidating the documented verdict.

Update: the external arm re-derived the economics, and it is worse

The final Terminal-Bench arm running on merged main excluded extract_llm entirely and independently re-derived its economics with a harsher result:

Quantity Value
Unique tokens saved 197,548
Where they sat the cached prefix — so they bill at the cache-read rate
Their value at cache-read $0.0395 (197,548 × $0.20/MTok)
Cost $3.26
Blocking time 1,592,467 ms
Ratio 82× underwater

The improvement plan's ~8× implicitly priced those tokens as fresh input. They were never fresh input: every one of them was inside the cached prefix, so the correct rate is the cache-read rate and the honest ratio is an order of magnitude worse. The 1,592,467 ms of blocking time is not priced at that ratio at all.

Priced instead at the $0.30/MTok sonnet-class rate the gate itself uses, the tokens are worth $0.0593 and the ratio is 55×. Both figures are the same conclusion; neither is within two orders of magnitude of paying for $3.26. The gate reasons with the more generous of the two, so the shipped decline is the conservative one.

This PR's verdict was right, and understated. Nothing in the recommendation changes — the component is already hard-declined on caching backends — but the docs now carry the corrected arithmetic instead of the 8× figure.

Cache-safety: ordering of the two result-cache reads

Restored during the rebase, and the reason it matters:

  • A same-session replay is safe at any depth, cached prefix included. This session already sent those compacted bytes, so the provider's cached prefix holds the compacted form and replaying is byte-identical.
  • A cross-session global hit has no such guarantee. The receiving session never compacted that content, so its cached prefix holds the original. Splicing another session's result at depth mutates the cached prefix and forces a suffix re-write at 11.5× the read price — exactly the harm fix(cache): slide the store TTL, pin frozen decisions, and repair lost ones #40 removed repairLostResult to prevent.

So the order is: getResult (depth-safe) → tail gategetResultGlobal, and only the same-session path may bypass the gate. A global hit is treated as a new decision: tail-only, then frozen into the session so later turns replay it from the depth-safe path. TestGlobalCacheHitIsNotSplicedAtDepth fails if the ordering is flattened — verified by flattening it and watching it fail, then restoring. The invariant is named at the call site so the next person does not have to rediscover it.

Rebase onto current main

This branch is a rebase of #34 onto main at 5768b00, replayed across nine merges (#33, #36, #40, #41, #42, #43, #44, #49, #50). Force-pushing the original branch is blocked by branch protection, so this supersedes #34 — same pattern as #37#42. Two merges required adaptation:

  • fix(cache): slide the store TTL, pin frozen decisions, and repair lost ones #40 unified the result-cache key. getResult/getResultGlobal now return (cachedResult, bool) carrying {Projected, Summary} in one JSON value, and putResult/putResultGlobal take four arguments; getSummary/putSummary/summaryKey and their global twins are gone. Adopted on both the session and global paths. The comments record why: the split cg:res: + cg:sum1: keys had independent TTLs and pin slots, so a replay could hit one and miss the other and emit half a decision — projected text with the summary segment silently gone. Splitting the pair across two global keys would re-create that bug cross-session, so the global namespace keeps them together too.
  • feat(proxy): add observe mode — measure what compaction would save, without enforcing it #43's observe-mode /stats block and this PR's extract block both extend the same function; both are additive and both are kept.

Shipping guard, re-verified

AllowOnCachingBackend (allow_on_caching_backend) and the hard decline in evaluateGate survived the rebase intact, and there is now an end-to-end test that fails if either is ever lost: TestNoDefaultConfigRunsExtractLLMOnCachingBackend drives the bare defaults and the codesmart preset config against a ~240k-token output — far above the ~30,500-token cached break-even, so the economics alone would permit the call and only the hard decline can suppress it. Mutation-checked: forcing allowCached = true at construction makes it fail. The escape hatch is asserted too, so the guard is a default and not a wall.

Verification

  • CGO_ENABLED=1 go build -tags cg_skeleton ./...
  • CGO_ENABLED=1 go test -tags cg_skeleton ./...
  • go test -race ✅ (the component runs concurrent per-output calls)
  • make lint
  • go test -race -count=2 ./components/... ./proxy/...
  • gofmt -l clean · go vet ./...
  • Tests: cached-system-block shape on both backends, cross-session cache reuse, version bump misses rather than serving stale, gate suppression when cache-aware, permission on non-caching and for recurring content, cost model vs known tokens × known price, documented break-even sizes, trigger not firing every step, and regressions for both deadlocks.
  • Added on the rebase: TestNoDefaultConfigRunsExtractLLMOnCachingBackend (the shipping guard, end to end through the presets). TestGlobalCacheHitIsNotSplicedAtDepth confirmed to fail when the read ordering is flattened.

Docs

docs/components/extract_llm.md rewritten (economics, triggering, caching, measured tables, honest verdict), docs/reference/config.md (pricing env), docs/reference/presets.md (codesmart/aggressive behavior change + verdict).

Assisted-By: Claude Opus 5

Osher-Elhadad added 5 commits August 10, 2026 15:31
…obally

extract_llm is the only component that spends money to save money, and on
Terminal-Bench it lost: 271 calls, $3.26, ~1,592s of added latency against
~197,548 unique tokens saved — ~8x underwater once those tokens are priced at
the rate they would actually have been billed. ~93% of its realized value came
from the replay cache, not from the LLM.

The cause is arithmetic. A request to a caching backend is ~99.95% cached, so a
token removed from a cached region saves the cache-read rate ($0.30/MTok), not
the fresh-input rate ($3/MTok) — a 10x haircut. Break-even for one ~$0.012 call
is therefore ~12,700 tokens of output under caching versus ~1,270 without it,
and most tool outputs are nowhere near that. The component already carried this
insight as a comment on skip_file_reads; this turns it into an actual gate that
applies to every candidate.

Economic gate (on by default): call the LLM only when
  expected_saving = removed x (1 + expected replays) x per-token value
exceeds the observed mean cost of a call. The per-token value comes from
Ctx.CacheAware, the compression ratio is learned from accepted results (repeated
misses drive it to zero and shut the gate), and the call cost is computed from
real token usage times real model pricing (CHEAP_MODEL_PRICE_*), never a
hard-coded constant. Every decision records a reason.

Global result cache: an extraction is a context-free derived result, so re-key it
on sha256(content + prompt version + model + config fingerprint) with no session
prefix — 82 of 103 unique contents recurred across sessions, and the old key
threw that reuse away. A version, model, or config change misses rather than
serving a stale extraction. Old entries are inert once; a session-scoped entry is
still honored as a migration read. (Contrast #27's xdedup index, session-scoped on
purpose: that makes a conversational reference, this caches a derived result.)

Derived triggering: no per-workload threshold. Context pressure plus growth rate
replace min_tokens, which stays honored when set explicitly. A merely growing
context no longer fires on every step.

Prompt-cache the preamble, with the measurement that matters: the ~1,463-token
invariant contract now goes in a stable system block with cache_control (a leading
system message on OpenAI, which has no explicit breakpoints). Measured against the
gateway, this is INERT on claude-haiku-4-5, whose minimum cacheable prefix is 4096
tokens — a sub-minimum breakpoint is silently ignored, write=0 read=0. It caches on
claude-sonnet-5 (minimum 1024). Shipped because it is free and correct where it
wins, but /stats exposes prompt_cache_read_tokens so nobody infers a cache win
from placement.

Reusing the agent's cached prefix (part B) prototyped and REJECTED, with numbers:
it works mechanically (a 103,019-token prefix read from cache, no write, no
invalidation), but costs $0.034 at 103k, $0.153 at 500k and $0.513 at 1.7M tokens
— 8.5x to 128x a dedicated cheap-model call, and ~$2.04/turn at 4 concurrent
calls. It also risks a cache-write on the agent's own prefix (11.5x a read) and
couples the compaction model to the agent model.

Metrics: /stats gains an extract block with calls, calls avoided by cache, calls
suppressed by the gate, prompt-cache behavior, cost, gross value and NET value
after cost — the honest headline, previously impossible to see because cost was
reported in a field separate from savings. Purely additive; every existing key
keeps its name so deploy/harbor/*.py keeps parsing.

Tests cover the cached-system-block shape on both backends, cross-session cache
reuse, a version bump missing rather than serving stale, gate suppression when
cache-aware, permission on a non-caching backend and for recurring content, the
cost model against known tokens times known price, the documented break-even
sizes, and that the trigger does not fire every step on a growing context.

Refs #28

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
Benchmarking the gate on real captures found two deadlocks of the same shape: a
pessimistic estimate suppressing every call, so nothing was ever observed, so the
estimate could never correct itself. A gate that cannot revise its own prior is
an off switch, not a gate.

1. Flat per-call cost. The gate priced every call at the ~$0.012 Terminal-Bench
   average — roughly 5x the true cost on a workload with small outputs — and
   suppressed everything. Observed cost was $0.0024/call, and calls that were
   ~2.2x profitable were declined. Cost is now analytic and size-aware (preamble
   1,463 tok + shown content + overhead, at real rates), reconciled with the
   observed mean once real calls exist, so it is right on the first call with no
   observations at all.

2. Pessimistic compression ratio. The default was 0.45; measured on real captures
   an accepted extraction removes only 31-254 tokens per call on 400-2,000-token
   outputs — a real ratio near 0.12. Corrected, with the direction of conservatism
   made explicit: for a SPENDING gate, conservative means UNDER-estimating the
   saving. But a pessimistic prior on a workload below break-even then suppressed
   everything and forwent a genuine +$0.0094 net, so the tracker now gets a bounded
   budget of 3 exploratory calls to learn whether this workload actually compresses.

Also fixes /compact hard-coding the context window as unknown, which silently
disabled every fraction-based trigger AND the new pressure-based triggering on
that endpoint — so offline replay/eval measured a different component than ships.

Measured after these fixes (replay of real captures, aws/claude-haiku-4-5;
forced = pre-#28 behavior, gated = new default):

  Terminal-Bench, non-caching:  net +$0.0287 vs +$0.0091 (3.2x), 17,286 vs 6,664
                                tokens saved, 4,654 vs 10,287 ms avg latency
  SWE-bench, non-caching:       13 calls -> 2, net -$0.0571 -> -$0.0016 (-97% waste)
  Both, caching backend:        loss reduced and latency ~30% lower, but still
                                NEGATIVE — the gate cannot make the component pay
                                when saved tokens bill at the cache-read rate

The honest verdict, now documented: on a non-caching backend extract_llm earns its
place; on a caching backend it does not, even after #28. Docs recommend codesafe
(no LLM pass) or dropping extract_llm from codesmart for caching traffic.

Break-even sizes are pinned by test so a drift in these figures fails CI rather
than silently invalidating the documented verdict.

Refs #28

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
…tion scope

Three high-severity review defects, plus the shipping decision.

H3 (worst): /stats credited the WHOLE pipeline's savings to extract_llm. It passed
snap.SavedTokens — every component's savings — valued against extract_llm's cost
alone, so on a preset like codesmart the component displayed as comfortably
positive while its own arithmetic proves it negative. It inverted the conclusion in
the single field an operator reads.

This also invalidated our own benchmark table: the previously reported "+$0.0287
net win" on non-caching traffic was produced BY the bug. With honest attribution
the best result on these captures is break-even, not profit. Docs corrected.

ExtractSnapshot now takes a per-saved-token RATE and applies it internally to the
component's own GrossSavedTokens, so passing a pipeline-wide total is no longer
expressible. Regression test fails if anyone re-wires it.

H1: markSeenContent fired after the gate ALLOWED a call, so first sight
reclassified itself as recurring and collected a 50% valuation bump (6 expected
reuses vs 4) it had not earned — the gate over-firing, opposite in direction to the
two pessimistic priors fixed earlier. It is now test-and-set on OBSERVATION,
returning the prior value; a suppressed candidate still counts as seen, which is
correct since recurrence is a property of the content, not of what we spent. The
flag-setting path now has tests; previously only the pre-computed bool did.

H2: the exploration budget was per-PROCESS. The tracker lives on a Pipeline held
for proxy lifetime, so the first session spent the whole budget and every later
session inherited an unrevisable prior — the self-justifying-prior failure at
process scope. Now per-session (budget 3 -> 2). The learned ratio is also shrunk
toward the prior and capped: minRatioSampleTokens is one medium output, so a raw
n=1 mean could drop the cached break-even from ~30,500 to ~7,000 permanently.

PR #37: exploration spends wall clock as well as money, and an agent on a task
deadline feels the former more (#37 measured 17.8s over 2 calls saving 0 tokens).
Speculative calls now stop once observed mean latency reaches 6s.

SHIPPING DECISION, in code rather than prose: extract_llm is disabled by default on
prompt-caching backends (allow_on_caching_backend: true overrides). Every caching
workload measured is net-negative even with a correct gate — break-even ~30,500
tokens/output against a largest-observed 2,053 — so the gate could reduce the loss
but never eliminate it. codesmart is the proxy default; shipping a component our own
numbers say loses money, guarded only by a doc note, is not a defensible default.
On caching traffic it now makes zero calls and costs nothing.

Medium items taken:
- Floor dropped from the cache fingerprint except in "auto" mode. It is derived from
  context pressure, so it rotated the key as the window filled and discarded the
  cross-session reuse the key exists to capture; strategyOrder reads it only on the
  "auto" branch, so elsewhere it cannot change the result.
- PromptVersion is now DERIVED from a hash of the prompt constants. A manual constant
  only works while every future editor remembers to bump it, and one omission serves
  extractions produced under rules that no longer exist, with no symptom.
  semanticsVersion remains as the manual hatch for validation-gate changes.
- Cross-session reuse gated on RECOVERABILITY rather than restricted to deletion-only:
  a cached result can be a lossy rewrite steered by another session's goal, which is
  acceptable only while expand can recover the original. With marker_mode summary/off
  (or a non-persisting store) the global cache falls back to same-session reuse, on
  both the read and write side. Verified deletion-only results are always global.

Gates: build, test -tags cg_skeleton, -race -count=5, gofmt -l (clean), go vet.

Refs #28

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
Only the SAME-SESSION replay may bypass the tail gate. That session already sent
the compacted bytes, so the provider's cached prefix holds the compacted form and
replaying is byte-identical at any depth. A cross-session global hit has no such
guarantee: the receiving session never compacted that content, so its cached
prefix holds the ORIGINAL, and splicing another session's result at depth mutates
already-cached content and forces a suffix re-write at 11.5x the read price --
exactly the harm #40 removed repairLostResult to prevent.

Restore the ordering: getResult, then the tail gate, then getResultGlobal only for
messages the gate permits. A global hit is then frozen into the session so later
turns replay it from the depth-safe path. TestGlobalCacheHitIsNotSplicedAtDepth
fails if the ordering is flattened again; name that invariant at the call site.

Also adapt to #40's unified result-cache key and document why the global namespace
must keep the pair together too: splitting projected text and summary across two
global keys would re-create the half-a-decision bug cross-session, where
independent TTLs let a hit on one and a miss on the other emit projected text with
the summary segment silently gone.

Add TestNoDefaultConfigRunsExtractLLMOnCachingBackend, which drives the bare
defaults and the codesmart preset end to end with an output far above the cached
break-even, so the economics alone would permit the call. It fails if
allow_on_caching_backend or the allowCached wiring is ever lost in a rebase.

Update the measured verdict: the 197,548 saved tokens sit in the cached prefix, so
at cache-read price they are worth $0.0395 against $3.26 and 1,592,467 ms -- 82x
underwater, not the 8x in the improvement plan, which priced them as fresh input.

Assisted-By: Claude Opus 5
Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
… used

The improvement plan's ~8x figure implicitly priced the 197,548 saved tokens as
fresh input. They sat in the cached prefix, so they bill at the cache-read rate:
$0.0395 against $3.26 and 1,592,467 ms of blocking time -- 82x underwater. A
later Terminal-Bench arm that excluded the component entirely re-derived this
independently. Name which cache-read rate each figure uses, since the gate reasons
at $0.30/MTok (55x) while the issue quoted $0.20/MTok (82x); the gate's is the
more generous of the two, so the shipped decline is the conservative one.

Assisted-By: Claude Opus 5
Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

Verified independently before merge

Ordering fix confirmed by reading the merged tree, not just the description — components/offload/extract_llm.go: same-session getResult at :343, tail gate at :365, getResultGlobal at :403, i.e. inside the gate. That is the invariant my resolution flattened.

Both guard tests mutation-verified myself. The AllowOnCachingBackend one is the important addition, because it closes the exact gap that let me nearly ship a regression:

allowCached: allowCached || true    // simulate losing the guard in a rebase
--- FAIL: TestNoDefaultConfigRunsExtractLLMOnCachingBackend/codesmart

Your diagnosis of why the old coverage was insufficient is right and worth restating: evaluateGate was unit-tested directly, so the gate logic was proven while the wiring to it was not. I lost AllowOnCachingBackend in my own rebase attempt and nothing failed. Driving the bare defaults and the codesmart preset end to end with a ~240k-token output — above the break-even, so economics alone would permit the call and only the hard decline can suppress it — is exactly the right shape for that test.

Also good catch that the rebase was never actually finished: the branch sat on #42 while main had moved five merges ahead. I'd assumed it was current.

Full gates re-run on the final tree: build, go test -tags cg_skeleton ./... (20/20), gofmt -l clean.

On your two judgement calls — both right

The rate inconsistency: documenting both is the correct choice. 82× at $0.20/MTok, 55× at $0.30/MTok, with the gate reasoning on the more generous rate so the shipped decline is the conservative one. Silently mixing them would have been the failure mode; picking the bigger number for the headline would have been worse. Either figure is an order of magnitude past break-even, so the conclusion doesn't depend on the choice — which is itself worth saying, because a conclusion that survives both plausible parameterisations is stronger than one tuned to a rate.

Replacing the stale "clear win: net +$0.0287" was right, not overreach. That figure was produced by the H3 attribution bug and I'd already retracted it in the fix-round comment; leaving it in the body while the doc said otherwise would have shipped a contradiction. Marking it as the third result traced to a measurement artifact is the honest framing.

That count now matters beyond this PR: three of this component's results were measurement artifacts, and all three pointed the same way — making the component look better than it is. That pattern is why the final Terminal-Bench arm excluded extract_llm outright rather than trusting the gate to suppress it.

What this PR now establishes

On prompt-caching backends extract_llm cannot pay for itself — break-even needs ~30,500 tokens/output against a largest-observed 2,053, a 15× structural gap — and it now makes zero calls and costs nothing there, enforced in code rather than a doc note. On non-caching traffic the gate is strictly better in every arm (68% less waste, 26 calls → 1, latency halved) but still only reaches break-even on available captures.

A negative result, honestly measured, with the component disabled where it loses money and instrumented so the next person can tell. Merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

perf(extract_llm): prompt-cache the preamble, global result cache, economic gate, and value-based triggering (measured ~8x underwater)

2 participants