Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
96df1d9
feat(response-cache): opt-in exact-match LLM response cache
zhongxuanwang-nv Jul 10, 2026
3b2f565
Merge remote-tracking branch 'upstream/main' into feat/response-cache…
zhongxuanwang-nv Jul 16, 2026
813e711
fix(response-cache): address code review findings
zhongxuanwang-nv Jul 16, 2026
4d0d5e3
Merge upstream/main into feat/response-cache-stage1
zhongxuanwang-nv Jul 21, 2026
be0df9a
Merge upstream/main into feat/response-cache-stage1
zhongxuanwang-nv Jul 22, 2026
e2a0c79
fix(response-cache): address review feedback
zhongxuanwang-nv Jul 22, 2026
d7205ce
fix(response-cache): address remaining review feedback
zhongxuanwang-nv Jul 27, 2026
316d77a
fix(response-cache): address CodeRabbit review findings
zhongxuanwang-nv Jul 28, 2026
6521a5b
fix(response-cache): bound Redis initialization and probes
zhongxuanwang-nv Jul 29, 2026
4a2a594
fix(response-cache): remove unsafe skip keys
zhongxuanwang-nv Jul 29, 2026
84c69d6
fix(go): preserve response cache defaults
zhongxuanwang-nv Jul 29, 2026
0f73b58
fix(response-cache): require explicit trust boundary
zhongxuanwang-nv Jul 30, 2026
b8911da
fix(gateway): isolate concurrent upstream attempts
zhongxuanwang-nv Jul 30, 2026
8d614b1
merge: sync response cache branch with main
zhongxuanwang-nv Jul 30, 2026
87a283b
fix(response-cache): preserve routing and response fidelity
zhongxuanwang-nv Jul 30, 2026
c62b2c7
fix: preserve cache partitions and provider failures
zhongxuanwang-nv Jul 30, 2026
601c28e
Merge upstream/main into feat/response-cache-stage1
zhongxuanwang-nv Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion crates/adaptive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ chrono = { version = "0.4", features = ["serde"] }
thiserror = "2"
tdigest = { version = "0.2", features = ["use_serde"] }
tokio = { version = "1", features = ["rt", "macros", "sync", "time"] }
tokio-stream = { version = "0.1", default-features = false, features = ["sync"] }
regex = "1"
serde_json_canonicalizer = "0.3"
sha2 = "0.11"
Expand All @@ -35,7 +36,6 @@ redis-backend = ["redis"]

[dev-dependencies]
tokio = { version = "1", default-features = false, features = ["rt", "macros", "sync", "time", "test-util", "rt-multi-thread"] }
tokio-stream = { version = "0.1", default-features = false }

[[test]]
name = "redis_integration"
Expand All @@ -48,3 +48,11 @@ path = "tests/integration/runtime_integration_tests.rs"
[[test]]
name = "acg_module_surface"
path = "tests/integration/acg_module_surface_tests.rs"

[[test]]
name = "response_cache_integration"
path = "tests/integration/response_cache_tests.rs"

[[test]]
name = "response_cache_benchmark_integration"
path = "tests/integration/response_cache_benchmark_tests.rs"
82 changes: 82 additions & 0 deletions crates/adaptive/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use nemo_relay::plugin::ConfigPolicy;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value as Json};

use crate::response_cache::config::{BackendConfig, KEY_STRATEGY_EXACT_REQUEST};

/// Canonical config document for the adaptive plugin component.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptiveConfig {
Expand All @@ -32,6 +34,10 @@ pub struct AdaptiveConfig {
/// Adaptive Cache Governor settings.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acg: Option<AcgComponentConfig>,
/// Opt-in LLM response cache (exact-match). When present, the
/// adaptive plugin installs the response-cache execution intercept(s).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_cache: Option<ResponseCacheConfig>,
/// Adaptive-local unsupported-config policy.
#[serde(default)]
pub policy: ConfigPolicy,
Expand All @@ -47,6 +53,7 @@ impl Default for AdaptiveConfig {
adaptive_hints: None,
tool_parallelism: None,
acg: None,
response_cache: None,
policy: ConfigPolicy::default(),
}
}
Expand Down Expand Up @@ -184,6 +191,56 @@ impl Default for AcgComponentConfig {
}
}

/// Configuration for the adaptive plugin's `response_cache` feature
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ResponseCacheConfig {
/// How long a stored answer stays reusable, in seconds.
pub ttl_seconds: u64,
/// Required, non-empty trust-domain partition folded into every key.
///
/// One response-cache namespace must not span mutually untrusted tenants
/// or upstreams. The empty default is an unconfigured sentinel rejected
/// when the response-cache section is enabled.
pub namespace: String,
/// Execution-intercept priority; lower runs first/outermost (default `50`).
pub priority: i32,
/// Probability in `[0.0, 1.0]` of skipping the cache and running live.
pub bypass_rate: f64,
/// Cache nondeterministic requests too. Set `false` to cache only
/// requests explicitly pinned deterministic (`temperature` = 0) — absent
/// or unreadable temperatures count as nondeterministic.
pub cache_nondeterministic: bool,
/// Key strategy. Only [`KEY_STRATEGY_EXACT_REQUEST`] is supported.
pub key_strategy: String,
/// Request headers (case-insensitive) folded into the key; never auth headers.
pub header_allowlist: Vec<String>,
/// Storage backend selection.
pub backend: BackendConfig,
}

impl Default for ResponseCacheConfig {
fn default() -> Self {
Self {
ttl_seconds: 3600,
namespace: String::new(),
priority: 50,
bypass_rate: 0.0,
cache_nondeterministic: false,
key_strategy: KEY_STRATEGY_EXACT_REQUEST.to_string(),
header_allowlist: Vec::new(),
backend: BackendConfig::default(),
}
}
}

impl ResponseCacheConfig {
/// TTL as a [`std::time::Duration`].
pub fn ttl(&self) -> std::time::Duration {
std::time::Duration::from_secs(self.ttl_seconds)
}
}

fn default_adaptive_config_version() -> u32 {
1
}
Expand Down Expand Up @@ -254,6 +311,13 @@ nemo_relay::editor_config! {
nested: AcgComponentConfig,
default: AcgComponentConfig,
},
response_cache => {
label: "response_cache",
kind: Section,
optional: true,
nested: ResponseCacheConfig,
default: ResponseCacheConfig,
},
policy => {
label: "policy",
kind: Section,
Expand Down Expand Up @@ -326,6 +390,24 @@ nemo_relay::editor_config! {
}
}

nemo_relay::editor_config! {
impl ResponseCacheConfig {
ttl_seconds => { label: "ttl_seconds", kind: Integer },
namespace => { label: "namespace", kind: String },
priority => { label: "priority", kind: Integer },
bypass_rate => { label: "bypass_rate", kind: Float },
cache_nondeterministic => { label: "cache_nondeterministic", kind: Boolean },
key_strategy => { label: "key_strategy", kind: String },
header_allowlist => { label: "header_allowlist", kind: Json },
backend => {
label: "backend",
kind: Section,
nested: BackendConfig,
default: BackendConfig,
},
}
}

nemo_relay::editor_config! {
impl crate::acg::stability::StabilityThresholds {
stable_threshold => { label: "stable_threshold", kind: Float },
Expand Down
7 changes: 5 additions & 2 deletions crates/adaptive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub mod learner;
pub mod plugin_component;
#[cfg(feature = "redis-backend")]
pub mod redis;
/// Opt-in LLM response cache (exact-match).
pub mod response_cache;
mod runtime;
/// Storage backends and backend traits for adaptive state persistence.
pub mod storage;
Expand All @@ -45,8 +47,8 @@ pub mod trie;
pub mod types;

pub use config::{
AcgComponentConfig, AdaptiveConfig, AdaptiveHintsComponentConfig, BackendSpec, StateConfig,
TelemetryComponentConfig, ToolParallelismComponentConfig,
AcgComponentConfig, AdaptiveConfig, AdaptiveHintsComponentConfig, BackendSpec,
ResponseCacheConfig, StateConfig, TelemetryComponentConfig, ToolParallelismComponentConfig,
};
pub use context_helpers::{
LATENCY_SENSITIVITY_POINTER, extract_scope_path, read_manual_latency_sensitivity,
Expand All @@ -55,6 +57,7 @@ pub use context_helpers::{
pub use error::{AdaptiveError, Result};
#[cfg(feature = "redis-backend")]
pub use redis::RedisBackend;
pub use response_cache::RESPONSE_CACHE_MARK;
pub use runtime::features::AdaptiveRuntime;
pub use storage::erased::AnyBackend;
pub use storage::memory::InMemoryBackend;
Expand Down
65 changes: 65 additions & 0 deletions crates/adaptive/src/plugin_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ fn validate_adaptive_plugin_config_with_policy(
"adaptive_hints",
"tool_parallelism",
"acg",
"response_cache",
"policy",
],
);
Expand Down Expand Up @@ -302,10 +303,74 @@ fn validate_adaptive_plugin_config_with_policy(
);
}

if let Some(response_cache_json) = plugin_config
.get("response_cache")
.and_then(Json::as_object)
{
validate_unknown_fields(
&mut diagnostics,
&config.policy,
Some("response_cache".to_string()),
response_cache_json,
&[
"ttl_seconds",
"namespace",
"priority",
"bypass_rate",
"cache_nondeterministic",
"key_strategy",
"header_allowlist",
"backend",
],
);
if let Some(backend_json) = response_cache_json.get("backend").and_then(Json::as_object) {
validate_unknown_fields(
&mut diagnostics,
&config.policy,
Some("response_cache.backend".to_string()),
backend_json,
&["kind", "config"],
);
let backend_kind = backend_json
.get("kind")
.and_then(Json::as_str)
.unwrap_or("in_memory");
if let Some(backend_config_json) = backend_json.get("config").and_then(Json::as_object)
{
validate_response_cache_backend_config_fields(
&mut diagnostics,
&config.policy,
backend_kind,
backend_config_json,
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

diagnostics.extend(AdaptiveRuntime::validate_config(&config).diagnostics);
diagnostics
}

fn validate_response_cache_backend_config_fields(
diagnostics: &mut Vec<ConfigDiagnostic>,
policy: &ConfigPolicy,
backend_kind: &str,
backend_config: &Map<String, Json>,
) {
let known_fields: &[&str] = match backend_kind {
"in_memory" => &["max_bytes"],
"redis" => &["url", "key_prefix"],
_ => return,
};
validate_unknown_fields(
diagnostics,
policy,
Some(format!("response_cache.backend.{backend_kind}")),
backend_config,
known_fields,
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn validate_backend_config_fields(
diagnostics: &mut Vec<ConfigDiagnostic>,
policy: &ConfigPolicy,
Expand Down
92 changes: 52 additions & 40 deletions crates/adaptive/src/redis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,57 @@ use crate::trie::serialization::TrieEnvelope;
use crate::types::plan::ExecutionPlan;
use crate::types::records::RunRecord;

/// Connect to Redis, returning the client and an auto-reconnecting
/// [`ConnectionManager`].
///
/// # Errors
///
/// Returns [`AdaptiveError::Storage`] if the client cannot be created or the
/// connection cannot be established.
pub(crate) async fn connect(url: &str) -> Result<(Client, ConnectionManager)> {
log::info!(
target: "nemo_relay.plugin",
event = "plugin_resource_access_pending",
plugin_kind = "adaptive",
resource_kind = "redis",
permission = "connect";
"Plugin Redis connectivity validation started"
);
let client = redis::Client::open(url).map_err(|e| {
log::warn!(
target: "nemo_relay.plugin",
event = "plugin_resource_access_failed",
plugin_kind = "adaptive",
resource_kind = "redis",
permission = "connect",
reason = "client_configuration";
"Plugin resource access validation failed"
);
AdaptiveError::Storage(format!("redis client: {e}"))
})?;
let conn = client.get_connection_manager().await.map_err(|e| {
Comment thread
zhongxuanwang-nv marked this conversation as resolved.
log::warn!(
target: "nemo_relay.plugin",
event = "plugin_resource_access_failed",
plugin_kind = "adaptive",
resource_kind = "redis",
permission = "connect",
reason = "connection_failed";
"Plugin resource access validation failed"
);
AdaptiveError::Storage(format!("redis connection: {e}"))
})?;
log::info!(
target: "nemo_relay.plugin",
event = "plugin_resource_connected",
plugin_kind = "adaptive",
resource_kind = "redis",
permission = "connect";
"Plugin Redis connectivity established"
);
Ok((client, conn))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// A Redis-backed storage backend for cross-process shared state.
///
/// Uses [`ConnectionManager`] which is `Clone` (internally `Arc`-based) and
Expand All @@ -55,46 +106,7 @@ impl RedisBackend {
/// Returns [`AdaptiveError::Storage`] if the client cannot be created or the
/// connection cannot be established.
pub async fn new(url: &str, key_prefix: impl Into<String>) -> Result<Self> {
log::info!(
target: "nemo_relay.plugin",
event = "plugin_resource_access_pending",
plugin_kind = "adaptive",
resource_kind = "redis",
permission = "connect";
"Plugin Redis connectivity validation started"
);
let client = redis::Client::open(url).map_err(|e| {
log::warn!(
target: "nemo_relay.plugin",
event = "plugin_resource_access_failed",
plugin_kind = "adaptive",
resource_kind = "redis",
permission = "connect",
reason = "client_configuration";
"Plugin resource access validation failed"
);
AdaptiveError::Storage(format!("redis client: {e}"))
})?;
let conn = client.get_connection_manager().await.map_err(|e| {
log::warn!(
target: "nemo_relay.plugin",
event = "plugin_resource_access_failed",
plugin_kind = "adaptive",
resource_kind = "redis",
permission = "connect",
reason = "connection_failed";
"Plugin resource access validation failed"
);
AdaptiveError::Storage(format!("redis connection: {e}"))
})?;
log::info!(
target: "nemo_relay.plugin",
event = "plugin_resource_connected",
plugin_kind = "adaptive",
resource_kind = "redis",
permission = "connect";
"Plugin Redis connectivity established"
);
let (client, conn) = connect(url).await?;
Ok(Self {
client,
conn,
Expand Down
Loading
Loading