Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
de4e675
feat!: make middleware async across primary bindings
willkill07 Jul 28, 2026
48301e6
test: preserve legacy FFI sanitizer errors
willkill07 Jul 28, 2026
f561f01
fix: address async middleware review feedback
willkill07 Jul 28, 2026
33f5149
docs: clarify Python awaitable middleware callers
willkill07 Jul 28, 2026
53ba8ff
fix: isolate async event sanitizer panics
willkill07 Jul 28, 2026
7217a51
fix: preserve queued sanitizer execution semantics
willkill07 Jul 28, 2026
d70fbd8
fix: address async middleware review follow-ups
willkill07 Jul 28, 2026
85afa47
test: align sanitizer failure expectations
willkill07 Jul 28, 2026
3d7f366
test: move runtime panic coverage out of source
willkill07 Jul 28, 2026
ca6978b
test: cover all sanitizer panic paths
willkill07 Jul 28, 2026
867f92a
test: assert middleware panic error variants
willkill07 Jul 28, 2026
11c8cae
fix: address hidden middleware review findings
willkill07 Jul 28, 2026
c623fe1
fix: retain Python loop context for sanitizers
willkill07 Jul 28, 2026
c073e9c
fix: preserve progressive event sanitizer context
willkill07 Jul 28, 2026
3fc90f8
fix: address async middleware review findings
willkill07 Jul 28, 2026
8f2d417
fix: prevent async sanitizer flush deadlocks
willkill07 Jul 28, 2026
8b60b5d
fix: scope reentrant flush guards to callbacks
willkill07 Jul 28, 2026
cbb23a7
fix(python): scope sanitizer flush reentrancy
willkill07 Jul 29, 2026
bd8099a
fix: address async middleware review regressions
willkill07 Jul 29, 2026
d7ebb67
fix: address async binding review findings
willkill07 Jul 29, 2026
4ace258
fix: resolve async middleware review findings
willkill07 Jul 29, 2026
24b02b3
test(go): encode streaming fixtures as valid JSON
willkill07 Jul 29, 2026
c32b5df
fix: prevent queued sanitizer flush deadlocks
willkill07 Jul 29, 2026
bc76388
fix: make publication barriers flush-safe
willkill07 Jul 29, 2026
2204cbd
fix: preserve async publication flush ordering
willkill07 Jul 29, 2026
fd32193
fix: address async middleware review feedback
willkill07 Jul 29, 2026
453b2fd
fix: preserve async binding publication progress
willkill07 Jul 29, 2026
6243d64
fix: complete async middleware review remediation
willkill07 Jul 29, 2026
f5a3818
fix: close async middleware lifecycle gaps
willkill07 Jul 29, 2026
b22299f
fix: make async publication context safe
willkill07 Jul 29, 2026
8ee99e4
fix: preserve async middleware lifecycle context
willkill07 Jul 29, 2026
bec8853
fix(node): expire nested middleware context
willkill07 Jul 29, 2026
70d8638
fix: isolate async middleware contexts
willkill07 Jul 29, 2026
6ac0f7c
fix: preserve queued middleware context
willkill07 Jul 29, 2026
7981b0d
fix(python): preserve middleware invocation context
willkill07 Jul 29, 2026
03879c1
fix(python): cancel expired middleware contexts
willkill07 Jul 29, 2026
9e0668b
fix: make async middleware cancellation safe
willkill07 Jul 29, 2026
affbe3d
fix: preserve async middleware execution context
willkill07 Jul 29, 2026
603dee9
fix: preserve async middleware lifecycle context
willkill07 Jul 29, 2026
cefed3c
style(plugin): format native async fixture
willkill07 Jul 29, 2026
8def88e
fix(runtime): preserve nested publication order
willkill07 Jul 29, 2026
03b4c8d
fix(runtime): preserve foreign nested publication order
willkill07 Jul 29, 2026
ed34e0b
fix(runtime): preserve nested async publication context
willkill07 Jul 29, 2026
372eaa1
fix: harden async middleware callbacks
willkill07 Jul 29, 2026
422f15e
fix: finalize async middleware contracts
willkill07 Jul 29, 2026
b320a03
chore: refresh stacked PR checks
willkill07 Jul 29, 2026
8763e5d
chore: refresh Rust attributions
willkill07 Jul 29, 2026
2e187fe
test(plugin): flush subscriber-emitted events
willkill07 Jul 29, 2026
9996065
fix: flush transitive subscriber publications
willkill07 Jul 29, 2026
2d24d3c
chore: refresh Rust attributions after rebase
willkill07 Jul 29, 2026
92706ae
fix(ci): align Rust attributions with pinned generator
willkill07 Jul 29, 2026
b0599c5
fix: enforce execution continuation lifetimes
willkill07 Jul 30, 2026
3dcc68a
test(node): await queued subscriber callbacks
willkill07 Jul 30, 2026
6623eb9
Merge branch 'main' into wkk_relay-509-async-primary-middleware
willkill07 Jul 30, 2026
9cb4e37
test: stabilize WebSocket reconnect capture
willkill07 Jul 30, 2026
0b5235f
fix(node): abort revoked continuation callbacks
willkill07 Jul 30, 2026
698a5f6
test(node): synchronize continuation cancellation
willkill07 Jul 30, 2026
9bdb5c9
fix: preserve callback cancellation and stream context
willkill07 Jul 30, 2026
15cf089
Merge branch 'main' into wkk_relay-509-async-primary-middleware
willkill07 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
46 changes: 26 additions & 20 deletions crates/adaptive/src/acg_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,26 +582,32 @@ pub(crate) fn create_acg_llm_request_intercept(
provider: String,
plugin: Arc<dyn ProviderPlugin>,
) -> LlmRequestInterceptFn {
Arc::new(move |_name: &str, request: LlmRequest, annotated| {
let input_content = request.content.clone();
let translated =
translate_request(&request, &agent_id, &provider, plugin.as_ref(), &hot_cache)
.unwrap_or(request);
if annotated.is_some() && translated.content != input_content {
let translated_annotated = build_semantic_request_view(&translated)
.map_err(|error| nemo_relay::error::FlowError::Internal(error.to_string()))?
.annotated_request;
return Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
LlmRequest {
headers: translated.headers,
content: input_content,
},
Some(translated_annotated),
));
}
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
translated, annotated,
))
Arc::new(move |_name: String, request: LlmRequest, annotated| {
let hot_cache = hot_cache.clone();
let agent_id = agent_id.clone();
let provider = provider.clone();
let plugin = plugin.clone();
Box::pin(async move {
let input_content = request.content.clone();
let translated =
translate_request(&request, &agent_id, &provider, plugin.as_ref(), &hot_cache)
.unwrap_or(request);
if annotated.is_some() && translated.content != input_content {
let translated_annotated = build_semantic_request_view(&translated)
.map_err(|error| nemo_relay::error::FlowError::Internal(error.to_string()))?
.annotated_request;
return Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
LlmRequest {
headers: translated.headers,
content: input_content,
},
Some(translated_annotated),
));
}
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
translated, annotated,
))
})
})
}

Expand Down
10 changes: 5 additions & 5 deletions crates/adaptive/src/adaptive_hints_intercept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,14 @@ impl AdaptiveHintsIntercept {
pub fn into_request_fn(self) -> LlmRequestInterceptFn {
let this = Arc::new(self);
Arc::new(
move |_name: &str,
move |_name: String,
mut request: LlmRequest,
mut annotated: Option<AnnotatedLlmRequest>| {
let this = this.clone();
let scope_path = extract_scope_path();
let manual_ls = read_manual_latency_sensitivity();
let scope_depth = scope_path.len();
let call_index = this.call_counter.fetch_add(1, Ordering::Relaxed);

let effective_agent_id = this.effective_agent_id();
let cached_hints =
this.load_hints(&scope_path, &effective_agent_id, call_index, scope_depth);
Expand All @@ -196,9 +196,9 @@ impl AdaptiveHintsIntercept {
inject_agent_hints(&mut request, &mut annotated, &hints);
}

Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
let outcome =
nemo_relay::api::llm::LlmRequestInterceptOutcome::new(request, annotated);
Box::pin(async move { Ok(outcome) })
},
)
}
Expand Down
4 changes: 4 additions & 0 deletions crates/adaptive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
pub(crate) static TEST_GLOBAL_CONTEXT_MUTEX: tokio::sync::Mutex<()> =
tokio::sync::Mutex::const_new(());

#[cfg(test)]
#[path = "../tests/support/mod.rs"]
pub(crate) mod test_support;

pub mod acg;
pub mod acg_component;
pub mod acg_learner;
Expand Down
12 changes: 8 additions & 4 deletions crates/adaptive/tests/integration/runtime_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ async fn test_adaptive_plugin_registers_and_passes_calls_through() {
content: json!({"messages": []}),
},
)
.await
.unwrap();
assert_eq!(request.request.content["messages"], json!([]));

Expand Down Expand Up @@ -739,9 +740,11 @@ impl Plugin for HeaderPlugin {
false,
Arc::new(|_name, mut request, annotated| {
request.headers.insert("x-plugin".into(), json!("set"));
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
Box::pin(async move {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
})
}),
)?;
ctx.register_tool_request_intercept(
Expand All @@ -752,7 +755,7 @@ impl Plugin for HeaderPlugin {
if let Json::Object(ref mut map) = args {
map.insert("x-tool-plugin".into(), json!(true));
}
Ok(args)
Box::pin(async move { Ok(args) })
}),
)?;
ctx.register_llm_execution_intercept(
Expand Down Expand Up @@ -823,6 +826,7 @@ async fn test_top_level_plugin_registers_request_and_execution_intercepts() {
content: json!({"messages": []}),
},
)
.await
.unwrap();
assert_eq!(request.request.headers.get("x-plugin"), Some(&json!("set")));

Expand Down
12 changes: 12 additions & 0 deletions crates/adaptive/tests/support/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use std::future::Future;

pub(crate) fn block_on<F: Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build")
.block_on(future)
}
13 changes: 9 additions & 4 deletions crates/adaptive/tests/unit/acg_component_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,11 +1087,11 @@ fn acg_component_request_intercept_passes_original_request_and_annotation_when_t
plugin,
);

let outcome = intercept(
"anthropic",
let outcome = crate::test_support::block_on(intercept(
"anthropic".to_string(),
invalid_request.clone(),
Some(annotated.clone()),
)
))
.expect("request intercept should pass through");
let translated = outcome.request;
let returned_annotated = outcome.annotated_request;
Expand Down Expand Up @@ -1335,7 +1335,12 @@ fn acg_component_request_intercept_rewrites_annotation_without_mutating_provider
plugin,
);

let outcome = intercept("anthropic", request, Some(original_annotation.clone())).unwrap();
let outcome = crate::test_support::block_on(intercept(
"anthropic".to_string(),
request,
Some(original_annotation.clone()),
))
.unwrap();

assert_eq!(outcome.request.content, original_content);
let annotation = outcome
Expand Down
19 changes: 10 additions & 9 deletions crates/adaptive/tests/unit/adaptive_hints_intercept_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Unit tests for adaptive hints intercept in the NeMo Relay adaptive crate.

use super::*;

use std::sync::{Mutex, OnceLock};

use crate::trie::data_models::{LlmCallPrediction, PredictionMetrics};
Expand Down Expand Up @@ -196,14 +197,14 @@ fn test_adaptive_hints_intercept_injects_prediction_hints_and_manual_override()
stream: None,
extra: serde_json::Map::new(),
};
let outcome = req_fn(
"model",
let outcome = crate::test_support::block_on(req_fn(
"model".to_string(),
LlmRequest {
headers: serde_json::Map::new(),
content: serde_json::json!({}),
},
Some(annotated.clone()),
)
))
.unwrap();
let request = outcome.request;
let returned_annotated = outcome.annotated_request;
Expand Down Expand Up @@ -266,14 +267,14 @@ fn test_adaptive_hints_intercept_uses_defaults_and_ignores_poisoned_cache() {
}));
let req_fn =
AdaptiveHintsIntercept::new(hot_cache, "fallback-agent".to_string()).into_request_fn();
let outcome = req_fn(
"model",
let outcome = crate::test_support::block_on(req_fn(
"model".to_string(),
LlmRequest {
headers: serde_json::Map::new(),
content: serde_json::json!({}),
},
None,
)
))
.unwrap();
let request = outcome.request;
let annotated = outcome.annotated_request;
Expand Down Expand Up @@ -305,14 +306,14 @@ fn test_adaptive_hints_intercept_uses_defaults_and_ignores_poisoned_cache() {
});
let poisoned_req_fn =
AdaptiveHintsIntercept::new(poisoned_cache, "fallback-agent".to_string()).into_request_fn();
let poisoned_outcome = poisoned_req_fn(
"model",
let poisoned_outcome = crate::test_support::block_on(poisoned_req_fn(
"model".to_string(),
LlmRequest {
headers: serde_json::Map::new(),
content: serde_json::json!({"existing": true}),
},
None,
)
))
.unwrap();
let poisoned_request = poisoned_outcome.request;
assert!(
Expand Down
1 change: 1 addition & 0 deletions crates/adaptive/tests/unit/plugin_component_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ async fn adaptive_plugin_registers_runtime_and_rolls_back_registration() {
content: json!({}),
},
)
.await
.unwrap();
assert!(request.request.headers.is_empty());

Expand Down
25 changes: 16 additions & 9 deletions crates/adaptive/tests/unit/runtime_features_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,11 @@ fn assert_llm_request_intercept_registered(name: &str) {
i32::MAX,
false,
Arc::new(|_name, request, annotated| {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
Box::pin(async move {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
})
}),
),
name,
Expand All @@ -154,9 +156,11 @@ fn assert_llm_request_intercept_absent(name: &str) {
i32::MAX,
false,
Arc::new(|_name, request, annotated| {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
Box::pin(async move {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
})
}),
)
.unwrap();
Expand Down Expand Up @@ -565,6 +569,7 @@ async fn adaptive_hints_feature_registers_request_intercept() {
content: json!({}),
},
)
.await
.unwrap();
assert!(request.request.headers.contains_key(AGENT_HINTS_HEADER_KEY));

Expand Down Expand Up @@ -730,9 +735,11 @@ async fn registration_context_registers_all_supported_callback_types() {
5,
false,
Arc::new(|_name, request, annotated| {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
Box::pin(async move {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
})
}),
)
.unwrap();
Expand Down
1 change: 1 addition & 0 deletions crates/adaptive/tests/unit/runtime_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ async fn adaptive_runtime_bind_scope_requires_registration_and_passes_through_wi
};

let translated = llm_request_intercepts("anthropic", request.clone())
.await
.expect("request intercept chain should pass through when no hot-cache state exists");

assert_eq!(translated.request.content, request.content);
Expand Down
53 changes: 2 additions & 51 deletions crates/cli/src/diagnostics/probes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,54 +127,5 @@ fn grpc_endpoint_port(endpoint: &reqwest::Url) -> u16 {
}

#[cfg(test)]
mod tcp_tests {
use super::*;

#[tokio::test]
async fn grpc_probe_uses_tcp_connectivity() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let endpoint = format!("http://{}", listener.local_addr().unwrap());
let check = probe_tcp_named("OpenTelemetry endpoint", &endpoint).await;
assert_eq!(check.status, Status::Pass);
assert!(check.details.contains("gRPC TCP connection succeeded"));
}

#[tokio::test]
async fn grpc_probe_reports_invalid_hostless_and_refused_endpoints() {
let invalid = probe_tcp_named("OpenTelemetry endpoint", "not a url").await;
assert_eq!(invalid.status, Status::Fail);
assert!(invalid.details.contains("invalid gRPC endpoint"));

let hostless = probe_tcp_named("OpenTelemetry endpoint", "file:///tmp/collector").await;
assert_eq!(hostless.status, Status::Fail);
assert!(hostless.details.contains("has no host"));

let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let endpoint = format!("http://{}", listener.local_addr().unwrap());
drop(listener);
let refused = probe_tcp_named("OpenTelemetry endpoint", &endpoint).await;
assert_eq!(refused.status, Status::Fail);
assert!(
refused.details.contains("connection failed")
|| refused.details.contains("connection timed out"),
"{}",
refused.details
);
}

#[test]
fn grpc_probe_uses_tls_and_otlp_default_ports() {
assert_eq!(
grpc_endpoint_port(&reqwest::Url::parse("https://collector.example.com").unwrap()),
443
);
assert_eq!(
grpc_endpoint_port(&reqwest::Url::parse("http://collector.example.com").unwrap()),
4317
);
assert_eq!(
grpc_endpoint_port(&reqwest::Url::parse("https://collector.example.com:8443").unwrap()),
8443
);
}
}
#[path = "../../tests/coverage/shared/probes_tests.rs"]
mod tcp_tests;
Loading
Loading