perf(model): cache all ollama completions in one bounded client middleware (JEF-362) - #189
Merged
thejefflarson merged 1 commit intoJul 6, 2026
Conversation
…eware (JEF-362) Move completion caching from the per-consumer verdict cache (JEF-350) to the model-client boundary, so EVERY model consumer is covered by one mechanism. The hypothesis stage (ModelHypothesizer) previously called ollama every pass, uncached; routing chat() through a shared bounded cache fixes it once. - Key = stable hash of the full request that determines the response (endpoint + model + messages + temperature + max_tokens), canonicalized with sorted object keys (array order preserved) so a byte-stable request always hits. - Bounded in-memory LRU, cap default 512, env PROTECTOR_MODEL_CACHE_ENTRIES. Mutex<LruCache>; the lock is never held across the HTTP await (get/put are tiny sync critical sections), so the cache sits in front of the JEF-337 concurrent dispatch without reintroducing serialization. - Cache successes only: transport errors, non-success statuses, over-cap bodies, and unparseable replies are never cached — they retry next pass. - keep_warm bypasses the cache by construction (caching a keep-alive ping would let the model unload). - JEF-350's deterministic verdict prompt + decisive-verdict journal persistence are kept intact for restart survival; this is the in-memory general cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
thejefflarson
enabled auto-merge (squash)
July 6, 2026 15:24
thejefflarson
deleted the
thejefflarson/jef-362-cache-all-ollama-calls-in-a-bounded-model-client-middleware
branch
July 6, 2026 15:24
thejefflarson
added a commit
that referenced
this pull request
Jul 6, 2026
…in-pinning hazard (JEF-364) (#191) JEF-362/#189 added a bounded LRU over `chat()`. After JEF-363 removed the model-backed hypothesis stage, the adjudicator is the sole `chat()` caller (keep_warm bypasses). The JEF-350 verdict cache already keys on the exact prompt hash, so the LRU is redundant by construction: a verdict-cache HIT means `chat()` is never called, and a MISS means the prompt changed so the LRU misses too. Worse, it was a live correctness hazard — the verdict store deliberately never caches `Uncertain` (must retry, JEF-234 backoff), but the LRU cached any 200 including replies that parse to `Uncertain`, pinning the entry to a stale completion until evidence changed. - Delete engine/src/engine/model/cache.rs and its tests. - Remove the mod/imports and cache::get/put calls from model.rs; `chat()` calls ollama directly again. - Remove the now-unused `lru` dependency. - Fix the stale module doc (no more "hypothesis source" / "frontier gateway"; the adjudicator is the sole consumer, keep_warm is a bypass ping). - Add a test that a transient `Uncertain` reply does not stick: an identical prompt is re-sent to the endpoint next pass (no cached completion short-circuits the retry). Verdict cache + journal (JEF-350) and keep_warm behavior untouched. Closes JEF-364 Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thejefflarson
added a commit
that referenced
this pull request
Aug 8, 2026
…#332) Process/review discipline, no engine code: when a PR removes a consumer of a shared layer or abstraction, its description states the layer's remaining live-consumer count in one line; a drop-to-one is flagged in review as a refactor smell, not merged silently. Concrete precedent recorded as Context: PR #189 added a shared model-completion cache "so EVERY model consumer is covered"; PR #190 merged eight minutes later and deleted one of its two consumers (the hypothesis stage); the conjunction — invisible to either single-PR review — left a redundant, Uncertain-pinning cache that PR #191 had to revert less than an hour later. Extends ADR-0024's single-PR redundant-by-construction rule across a two-PR conjunction. Adds a one-line CLAUDE.md Workflow checklist entry pointing at the ADR, and a docs/adr/README.md index row. Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes JEF-362
What
Moves completion caching from the per-consumer verdict cache (JEF-350) to the model-client boundary in
engine/src/engine/model.rs, so every model consumer is covered by ONE bounded mechanism instead of a cache per consumer.chat()now checks a shared cache before the HTTP call and stores only successful completions.Middleware shape + key
DefaultHasher(SipHash) overendpoint+ the request body, canonicalized with sorted object keys (array/message order preserved). The body carries everything that determines the response: model name, messages/prompt,temperature,max_tokens. So only a byte-identical request hits.Bounding + env
Mutex<LruCache<u64, String>>, capacity default 512, overridable viaPROTECTOR_MODEL_CACHE_ENTRIES(unset / unparseable /0→ default).cache::get/cache::put; it is never held across the HTTPawait, so the cache sits in front of the JEF-337 concurrent dispatch without reintroducing serialization.Cache successes only
Transport errors, non-success statuses (500/502/503), over-cap bodies, and unparseable/shapeless replies all short-circuit to
Nonebefore the store — they are never cached and retry next pass.keep_warm bypass
keep_warmdoes not route through the cache (it never callschat), so a keep-alive ping always hits the wire — caching it would let the model unload. Covered by a test.Kept intact
JEF-350's deterministic verdict prompt and the decisive-verdict journal persistence (restart survival) are untouched; this is the in-memory general cache, not a replacement for the durable journal.
Scope note (JEF-363)
Per coordinator direction, the model-hypothesis stage is being removed by JEF-363, leaving adjudication as the only ollama consumer. This middleware is intentionally consumer-agnostic — it now primarily serves adjudication and backstops any future consumer. All edits here are confined to
model.rs+model/cache.rs; no changes tohypothesis.rs,run_loop.rs,mod.rs, orreason/proof.Tests
New tests (12): request-canonicalization key stability + sensitivity (prompt/model/temperature/endpoint/message-order), env parsing, LRU eviction at cap, and end-to-end through
chatagainst a connection-counting localhost server — identical request served from cache with zero HTTP, distinct request misses, error not cached (retries),keep_warmbypass (always hits), and concurrent identical requests don't deadlock.cargo fmt/cargo clippy --all-targets(clean) /cargo nextest run→ 739 passed, 1 skipped (incl. the file-size guard; no file over 1000 lines).🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP