feat: add opt-in exact-match caching for LLM responses - #404
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds an opt-in exact-match LLM response cache with canonical keying, TTL-backed in-memory or Redis storage, buffered and streaming interception, provider-native replay, telemetry, configuration validation, gateway handling, and Rust, Go, Python, and Node APIs. ChangesResponse cache feature
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Gateway
participant ResponseCacheIntercept
participant CacheStore
participant Provider
Client->>Gateway: submit LLM request
Gateway->>ResponseCacheIntercept: execute buffered or streaming request
ResponseCacheIntercept->>CacheStore: lookup canonical key
alt cache hit
CacheStore-->>ResponseCacheIntercept: cached response aggregate
ResponseCacheIntercept-->>Gateway: return response or replay chunks
else cache miss
ResponseCacheIntercept->>Provider: execute upstream request
Provider-->>ResponseCacheIntercept: response or stream
ResponseCacheIntercept->>CacheStore: store eligible result
ResponseCacheIntercept-->>Gateway: return live response or stream
end
Gateway-->>Client: response with appropriate content type
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
License DiffCompared against Lockfile license changesLockfile License ChangesRustAdded
Removed
Updated/Changed
NodeAdded
Removed
Updated/Changed
PythonAdded
Removed
Updated/Changed
Status output |
4b575ad to
22b5fdb
Compare
22b5fdb to
17520f9
Compare
17520f9 to
043afc6
Compare
043afc6 to
b149846
Compare
b149846 to
7253a68
Compare
7253a68 to
4363be4
Compare
4363be4 to
83db5c1
Compare
An opt-in feature of the adaptive plugin (a response_cache config section, not a new plugin kind): managed LLM calls are keyed by a SHA-256 fingerprint of the normalized request and repeats are served from the store — instant, free, reproducible. Buffered and streaming calls share one keyspace; a streamed miss is teed and stored as its aggregated response, a hit replays provider-native chunks. Keying auto-detects the provider surface from the request shape and trusts the decode only where it is faithful: known-lossy shapes and decodes that fail to round-trip fall back to raw-body fingerprinting, which can only cost a miss. Stateful calls (Responses persistence, conversations, containers) and nondeterministic calls under the safety toggle bypass entirely. Stored answers must be complete: non-null errors, non-final statuses, truncated or lossily-collected streams, and upstream failures are never cached — the CLI gateway relays failed upstream replies to the client verbatim while keeping them invisible to the execution chain. The in-memory store is bounded by an honest resident-size budget with oldest-first eviction; the Redis backend runs under hard deadlines and re-checks entry expiry. Everything fails open: any cache error falls through to a live call. Same config surface in Rust, Python, Node, and Go; doctor reports configuration and backend reachability; hit/miss/ bypass marks carry fingerprints and savings, never bodies. Signed-off-by: Zhongxuan Wang <daniewang@nvidia.com>
83db5c1 to
96df1d9
Compare
willkill07
left a comment
There was a problem hiding this comment.
One issue found that should be addressed.
Signed-off-by: Zhongxuan Wang <daniewang@nvidia.com>
Signed-off-by: Zhongxuan Wang <daniewang@nvidia.com>
Signed-off-by: Zhongxuan Wang <daniewang@nvidia.com>
Signed-off-by: Zhongxuan Wang <daniewang@nvidia.com>
|
/ok to test 5975aa6 |
|
/merge |
#### Overview > **Documents the feature in #404.** This branch is synced with current `main`, so the diff is documentation-only and can be reviewed independently. It should merge after #404 so the documentation does not land before the feature. Documents the opt-in exact-match response cache, its safety boundaries, binding APIs, streaming publication model, and managed-gateway integration. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Adds the Adaptive response-cache guide with `plugins.toml`, Python, Node.js, and Rust examples plus manual lifecycle APIs. - Documents the mandatory trust-domain namespace, fixed audited noise fields, response-affecting key partitions, header restrictions, and canonicalization safety bypass. - Documents eligibility, replay fidelity, TTL and eviction semantics, streaming write-behind behavior, lack of single-flight coordination, fail-open behavior, observability, and unredacted storage. - Updates the Adaptive entry points and crate README. - Calls out the intentional Rust source break for exhaustive `AdaptiveConfig` struct literals in the 0.7 migration guide. - Validated with `just docs`, `just docs-linkcheck`, targeted pre-commit, and the full pre-commit hook set. Docs passed with zero errors; the authenticated redirects check was skipped because `FERN_TOKEN` is not present. #### Where should the reviewer start? `docs/configure-plugins/adaptive/response-cache.mdx` — the complete behavior, safety, and configuration contract. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Relates to #404 ## Summary by CodeRabbit * **Documentation** * Added guidance for enabling and configuring optional response caching for repeated LLM requests. * Documented cache behavior, including TTLs, request matching, streaming replay, bypass conditions, and failure handling. * Added details on cache keys, observability signals, configurable settings, backend requirements, and validation warnings. * Updated Adaptive configuration and navigation guidance to include the new Response Cache documentation. * Documented in-memory and Redis-backed storage options, including Redis setup for shared persistence. * Clarified gateway handling for provider payloads, lifecycle events, streaming responses, errors, and retry behavior. Authors: - Zhongxuan (Daniel) Wang (https://github.com/zhongxuanwang-nv) - Will Killian (https://github.com/willkill07) Approvers: - Will Killian (https://github.com/willkill07) URL: #405
#### Overview ACG includes the first non-system message in its learning key. A workflow whose system prompt, tool schemas, and output contract never change is therefore split into a separate profile for every distinct first user task, so observations never accumulate and the stable scaffold is never recognized as a reusable prefix. This change buckets learning by the stable system/tool/structured-output scaffold, preserves the prior task seed for requests without one, and gates reuse on a fingerprint bound to the exact scaffold key. Scope is the three items assessed as meaningful in the [#323 review](#323 (comment)): scaffold-aware keying, a stable-prefix fingerprint guard, and the structured-output contract in `PromptIR`. No governor, drift detector, convergence machinery, topology state, configuration surface, or binding change. Nothing is added to Python, Node, Go, WebAssembly, or C FFI. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details 5 source files (+432/-51), 18 test files (+938/-35). - The learning key derives from the stable scaffold. Requests carrying a system prompt, tool schemas, or a structured-output contract bucket under `seed=stable-scaffold`; requests without one keep the prior task-seed behavior. - `structured_output_schema_id` is populated and the canonicalized `response_format` becomes a scaffold block ahead of the first non-system message, so an unchanging output contract sits inside the stable prefix. - Fingerprints bind to the exact scaffold key. A prefix stopping inside the scaffold binds to that scaffold; one extending past it binds to the complete source request, because the normalized IR retains no lossless provider-prefix representation. - The gate fails closed on legacy, missing, shorter, mismatched, and beyond-scaffold-without-a-source-request fingerprints. - `find_stable_prefix_length` is removed. This change replaces its only caller and nothing else in the workspace used it. #### Learning-key fragmentation on `main` Control: `main` at `2ab7a070`, no source change. Two requests sharing a system prompt and tool schema, differing only in the first user task, key as: ```text agent-a::model=gpt-4o::seed=user:sha256:d006::system=sha256:1a842e58b::tools=sha256:69848c057 agent-a::model=gpt-4o::seed=user:sha256:5607::system=sha256:1a842e58b::tools=sha256:69848c057 ``` `system` and `tools` are identical; only `seed` differs, so the two runs land in separate profiles and neither accumulates enough observations to produce a reusable prefix. On this head the pair produces one key with `seed=stable-scaffold`. #### Reuse determinism A profile persisted by one process is read back by another, so the analysis must not depend on the per-process seed Rust draws for `HashMap` iteration. Over `N` processes producing outcomes with multiplicities `c_1..c_k`, the cross-process agreement rate is ```text A = sum_i c_i (c_i - 1) / (N (N - 1)) ``` the probability two independent processes agree, and equivalently the probability that a persisted profile satisfies the reuse gate elsewhere. `A = 1` exactly when the analysis is seed-independent. Measured over `N = 20` processes on one 20-observation window whose first turn varies in 1 of 20 runs, with four tool spans sharing a sequence index: | Rule | Span order `A` | Prefix length `A` | Fingerprint `A` | Prefix | |---|---|---|---|---| | Per-span run on `sequence_index` (`main`) | 0.000 | 0.479 | 0.479 | `1` ×9, `2` ×11 | | Per-span run on `(index, rank, span id)` | 1.000 | 1.000 | 1.000 | `1` | | Exact prefix mass | 1.000 | 1.000 | 1.000 | `6` | `main` derives the prefix as the leading run of per-span scores, then re-sequences those scores by first-seen index. That index does not order the span set: span ids carry the role and tool suffix, so `assistant-1-search` and `assistant-1-fetch` are distinct spans at index 1, and the index is a minimum across observations. At `A = 0.479`, better than half of cross-process profile reads miss. Two changes follow. Score ordering extends to `(index, stability rank, span id)`, injective because span ids are unique within one analysis. Prefix economics walks that vector and stops pricing at the first non-stable span, so ranking the least stable first ends the priced prefix at a contested position instead of pricing across it. The prefix length no longer reads that vector. It descends the prefix tree of block sequences while one child holds the configured share, which is the rule named in the #323 review: the same exact prefix observed for N samples. Descent also requires a strict majority, which two disjoint children cannot both hold, so the dominant child is unique where it exists and no tie-break is reachable; where none dominates, the prefix stops. Equivalently, under `d(x, y) = 2^-lcp(x, y)` the window is ultrametric, and every point of a closed ball is a center, so the ball is fixed by its members rather than by traversal order. That also recovers reuse. On this window 19 of 20 observations share an exact 6-block prefix; the per-span rule reports 1 because the minority span at index 1 stops the run. The outlier still fails the fingerprint gate, so the wider prefix stays exactly validated. #### Test coverage Variable first-user tasks under one scaffold; changed system prompts, tool schemas, and output contracts; canonical structured output; whitespace changes hidden by normalized IR; prefixes beyond the scaffold; legacy state with no fingerprint; same-run aggregation; fresh-cache rehydration; Redis restart; serialization compatibility. Adversarial cases: an interleaving storage backend that yields between the learner's load and store halves shows concurrent runs never pair a fingerprint with a foreign observation window; two workflows sharing a system prompt and anchor turn collapse to one key and then fail closed once the prefix extends past the scaffold; every rotation of one span set produces a single canonical ordering without depending on a hash seed. The Redis restart test previously read its learning key out of the hot cache, so it passed for whichever key `process_run` wrote — and `process_run` also persists under the plain agent id for rehydration, so a mis-keyed scaffold record could have satisfied it. It now asserts the key is not the bare agent id and carries the stable-scaffold marker before reloading. #### Validation PR head `25c771e7` on `main` at `2ab7a070`, Windows 11, rustc 1.97.1, go 1.26.1: | Suite | Result | |---|---| | `cargo test -p nemo-relay-adaptive` | 538/538 lib, 8/8 surface, 36/36 response cache, 4/4 response-cache bench, 12/12 runtime integration, 1 doctest passed and 3 ignored | | `NEMO_RELAY_RUN_REDIS_TESTS=1 … --features redis-backend --test redis_integration` | 11/11 against Redis 8.0.5 on `127.0.0.1:6379` | | `just test-python` | 609 passed, 5 skipped | | `just test-node` | 342 passed, 2 skipped, 0 failed of 344 | | `cargo clippy -p nemo-relay-adaptive -p nemo-relay-python --all-targets -- -D warnings` | clean | | `cargo fmt --all -- --check` | clean | The 36 response-cache and 4 response-cache benchmark tests cover the opt-in exact-match LLM response cache from #404 and pass unchanged on this head. #### Limits No provider-cache, cost, or latency improvement is claimed. The analysis-only trace report, the three-arm interleaved experiment, and the provider cache-read/cache-write, billed-cost, parity, and p50/p95/p99 evidence requested in points 1 through 3 of the #323 review are not supplied here; this diff is the internal keying and validation change those points would measure. An earlier interleaved run showed the fragmentation directly (144 keys and 0 hints versus 1 key and 120 hints) but used an older `main` and a benchmark overlay, so it is not offered as evidence for this diff. The exact-prefix-mass rule widens prefixes relative to `main`, which is a behavior change and not only a determinism fix; reuse remains gated on the exact fingerprint. Two suites did not run on this host: `just test-go` requires `clang`/`lld`, and full-workspace `just test-rust` reaches unrelated CLI filesystem failures on Windows. CI provides the authoritative Linux matrix. Persisted JSON stays backward-compatible through an optional fingerprint field, which is source-visible to downstream Rust constructing `StabilityAnalysisResult` with a struct literal. #### Where should the reviewer start? `crates/adaptive/src/acg_profile.rs` for the learning-key boundary, then `crates/adaptive/src/acg/stability.rs` for the prefix and ordering rules and `crates/adaptive/src/acg_component.rs` for the gate. Focused tests are in `crates/adaptive/tests/unit/acg_profile_tests.rs`, `crates/adaptive/tests/unit/acg/stability_internal_tests.rs`, and `crates/adaptive/tests/unit/acg_component_tests.rs`. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Relates to #323. - Replaces the scaffold-keying and exact-validation core of #322. The convergence, governor, drift, and binding scope rejected there is absent. Authors: - Teerth Sharma (https://github.com/teerthsharma) - Will Killian (https://github.com/willkill07) Approvers: - Will Killian (https://github.com/willkill07) URL: #481
#### Overview Adds opt-in tool-result caching to Adaptive response caching. A cache hit suppresses the real tool call, so caching remains disabled by default and only explicitly classified read-only, TTL-stable tools are eligible. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Adds default, class, and per-tool override policies with exact/wildcard resolution, TTL, bypass rate, argument skips, and tool-version keying. - Shares the existing cache store while keeping tool and LLM keys disjoint; key and store failures fail open to a live tool call. - Adds Rust, Python, Node.js, and Go configuration parity, validation, doctor diagnostics, saved-invocation marks, and focused integration/benchmark coverage. - Keeps the feature diff focused: user-facing tool-cache documentation remains outside this PR, and only essential API/safety comments are retained. - Is based directly on NVIDIA/NeMo-Relay `main` at `b467deae`; this branch reconciles with the merged canonical tool-execution-result API while preserving annotations on cache hits. Earlier logical-key and staged documentation changes are not part of this diff. - Breaking changes: none. - Validation: `just ci=true test-rust` (3,717 passed), response-cache unit tests (61 passed), response-cache integration tests (50 passed), Python (613 passed), Node.js (342 passed), Go, workspace clippy with warnings denied, and all repository pre-commit hooks pass. The canonical non-CI Rust run also completed all unit/integration tests but encounters the existing upstream `scope_stack.rs` doctest reference to unavailable `nemo_relay::Result`. #### Where should the reviewer start? Start with `crates/adaptive/src/response_cache/tool.rs` for policy resolution and fail-open execution, then `crates/adaptive/tests/integration/response_cache_tests.rs` for the behavioral contract. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Relates to #404 ## Summary by CodeRabbit * **New Features** * Added opt-in caching for tool results alongside exact-match LLM response caching. * Added configurable tool policies for classes, overrides, TTLs, argument exclusions, bypass rates, priorities, and optional error caching. * Exposed tool-cache configuration across Python, Node.js, and Go APIs. * Added diagnostics for tool-cache status and configuration issues. * **Bug Fixes** * Improved cache-key normalization and validation for tool arguments, headers, wildcard rules, and expired entries. * Improved handling of cache failures and malformed streaming responses. Authors: - Zhongxuan (Daniel) Wang (https://github.com/zhongxuanwang-nv) Approvers: - Will Killian (https://github.com/willkill07) - Bryan Bednarski (https://github.com/bbednarski9) URL: #597
Overview
First implementation stage of a five-stage response-cache series (exact match → logical keys → tool-result cache → semantic → streaming semantic), with the standalone documentation in #405. This stage is independently reviewable.
Adds an opt-in, exact-match response cache for managed LLM calls as a
response_cachesection of the Adaptive plugin config. Repeated eligible calls can be served from an in-memory or Redis store instead of re-running the provider. The feature is off until the section is present; by default, only explicitly deterministic requests (temperature = 0) are cacheable. Runtime backend failures fail open to a live call, while invalid configuration is rejected.Details
crates/adaptive/src/response_cache/key.rs): keys use a SHA-256 fingerprint of the RFC 8785-canonicalized, normalized request. Provider surface detection and guarded raw-body fallback avoid incorrect reuse when a decode would be lossy. Provider, namespace, and schema version are folded into the key; only allowlisted headers participate.crates/adaptive/src/response_cache/intercept.rs): incomplete, error, stateful, unparseable, or otherwise unsafe calls bypass or remain unstored. Nondeterministic requests bypass unlesscache_nondeterministic = trueis set explicitly.crates/adaptive/src/response_cache/store.rs): bounded in-memory storage and feature-gated Redis storage with expiry and operation deadlines. Redis initialization/storage failures disable or bypass the optional cache rather than blocking managed calls.crates/cli/src/gateway): upstream failures remain verbatim for the client but are not cacheable; post-success runtime rejections still surface as errors. Buffered and streaming cache hits use the correct content type.cache_nondeterministic = false) and validation behavior.nemo-relay doctorreports configuration and backend reachability.response_cachehit/miss/bypass marks include fingerprints, reasons, and savings without request or response bodies.Validation completed on the final branch head:
RUST_TEST_THREADS=1 just test-rustcargo clippy --workspace --all-targets -- -D warningsjust test-python— 542 passedjust test-node— 286 passedjust test-gouv run pre-commit run --all-files— all hooks pass on the final treeWhere should the reviewer start?
crates/adaptive/src/response_cache/key.rs— key derivation and guarded normalization.crates/adaptive/tests/integration/response_cache_tests.rs— end-to-end eligibility, replay, and stream-lifecycle behavior.crates/cli/src/gateway— the upstream-failure and cached-response boundary.Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)
Summary by CodeRabbit