use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "http-server")]
use http::request::Parts;
#[cfg(feature = "http-server")]
use serde_json::Value;
#[cfg(feature = "http-server")]
use super::admitted::{self, Admission, AdmittedBody, Payload};
#[cfg(feature = "http-server")]
use lean_ctx::core::context_admission::egress::{EgressTarget, TrustedDestinationBinding};
#[cfg(feature = "http-server")]
const PROBE_TIMEOUT_SECS: u64 = 10;
#[derive(Clone, Debug, Default)]
pub struct CounterfactualSlot(Arc<AtomicU64>);
impl PartialEq for CounterfactualSlot {
fn eq(&self, other: &Self) -> bool {
self.get() == other.get()
}
}
impl CounterfactualSlot {
pub fn new() -> Self {
Self::default()
}
pub fn set(&self, tokens: u64) {
self.0.store(tokens.max(1), Ordering::Relaxed);
}
pub fn get(&self) -> Option<u64> {
match self.0.load(Ordering::Relaxed) {
0 => None,
n => Some(n),
}
}
}
#[cfg(feature = "http-server")]
const COUNT_TOKENS_FIELDS: &[&str] = &[
"model",
"messages",
"system",
"tools",
"tool_choice",
"thinking",
];
#[cfg(feature = "http-server")]
pub(crate) fn probe_body(original: &Value, original_model: Option<&str>) -> Option<Vec<u8>> {
let obj = original.as_object()?;
if !obj.contains_key("model") || !obj.contains_key("messages") {
return None;
}
let mut probe = serde_json::Map::new();
for &field in COUNT_TOKENS_FIELDS {
if let Some(v) = obj.get(field) {
probe.insert(field.to_string(), v.clone());
}
}
if let Some(model) = original_model {
probe.insert("model".to_string(), Value::String(model.to_string()));
}
serde_json::to_vec(&Value::Object(probe)).ok()
}
#[cfg(feature = "http-server")]
const PROBE_HEADERS: &[&str] = &[
"x-api-key",
"authorization",
"anthropic-version",
"anthropic-beta",
];
#[cfg(feature = "http-server")]
pub(crate) fn maybe_spawn_probe(
client: &reqwest::Client,
parts: &Parts,
upstream_base: &str,
original: Option<&Value>,
original_model: Option<&str>,
request_was_rewritten: bool,
) -> Option<CounterfactualSlot> {
if !request_was_rewritten
|| !parts
.uri
.path()
.trim_end_matches('/')
.ends_with("/v1/messages")
|| !lean_ctx::core::config::Config::load()
.proxy
.counterfactual_metering_enabled()
{
return None;
}
let original_body = probe_body(original?, original_model)?;
let probe_doc: Value = serde_json::from_slice(&original_body).ok()?;
let target = EgressTarget {
provider: "Anthropic",
model: probe_doc.get("model").and_then(Value::as_str),
upstream_base,
};
let mut admission = admitted::admit(
Payload::Document(&probe_doc),
&target,
TrustedDestinationBinding::default(),
)
.ok()?;
if admission.is_refused() {
return None;
}
let original_size = original_body.len();
let body = match admission.take_rewritten() {
Some(admitted_doc) => serde_json::to_vec(&admitted_doc).ok()?,
None => original_body,
};
let probe = Probe {
client: client.clone(),
upstream_base: upstream_base.to_owned(),
headers: PROBE_HEADERS
.iter()
.filter_map(|&name| Some((name, parts.headers.get(name)?.clone())))
.collect(),
body,
original_size,
};
let slot = CounterfactualSlot::new();
let task_slot = slot.clone();
tokio::spawn(async move {
dispatch_probe(probe, admission, task_slot).await;
});
Some(slot)
}
#[cfg(feature = "http-server")]
struct Probe {
client: reqwest::Client,
upstream_base: String,
headers: Vec<(&'static str, axum::http::HeaderValue)>,
body: Vec<u8>,
original_size: usize,
}
#[cfg(feature = "http-server")]
async fn dispatch_probe(probe: Probe, admission: Admission, task_slot: CounterfactualSlot) {
let url = format!(
"{}/v1/messages/count_tokens",
probe.upstream_base.trim_end_matches('/')
);
let head = crate::admitted::RequestHead::new(
reqwest::Method::POST,
std::iter::once((
axum::http::header::CONTENT_TYPE,
axum::http::HeaderValue::from_static("application/json"),
))
.chain(
probe
.headers
.into_iter()
.map(|(name, value)| (axum::http::HeaderName::from_static(name), value)),
),
);
let Ok(admitted_probe) = AdmittedBody::bind(
admission,
probe.body,
&probe.upstream_base,
&url,
head,
probe.original_size,
None,
)
.await
else {
return;
};
let attempt = admitted_probe
.send(&probe.client, 1, |request| {
request.timeout(std::time::Duration::from_secs(PROBE_TIMEOUT_SECS))
})
.await;
let Ok(attempt) = attempt else {
return;
};
match attempt.response {
Ok(resp) if resp.status().is_success() => match resp.json::<Value>().await {
Ok(v) => {
if let Some(tokens) = v.get("input_tokens").and_then(Value::as_u64) {
task_slot.set(tokens);
} else {
tracing::debug!("counterfactual probe: response without input_tokens");
}
}
Err(e) => tracing::debug!(
"counterfactual probe: unreadable response: {}",
admitted::transport_failure(&e)
),
},
Ok(resp) => tracing::debug!(
"counterfactual probe: count_tokens returned {}",
resp.status()
),
Err(e) => tracing::debug!("counterfactual probe: {}", admitted::transport_failure(&e)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "http-server")]
use serde_json::json;
#[cfg(feature = "http-server")]
fn full_request() -> Value {
json!({
"model": "claude-sonnet-4",
"messages": [{"role": "user", "content": "hello"}],
"system": "be terse",
"tools": [{"name": "get_weather", "input_schema": {"type": "object"}}],
"tool_choice": {"type": "auto"},
"thinking": {"type": "enabled", "budget_tokens": 1024},
"max_tokens": 4096,
"stream": true,
"temperature": 0.7,
"metadata": {"user_id": "u1"}
})
}
#[cfg(feature = "http-server")]
#[tokio::test(flavor = "current_thread")]
async fn mandatory_receipt_failure_prevents_counterfactual_network_request() {
let _data = lean_ctx::core::data_dir::isolated_data_dir();
let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
let config = lean_ctx::core::paths::config_dir_read_only().expect("config directory");
std::fs::create_dir_all(&config).expect("config directory");
std::fs::write(
config.join("config.toml"),
"[context_gateway]\nmode = \"governed\"\n",
)
.expect("governed config");
let data = lean_ctx::core::data_dir::lean_ctx_data_dir().expect("data directory");
std::fs::create_dir_all(&data).expect("data directory");
std::fs::write(data.join("gateway"), b"blocked test store").expect("store obstruction");
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("listener");
let upstream = format!("http://{}", listener.local_addr().expect("address"));
let body = probe_body(&full_request(), None).expect("probe bytes");
let document: Value = serde_json::from_slice(&body).expect("probe JSON");
let admission = admitted::admit(
Payload::Document(&document),
&EgressTarget {
provider: "Anthropic",
model: document.get("model").and_then(Value::as_str),
upstream_base: &upstream,
},
TrustedDestinationBinding::default(),
)
.expect("probe admission");
let probe = Probe {
client: reqwest::Client::new(),
upstream_base: upstream.clone(),
headers: Vec::new(),
original_size: body.len(),
body,
};
let slot = CounterfactualSlot::new();
dispatch_probe(probe, admission, slot.clone()).await;
assert!(slot.get().is_none());
assert!(
tokio::time::timeout(std::time::Duration::from_millis(50), listener.accept())
.await
.is_err()
);
}
#[cfg(feature = "http-server")]
#[test]
fn probe_body_is_the_count_tokens_whitelist() {
let body = probe_body(&full_request(), None).expect("probe body");
let v: Value = serde_json::from_slice(&body).unwrap();
let obj = v.as_object().unwrap();
for field in COUNT_TOKENS_FIELDS {
assert!(obj.contains_key(*field), "{field} must be projected");
}
for rejected in ["max_tokens", "stream", "temperature", "metadata"] {
assert!(!obj.contains_key(rejected), "{rejected} must be stripped");
}
assert_eq!(obj["model"], "claude-sonnet-4");
}
#[cfg(feature = "http-server")]
#[test]
fn probe_body_restores_the_prerouting_model() {
let mut req = full_request();
req["model"] = json!("claude-haiku-3.5");
let body = probe_body(&req, Some("claude-sonnet-4")).unwrap();
let v: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v["model"], "claude-sonnet-4");
}
#[cfg(feature = "http-server")]
#[test]
fn probe_body_requires_model_and_messages() {
assert!(probe_body(&json!({"messages": []}), None).is_none());
assert!(probe_body(&json!({"model": "m"}), None).is_none());
assert!(probe_body(&json!("not an object"), None).is_none());
}
#[test]
fn slot_roundtrip_and_zero_means_empty() {
let slot = CounterfactualSlot::new();
assert_eq!(slot.get(), None, "fresh slot is empty");
slot.set(1234);
assert_eq!(slot.get(), Some(1234));
let zero = CounterfactualSlot::new();
zero.set(0);
assert_eq!(zero.get(), Some(1));
}
#[test]
fn slots_share_state_across_clones() {
let slot = CounterfactualSlot::new();
let clone = slot.clone();
clone.set(77);
assert_eq!(slot.get(), Some(77));
}
}