mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 17:37:47 +08:00
fix: harden concurrency limits and high-RPM runtime paths
Bound request, stream, queue, and shutdown resource lifetimes. Reduce scheduler and Redis hot-path work and isolate database maintenance. Include regression coverage, load probes, and concurrency audit results.
This commit is contained in:
@@ -224,6 +224,36 @@ pub struct StoredRequestCandidate {
|
||||
}
|
||||
|
||||
impl StoredRequestCandidate {
|
||||
/// Scheduling needs identity, status, counters and times, without diagnostic payloads.
|
||||
pub fn runtime_snapshot(&self) -> Self {
|
||||
Self {
|
||||
id: self.id.clone(),
|
||||
request_id: self.request_id.clone(),
|
||||
user_id: self.user_id.clone(),
|
||||
api_key_id: self.api_key_id.clone(),
|
||||
username: None,
|
||||
api_key_name: None,
|
||||
candidate_index: self.candidate_index,
|
||||
retry_index: self.retry_index,
|
||||
provider_id: self.provider_id.clone(),
|
||||
endpoint_id: self.endpoint_id.clone(),
|
||||
key_id: self.key_id.clone(),
|
||||
status: self.status,
|
||||
skip_reason: None,
|
||||
is_cached: self.is_cached,
|
||||
status_code: self.status_code,
|
||||
error_type: None,
|
||||
error_message: None,
|
||||
latency_ms: self.latency_ms,
|
||||
concurrent_requests: self.concurrent_requests,
|
||||
extra_data: None,
|
||||
required_capabilities: None,
|
||||
created_at_unix_ms: self.created_at_unix_ms,
|
||||
started_at_unix_ms: self.started_at_unix_ms,
|
||||
finished_at_unix_ms: self.finished_at_unix_ms,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sanitize_for_persistence(&mut self) {
|
||||
self.username = None;
|
||||
self.api_key_name = None;
|
||||
@@ -691,6 +721,19 @@ pub trait RequestCandidateReadRepository: Send + Sync {
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredRequestCandidate>, crate::DataLayerError>;
|
||||
|
||||
/// Same ordering and limit as `list_recent`, omitting diagnostic fields.
|
||||
async fn list_recent_runtime(
|
||||
&self,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredRequestCandidate>, crate::DataLayerError> {
|
||||
Ok(self
|
||||
.list_recent(limit)
|
||||
.await?
|
||||
.iter()
|
||||
.map(StoredRequestCandidate::runtime_snapshot)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_by_provider_id(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
@@ -849,9 +892,7 @@ pub fn sanitize_request_candidate_extra_data_for_persistence(
|
||||
extra_data: Option<serde_json::Value>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let object = extra_data.as_ref()?.as_object()?;
|
||||
let mut sanitized = sanitize_request_candidate_extra_data(extra_data.clone())
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.unwrap_or_default();
|
||||
let mut sanitized = sanitize_candidate_extra_data_object(object);
|
||||
for (key, fields) in [
|
||||
("upstream_response", &["headers", "body"][..]),
|
||||
("error_flow", &["message"][..]),
|
||||
@@ -884,7 +925,10 @@ pub fn sanitize_request_candidate_extra_data_for_persistence(
|
||||
};
|
||||
let mut summary = sanitized
|
||||
.remove(key)
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.and_then(|value| match value {
|
||||
serde_json::Value::Object(object) => Some(object),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
for field in fields {
|
||||
if let Some(value) = diagnostic.get(*field).filter(|value| !value.is_null()) {
|
||||
@@ -907,70 +951,72 @@ pub fn sanitize_request_candidate_extra_data(
|
||||
let serde_json::Value::Object(object) = extra_data? else {
|
||||
return None;
|
||||
};
|
||||
let sanitized = sanitize_candidate_extra_data_object(&object);
|
||||
(!sanitized.is_empty()).then_some(serde_json::Value::Object(sanitized))
|
||||
}
|
||||
|
||||
fn sanitize_candidate_extra_data_object(
|
||||
object: &serde_json::Map<String, serde_json::Value>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut sanitized = serde_json::Map::new();
|
||||
|
||||
for field in ["gateway_execution_runtime", "stream_completed", "cache_1h"] {
|
||||
insert_candidate_bool(&object, &mut sanitized, field);
|
||||
insert_candidate_bool(object, &mut sanitized, field);
|
||||
}
|
||||
for field in ["first_byte_time_ms", "pool_key_index"] {
|
||||
insert_candidate_u64(&object, &mut sanitized, field);
|
||||
insert_candidate_u64(object, &mut sanitized, field);
|
||||
}
|
||||
insert_candidate_i64(&object, &mut sanitized, "priority_slot");
|
||||
insert_candidate_u64(&object, &mut sanitized, "ranking_index");
|
||||
insert_candidate_i64(object, &mut sanitized, "priority_slot");
|
||||
insert_candidate_u64(object, &mut sanitized, "ranking_index");
|
||||
|
||||
insert_candidate_known_string(&object, &mut sanitized, "phase", sanitize_candidate_phase);
|
||||
insert_candidate_known_string(object, &mut sanitized, "phase", sanitize_candidate_phase);
|
||||
for field in [
|
||||
"client_api_format",
|
||||
"provider_api_format",
|
||||
"client_contract",
|
||||
"provider_contract",
|
||||
] {
|
||||
insert_candidate_known_string(
|
||||
&object,
|
||||
&mut sanitized,
|
||||
field,
|
||||
sanitize_candidate_api_format,
|
||||
);
|
||||
insert_candidate_known_string(object, &mut sanitized, field, sanitize_candidate_api_format);
|
||||
}
|
||||
insert_candidate_known_string(
|
||||
&object,
|
||||
object,
|
||||
&mut sanitized,
|
||||
"execution_strategy",
|
||||
sanitize_candidate_execution_strategy,
|
||||
);
|
||||
insert_candidate_known_string(
|
||||
&object,
|
||||
object,
|
||||
&mut sanitized,
|
||||
"conversion_mode",
|
||||
sanitize_candidate_conversion_mode,
|
||||
);
|
||||
insert_candidate_known_string(
|
||||
&object,
|
||||
object,
|
||||
&mut sanitized,
|
||||
"ranking_mode",
|
||||
sanitize_candidate_ranking_mode,
|
||||
);
|
||||
insert_candidate_known_string(
|
||||
&object,
|
||||
object,
|
||||
&mut sanitized,
|
||||
"priority_mode",
|
||||
sanitize_candidate_priority_mode,
|
||||
);
|
||||
insert_candidate_known_string(
|
||||
&object,
|
||||
object,
|
||||
&mut sanitized,
|
||||
"promoted_by",
|
||||
sanitize_candidate_promotion_reason,
|
||||
);
|
||||
insert_candidate_known_string(
|
||||
&object,
|
||||
object,
|
||||
&mut sanitized,
|
||||
"demoted_by",
|
||||
sanitize_candidate_demotion_reason,
|
||||
);
|
||||
insert_candidate_known_string(&object, &mut sanitized, "source", sanitize_candidate_source);
|
||||
insert_candidate_known_string(object, &mut sanitized, "source", sanitize_candidate_source);
|
||||
insert_candidate_known_string(
|
||||
&object,
|
||||
object,
|
||||
&mut sanitized,
|
||||
"execution_path",
|
||||
sanitize_candidate_execution_path,
|
||||
@@ -1030,7 +1076,7 @@ pub fn sanitize_request_candidate_extra_data(
|
||||
sanitized.insert("pool_group_exhaustion".to_string(), exhaustion);
|
||||
}
|
||||
|
||||
(!sanitized.is_empty()).then_some(serde_json::Value::Object(sanitized))
|
||||
sanitized
|
||||
}
|
||||
|
||||
pub fn sanitize_request_candidate_required_capabilities(
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use super::types::UsageBodyCaptureState;
|
||||
|
||||
/// Shared accounting for the estimated heap retained by diagnostic JSON bodies.
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug)]
|
||||
pub struct UsageCaptureMemoryBudget {
|
||||
limit: usize,
|
||||
retained: AtomicUsize,
|
||||
downgraded_total: AtomicU64,
|
||||
}
|
||||
|
||||
impl UsageCaptureMemoryBudget {
|
||||
pub fn new(limit: usize) -> Self {
|
||||
Self {
|
||||
limit,
|
||||
retained: AtomicUsize::new(0),
|
||||
downgraded_total: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_reserve(&self, bytes: usize) -> bool {
|
||||
if bytes == 0 {
|
||||
return true;
|
||||
}
|
||||
self.retained
|
||||
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |retained| {
|
||||
retained
|
||||
.checked_add(bytes)
|
||||
.filter(|next| *next <= self.limit)
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn release(&self, bytes: usize) {
|
||||
if bytes != 0 {
|
||||
self.retained.fetch_sub(bytes, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
fn record_downgrade(&self) {
|
||||
self.downgraded_total.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn retained_bytes(&self) -> usize {
|
||||
self.retained.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn downgraded_total(&self) -> u64 {
|
||||
self.downgraded_total.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> (usize, usize, u64) {
|
||||
(self.limit, self.retained_bytes(), self.downgraded_total())
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-serialized ownership of the diagnostic JSON heap estimate.
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct UsageCaptureRetention {
|
||||
budget: Option<Arc<UsageCaptureMemoryBudget>>,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
// Runtime accounting does not participate in value or wire equality.
|
||||
impl PartialEq for UsageCaptureRetention {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl UsageCaptureRetention {
|
||||
pub fn reserve(&mut self, budget: Arc<UsageCaptureMemoryBudget>, bytes: usize) -> bool {
|
||||
if self
|
||||
.budget
|
||||
.as_ref()
|
||||
.is_some_and(|current| Arc::ptr_eq(current, &budget))
|
||||
{
|
||||
if bytes > self.bytes && !budget.try_reserve(bytes - self.bytes) {
|
||||
budget.record_downgrade();
|
||||
return false;
|
||||
}
|
||||
if bytes < self.bytes {
|
||||
budget.release(self.bytes - bytes);
|
||||
}
|
||||
self.bytes = bytes;
|
||||
return true;
|
||||
}
|
||||
if !budget.try_reserve(bytes) {
|
||||
budget.record_downgrade();
|
||||
return false;
|
||||
}
|
||||
*self = Self {
|
||||
budget: Some(budget),
|
||||
bytes,
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, budget: Arc<UsageCaptureMemoryBudget>) {
|
||||
*self = Self {
|
||||
budget: Some(budget),
|
||||
bytes: 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clone_for_bodies(&self, estimate: impl FnOnce() -> usize) -> (Self, bool) {
|
||||
let Some(budget) = &self.budget else {
|
||||
return (Self::default(), true);
|
||||
};
|
||||
let mut retention = Self::default();
|
||||
if retention.reserve(Arc::clone(budget), estimate()) {
|
||||
(retention, true)
|
||||
} else {
|
||||
retention.clear(Arc::clone(budget));
|
||||
(retention, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UsageCaptureRetention {
|
||||
fn drop(&mut self) {
|
||||
if let Some(budget) = &self.budget {
|
||||
budget.release(self.bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serde_json::Map does not expose its backing allocation capacity. This charges a
|
||||
// conservative per-entry estimate, not an allocator or process RSS measurement.
|
||||
#[doc(hidden)]
|
||||
pub fn usage_json_heap_estimate(value: &Value) -> usize {
|
||||
match value {
|
||||
Value::String(value) => value.capacity(),
|
||||
Value::Array(values) => values.iter().fold(
|
||||
values
|
||||
.capacity()
|
||||
.saturating_mul(std::mem::size_of::<Value>()),
|
||||
|bytes, value| bytes.saturating_add(usage_json_heap_estimate(value)),
|
||||
),
|
||||
Value::Object(values) => values.iter().fold(
|
||||
values.len().saturating_mul(
|
||||
4 * (std::mem::size_of::<String>()
|
||||
+ std::mem::size_of::<Value>()
|
||||
+ std::mem::size_of::<usize>()),
|
||||
),
|
||||
|bytes, (key, value)| {
|
||||
bytes
|
||||
.saturating_add(key.capacity())
|
||||
.saturating_add(usage_json_heap_estimate(value))
|
||||
},
|
||||
),
|
||||
Value::Null | Value::Bool(_) | Value::Number(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks an omitted diagnostic body using the existing capture metadata shape.
|
||||
#[doc(hidden)]
|
||||
pub fn mark_usage_capture_memory_omitted(metadata: &mut Option<Value>, key: &str) {
|
||||
let source_bytes = metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.get("body_capture"))
|
||||
.and_then(|capture| capture.get(key))
|
||||
.and_then(|entry| entry.get("source_bytes"))
|
||||
.and_then(Value::as_u64);
|
||||
let Some(metadata) = metadata
|
||||
.get_or_insert_with(|| Value::Object(Map::with_capacity(1)))
|
||||
.as_object_mut()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(body_capture) = metadata
|
||||
.entry("body_capture".to_owned())
|
||||
.or_insert_with(|| Value::Object(Map::with_capacity(1)))
|
||||
.as_object_mut()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut entry = Map::with_capacity(3 + usize::from(source_bytes.is_some()));
|
||||
entry.insert(
|
||||
"state".to_owned(),
|
||||
Value::String(UsageBodyCaptureState::Truncated.as_str().to_owned()),
|
||||
);
|
||||
entry.insert("stored_bytes".to_owned(), Value::from(0));
|
||||
if let Some(source_bytes) = source_bytes {
|
||||
entry.insert("source_bytes".to_owned(), Value::from(source_bytes));
|
||||
}
|
||||
entry.insert(
|
||||
"reason".to_owned(),
|
||||
Value::String("usage_event_memory_budget_exceeded".to_owned()),
|
||||
);
|
||||
body_capture.insert(key.to_owned(), Value::Object(entry));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::types::UpsertUsageRecord;
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn upsert_with_diagnostic_bodies() -> UpsertUsageRecord {
|
||||
serde_json::from_str(
|
||||
r#"{
|
||||
"request_id": "retained-request",
|
||||
"provider_name": "openai",
|
||||
"model": "test-model",
|
||||
"status": "completed",
|
||||
"billing_status": "settled",
|
||||
"updated_at_unix_secs": 123,
|
||||
"input_tokens": 100,
|
||||
"output_tokens": 500,
|
||||
"total_tokens": 600,
|
||||
"cache_creation_input_tokens": 7,
|
||||
"cache_creation_ephemeral_5m_input_tokens": 7,
|
||||
"cache_creation_ephemeral_1h_input_tokens": 0,
|
||||
"cache_read_input_tokens": 0,
|
||||
"total_cost_usd": 1.25,
|
||||
"actual_total_cost_usd": 0.75,
|
||||
"cache_creation_cost_usd": 0.05,
|
||||
"cache_read_cost_usd": 0.0,
|
||||
"status_code": 200,
|
||||
"error_message": "preserved diagnostic classification",
|
||||
"request_headers": {"x-request": "original"},
|
||||
"provider_request_headers": {"x-provider-request": "original"},
|
||||
"response_headers": {"x-response": "original"},
|
||||
"client_response_headers": {"x-client-response": "original"},
|
||||
"request_body": {"text": "original request body"},
|
||||
"provider_request_body": {"text": "original provider request body"},
|
||||
"response_body": {"text": "original provider response body"},
|
||||
"client_response_body": {"text": "original client response body"},
|
||||
"request_body_ref": "usage://retained-request/request",
|
||||
"provider_request_body_ref": "usage://retained-request/provider_request",
|
||||
"response_body_ref": "usage://retained-request/response",
|
||||
"client_response_body_ref": "usage://retained-request/client_response",
|
||||
"request_body_state": "inline",
|
||||
"provider_request_body_state": "reference",
|
||||
"response_body_state": "inline",
|
||||
"client_response_body_state": "truncated",
|
||||
"request_metadata": {
|
||||
"trace_id": "unchanged",
|
||||
"body_capture": {
|
||||
"request": {"state": "inline", "source_bytes": 100},
|
||||
"provider_request": {"state": "reference", "source_bytes": 200},
|
||||
"response": {"state": "inline", "source_bytes": 300},
|
||||
"client_response": {"state": "truncated", "source_bytes": 400}
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("valid usage write fixture")
|
||||
}
|
||||
|
||||
fn upsert_body_estimate(record: &UpsertUsageRecord) -> usize {
|
||||
[
|
||||
&record.request_body,
|
||||
&record.provider_request_body,
|
||||
&record.response_body,
|
||||
&record.client_response_body,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|body| std::mem::size_of::<Value>() + usage_json_heap_estimate(body))
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_capture_memory_upsert_serde_skips_retention_and_preserves_value_equality() {
|
||||
let mut source = upsert_with_diagnostic_bodies();
|
||||
let weight = upsert_body_estimate(&source);
|
||||
let budget = Arc::new(UsageCaptureMemoryBudget::new(weight));
|
||||
assert!(source
|
||||
.capture_retention
|
||||
.reserve(Arc::clone(&budget), weight));
|
||||
let mut serialized = serde_json::to_value(&source).unwrap();
|
||||
assert!(serialized.get("capture_retention").is_none());
|
||||
serialized["capture_retention"] = json!({"bytes": usize::MAX});
|
||||
let roundtrip: UpsertUsageRecord = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(source, roundtrip);
|
||||
let unmanaged_clone = roundtrip.clone();
|
||||
assert_eq!(source, unmanaged_clone);
|
||||
assert!(unmanaged_clone.request_body.is_some());
|
||||
assert!(unmanaged_clone.provider_request_body.is_some());
|
||||
assert!(unmanaged_clone.response_body.is_some());
|
||||
assert!(unmanaged_clone.client_response_body.is_some());
|
||||
assert_eq!(budget.retained_bytes(), weight);
|
||||
assert_eq!(budget.downgraded_total(), 0);
|
||||
drop((roundtrip, unmanaged_clone));
|
||||
assert_eq!(budget.retained_bytes(), weight);
|
||||
drop(source);
|
||||
assert_eq!(budget.retained_bytes(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_capture_memory_upsert_clone_reserves_for_all_four_deep_copies() {
|
||||
let mut source = upsert_with_diagnostic_bodies();
|
||||
let original = serde_json::to_value(&source).unwrap();
|
||||
let weight = upsert_body_estimate(&source);
|
||||
let budget = Arc::new(UsageCaptureMemoryBudget::new(weight * 2));
|
||||
assert!(source
|
||||
.capture_retention
|
||||
.reserve(Arc::clone(&budget), weight));
|
||||
let cloned = source.clone();
|
||||
assert_eq!(source, cloned);
|
||||
assert_eq!(budget.retained_bytes(), weight * 2);
|
||||
assert_eq!(budget.downgraded_total(), 0);
|
||||
for (source_body, cloned_body) in [
|
||||
(&source.request_body, &cloned.request_body),
|
||||
(&source.provider_request_body, &cloned.provider_request_body),
|
||||
(&source.response_body, &cloned.response_body),
|
||||
(&source.client_response_body, &cloned.client_response_body),
|
||||
] {
|
||||
let source_text = source_body.as_ref().unwrap()["text"].as_str().unwrap();
|
||||
let cloned_text = cloned_body.as_ref().unwrap()["text"].as_str().unwrap();
|
||||
assert_eq!(source_text, cloned_text);
|
||||
assert_ne!(source_text.as_ptr(), cloned_text.as_ptr());
|
||||
}
|
||||
assert_eq!(serde_json::to_value(&source).unwrap(), original);
|
||||
drop(source);
|
||||
assert_eq!(budget.retained_bytes(), weight);
|
||||
assert_eq!(serde_json::to_value(&cloned).unwrap(), original);
|
||||
drop(cloned);
|
||||
assert_eq!(budget.retained_bytes(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_capture_memory_upsert_clone_over_budget_only_omits_four_bodies() {
|
||||
let mut source = upsert_with_diagnostic_bodies();
|
||||
let original = serde_json::to_value(&source).unwrap();
|
||||
let weight = upsert_body_estimate(&source);
|
||||
let budget = Arc::new(UsageCaptureMemoryBudget::new(weight));
|
||||
assert!(source
|
||||
.capture_retention
|
||||
.reserve(Arc::clone(&budget), weight));
|
||||
let cloned = source.clone();
|
||||
assert_eq!(budget.retained_bytes(), weight);
|
||||
assert_eq!(budget.downgraded_total(), 1);
|
||||
assert_eq!(serde_json::to_value(&source).unwrap(), original);
|
||||
|
||||
let mut expected = original;
|
||||
for (body, state, key, source_bytes) in [
|
||||
("request_body", "request_body_state", "request", 100),
|
||||
(
|
||||
"provider_request_body",
|
||||
"provider_request_body_state",
|
||||
"provider_request",
|
||||
200,
|
||||
),
|
||||
("response_body", "response_body_state", "response", 300),
|
||||
(
|
||||
"client_response_body",
|
||||
"client_response_body_state",
|
||||
"client_response",
|
||||
400,
|
||||
),
|
||||
] {
|
||||
expected[body] = Value::Null;
|
||||
expected[state] = json!("truncated");
|
||||
expected["request_metadata"]["body_capture"][key] = json!({
|
||||
"state": "truncated",
|
||||
"stored_bytes": 0,
|
||||
"source_bytes": source_bytes,
|
||||
"reason": "usage_event_memory_budget_exceeded"
|
||||
});
|
||||
}
|
||||
assert_eq!(serde_json::to_value(&cloned).unwrap(), expected);
|
||||
assert_eq!(cloned.output_tokens, Some(500));
|
||||
assert_eq!(cloned.cache_read_input_tokens, Some(0));
|
||||
assert_eq!(cloned.cache_creation_ephemeral_1h_input_tokens, Some(0));
|
||||
assert_eq!(cloned.total_cost_usd, Some(1.25));
|
||||
assert_eq!(cloned.actual_total_cost_usd, Some(0.75));
|
||||
drop(cloned);
|
||||
assert_eq!(budget.retained_bytes(), weight);
|
||||
drop(source);
|
||||
assert_eq!(budget.retained_bytes(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_capture_memory_upsert_clone_preserves_explicit_clearing_states() {
|
||||
for state in [
|
||||
UsageBodyCaptureState::None,
|
||||
UsageBodyCaptureState::Disabled,
|
||||
UsageBodyCaptureState::Unavailable,
|
||||
] {
|
||||
let mut source = upsert_with_diagnostic_bodies();
|
||||
source.request_body_state = Some(state);
|
||||
source.provider_request_body_state = Some(state);
|
||||
source.response_body_state = Some(state);
|
||||
source.client_response_body_state = Some(state);
|
||||
let original = serde_json::to_value(&source).unwrap();
|
||||
let weight = upsert_body_estimate(&source);
|
||||
let budget = Arc::new(UsageCaptureMemoryBudget::new(weight));
|
||||
assert!(source
|
||||
.capture_retention
|
||||
.reserve(Arc::clone(&budget), weight));
|
||||
let cloned = source.clone();
|
||||
let mut expected = original.clone();
|
||||
for body in [
|
||||
"request_body",
|
||||
"provider_request_body",
|
||||
"response_body",
|
||||
"client_response_body",
|
||||
] {
|
||||
expected[body] = Value::Null;
|
||||
}
|
||||
assert_eq!(serde_json::to_value(&cloned).unwrap(), expected);
|
||||
assert_eq!(serde_json::to_value(&source).unwrap(), original);
|
||||
assert_eq!(budget.retained_bytes(), weight);
|
||||
assert_eq!(budget.downgraded_total(), 1);
|
||||
drop(cloned);
|
||||
assert_eq!(budget.retained_bytes(), weight);
|
||||
drop(source);
|
||||
assert_eq!(budget.retained_bytes(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_capture_memory_metadata_preserves_source_bytes_and_unrelated_metadata() {
|
||||
let mut metadata = Some(json!({
|
||||
"trace_id": "unchanged",
|
||||
"body_capture": {
|
||||
"request": {"state": "complete", "source_bytes": 42, "stored_bytes": 42, "extra": true},
|
||||
"response": {"state": "complete", "source_bytes": 7}
|
||||
}
|
||||
}));
|
||||
mark_usage_capture_memory_omitted(&mut metadata, "request");
|
||||
assert_eq!(
|
||||
metadata,
|
||||
Some(json!({
|
||||
"trace_id": "unchanged",
|
||||
"body_capture": {
|
||||
"request": {
|
||||
"state": "truncated",
|
||||
"source_bytes": 42,
|
||||
"stored_bytes": 0,
|
||||
"reason": "usage_event_memory_budget_exceeded"
|
||||
},
|
||||
"response": {"state": "complete", "source_bytes": 7}
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_capture_memory_metadata_creates_missing_objects_and_replaces_entries() {
|
||||
for mut metadata in [
|
||||
None,
|
||||
Some(json!({})),
|
||||
Some(json!({"body_capture": {}})),
|
||||
Some(json!({"body_capture": {"request": null}})),
|
||||
Some(json!({"body_capture": {"request": "legacy"}})),
|
||||
Some(json!({"body_capture": {"request": {"source_bytes": "42"}}})),
|
||||
Some(json!({"body_capture": {"request": {"source_bytes": -1}}})),
|
||||
] {
|
||||
mark_usage_capture_memory_omitted(&mut metadata, "request");
|
||||
assert_eq!(
|
||||
metadata,
|
||||
Some(json!({"body_capture": {"request": {
|
||||
"state": "truncated",
|
||||
"stored_bytes": 0,
|
||||
"reason": "usage_event_memory_budget_exceeded"
|
||||
}}}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_capture_memory_metadata_preserves_existing_non_object_containers() {
|
||||
for metadata in [
|
||||
Value::Null,
|
||||
json!(false),
|
||||
json!(7),
|
||||
json!("legacy"),
|
||||
json!([]),
|
||||
json!({"body_capture": null}),
|
||||
json!({"body_capture": false}),
|
||||
json!({"body_capture": 7}),
|
||||
json!({"body_capture": "legacy"}),
|
||||
json!({"body_capture": []}),
|
||||
] {
|
||||
let mut actual = Some(metadata.clone());
|
||||
mark_usage_capture_memory_omitted(&mut actual, "request");
|
||||
assert_eq!(actual, Some(metadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
mod capture_memory;
|
||||
mod compression;
|
||||
mod metadata_policy;
|
||||
mod policy;
|
||||
mod types;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use capture_memory::{
|
||||
mark_usage_capture_memory_omitted, usage_json_heap_estimate, UsageCaptureMemoryBudget,
|
||||
UsageCaptureRetention,
|
||||
};
|
||||
pub use compression::{read_decompressed_usage_json, MAX_DECOMPRESSED_USAGE_JSON_BYTES};
|
||||
pub use metadata_policy::*;
|
||||
pub use policy::*;
|
||||
|
||||
@@ -673,6 +673,7 @@ mod tests {
|
||||
|
||||
fn usage_with_http_capture() -> UpsertUsageRecord {
|
||||
UpsertUsageRecord {
|
||||
capture_retention: Default::default(),
|
||||
request_id: "req-sensitive-capture".to_string(),
|
||||
user_id: Some("user-1".to_string()),
|
||||
api_key_id: Some("key-1".to_string()),
|
||||
|
||||
@@ -54,11 +54,11 @@ pub fn extract_provider_reasoning_effort_from_body(value: Option<&Value>) -> Opt
|
||||
}
|
||||
|
||||
fn normalize_provider_reasoning_effort(value: &str) -> Option<String> {
|
||||
let normalized = value.trim().to_ascii_lowercase();
|
||||
if normalized.is_empty() || normalized.len() > 64 {
|
||||
let value = value.trim();
|
||||
if value.is_empty() || value.len() > 64 {
|
||||
return None;
|
||||
}
|
||||
Some(normalized)
|
||||
Some(value.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
pub fn extract_provider_service_tier_from_body(value: Option<&Value>) -> Option<String> {
|
||||
@@ -112,11 +112,11 @@ pub fn extract_provider_actual_service_tier_from_response(value: Option<&Value>)
|
||||
}
|
||||
|
||||
pub fn normalize_provider_service_tier(value: &str) -> Option<String> {
|
||||
let normalized = value.trim().to_ascii_lowercase();
|
||||
if normalized.is_empty() || normalized.len() > 64 {
|
||||
let value = value.trim();
|
||||
if value.is_empty() || value.len() > 64 {
|
||||
return None;
|
||||
}
|
||||
Some(normalized)
|
||||
Some(value.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// Resolves a provider processing tier exclusively from the final upstream request.
|
||||
@@ -1980,7 +1980,7 @@ pub trait UsageReadRepository: Send + Sync {
|
||||
/// Request/response headers and bodies here are capture inputs that the repository persists into
|
||||
/// the dedicated HTTP audit/body stores. Deprecated mirror columns on `public.usage` remain in the
|
||||
/// schema for compatibility only and are not the intended long-term destination for new writes.
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UpsertUsageRecord {
|
||||
pub request_id: String,
|
||||
pub user_id: Option<String>,
|
||||
@@ -2055,6 +2055,142 @@ pub struct UpsertUsageRecord {
|
||||
pub finalized_at_unix_secs: Option<u64>,
|
||||
pub created_at_unix_ms: Option<u64>,
|
||||
pub updated_at_unix_secs: u64,
|
||||
#[doc(hidden)]
|
||||
#[serde(skip)]
|
||||
pub capture_retention: super::UsageCaptureRetention,
|
||||
}
|
||||
|
||||
impl Clone for UpsertUsageRecord {
|
||||
fn clone(&self) -> Self {
|
||||
let (capture_retention, retain_bodies) = self.capture_retention.clone_for_bodies(|| {
|
||||
[
|
||||
&self.request_body,
|
||||
&self.provider_request_body,
|
||||
&self.response_body,
|
||||
&self.client_response_body,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.fold(0usize, |bytes, body| {
|
||||
bytes
|
||||
.saturating_add(std::mem::size_of::<Value>())
|
||||
.saturating_add(super::usage_json_heap_estimate(body))
|
||||
})
|
||||
});
|
||||
let mut cloned = Self {
|
||||
request_id: self.request_id.clone(),
|
||||
user_id: self.user_id.clone(),
|
||||
api_key_id: self.api_key_id.clone(),
|
||||
username: self.username.clone(),
|
||||
api_key_name: self.api_key_name.clone(),
|
||||
provider_name: self.provider_name.clone(),
|
||||
model: self.model.clone(),
|
||||
target_model: self.target_model.clone(),
|
||||
provider_id: self.provider_id.clone(),
|
||||
provider_endpoint_id: self.provider_endpoint_id.clone(),
|
||||
provider_api_key_id: self.provider_api_key_id.clone(),
|
||||
request_type: self.request_type.clone(),
|
||||
api_format: self.api_format.clone(),
|
||||
api_family: self.api_family.clone(),
|
||||
endpoint_kind: self.endpoint_kind.clone(),
|
||||
endpoint_api_format: self.endpoint_api_format.clone(),
|
||||
provider_api_family: self.provider_api_family.clone(),
|
||||
provider_endpoint_kind: self.provider_endpoint_kind.clone(),
|
||||
has_format_conversion: self.has_format_conversion,
|
||||
is_stream: self.is_stream,
|
||||
input_tokens: self.input_tokens,
|
||||
output_tokens: self.output_tokens,
|
||||
total_tokens: self.total_tokens,
|
||||
cache_creation_input_tokens: self.cache_creation_input_tokens,
|
||||
cache_creation_ephemeral_5m_input_tokens: self.cache_creation_ephemeral_5m_input_tokens,
|
||||
cache_creation_ephemeral_1h_input_tokens: self.cache_creation_ephemeral_1h_input_tokens,
|
||||
cache_read_input_tokens: self.cache_read_input_tokens,
|
||||
cache_creation_cost_usd: self.cache_creation_cost_usd,
|
||||
cache_read_cost_usd: self.cache_read_cost_usd,
|
||||
output_price_per_1m: self.output_price_per_1m,
|
||||
total_cost_usd: self.total_cost_usd,
|
||||
actual_total_cost_usd: self.actual_total_cost_usd,
|
||||
status_code: self.status_code,
|
||||
error_message: self.error_message.clone(),
|
||||
error_category: self.error_category.clone(),
|
||||
response_time_ms: self.response_time_ms,
|
||||
first_byte_time_ms: self.first_byte_time_ms,
|
||||
status: self.status.clone(),
|
||||
billing_status: self.billing_status.clone(),
|
||||
request_headers: self.request_headers.clone(),
|
||||
request_body: retain_bodies.then(|| self.request_body.clone()).flatten(),
|
||||
request_body_ref: self.request_body_ref.clone(),
|
||||
request_body_state: self.request_body_state,
|
||||
provider_request_headers: self.provider_request_headers.clone(),
|
||||
provider_request_body: retain_bodies
|
||||
.then(|| self.provider_request_body.clone())
|
||||
.flatten(),
|
||||
provider_request_body_ref: self.provider_request_body_ref.clone(),
|
||||
provider_request_body_state: self.provider_request_body_state,
|
||||
response_headers: self.response_headers.clone(),
|
||||
response_body: retain_bodies.then(|| self.response_body.clone()).flatten(),
|
||||
response_body_ref: self.response_body_ref.clone(),
|
||||
response_body_state: self.response_body_state,
|
||||
client_response_headers: self.client_response_headers.clone(),
|
||||
client_response_body: retain_bodies
|
||||
.then(|| self.client_response_body.clone())
|
||||
.flatten(),
|
||||
client_response_body_ref: self.client_response_body_ref.clone(),
|
||||
client_response_body_state: self.client_response_body_state,
|
||||
candidate_id: self.candidate_id.clone(),
|
||||
candidate_index: self.candidate_index,
|
||||
key_name: self.key_name.clone(),
|
||||
planner_kind: self.planner_kind.clone(),
|
||||
route_family: self.route_family.clone(),
|
||||
route_kind: self.route_kind.clone(),
|
||||
execution_path: self.execution_path.clone(),
|
||||
local_execution_runtime_miss_reason: self.local_execution_runtime_miss_reason.clone(),
|
||||
request_metadata: self.request_metadata.clone(),
|
||||
finalized_at_unix_secs: self.finalized_at_unix_secs,
|
||||
created_at_unix_ms: self.created_at_unix_ms,
|
||||
updated_at_unix_secs: self.updated_at_unix_secs,
|
||||
capture_retention,
|
||||
};
|
||||
if !retain_bodies {
|
||||
for (present, key, state) in [
|
||||
(
|
||||
self.request_body.is_some(),
|
||||
"request",
|
||||
&mut cloned.request_body_state,
|
||||
),
|
||||
(
|
||||
self.provider_request_body.is_some(),
|
||||
"provider_request",
|
||||
&mut cloned.provider_request_body_state,
|
||||
),
|
||||
(
|
||||
self.response_body.is_some(),
|
||||
"response",
|
||||
&mut cloned.response_body_state,
|
||||
),
|
||||
(
|
||||
self.client_response_body.is_some(),
|
||||
"client_response",
|
||||
&mut cloned.client_response_body_state,
|
||||
),
|
||||
] {
|
||||
if present
|
||||
&& !matches!(
|
||||
state,
|
||||
Some(
|
||||
UsageBodyCaptureState::None
|
||||
| UsageBodyCaptureState::Disabled
|
||||
| UsageBodyCaptureState::Unavailable
|
||||
)
|
||||
)
|
||||
{
|
||||
*state = Some(UsageBodyCaptureState::Truncated);
|
||||
super::mark_usage_capture_memory_omitted(&mut cloned.request_metadata, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
cloned
|
||||
}
|
||||
}
|
||||
|
||||
impl UpsertUsageRecord {
|
||||
@@ -2501,15 +2637,59 @@ fn parse_timestamp(value: i64, field_name: &str) -> Result<u64, crate::DataLayer
|
||||
mod tests {
|
||||
use super::{
|
||||
canonical_usage_body_ref_for, extract_provider_actual_service_tier_from_response,
|
||||
extract_provider_service_tier_from_body, resolve_provider_cache_ttl_minutes,
|
||||
usage_body_ref, StoredRequestUsageAudit, UpsertUsageRecord, UsageBodyCaptureState,
|
||||
UsageBodyCaptureStorage, UsageBodyField, UsageProviderPerformanceQuery,
|
||||
REALTIME_SESSION_METADATA_KEY, USAGE_AVAILABLE_METADATA_KEY,
|
||||
USAGE_PRICING_AVAILABLE_METADATA_KEY, WEBSOCKET_MODE_METADATA_KEY,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
extract_provider_service_tier_from_body, normalize_provider_reasoning_effort,
|
||||
normalize_provider_service_tier, resolve_provider_cache_ttl_minutes, usage_body_ref,
|
||||
StoredRequestUsageAudit, UpsertUsageRecord, UsageBodyCaptureState, UsageBodyCaptureStorage,
|
||||
UsageBodyField, UsageProviderPerformanceQuery, REALTIME_SESSION_METADATA_KEY,
|
||||
USAGE_AVAILABLE_METADATA_KEY, USAGE_PRICING_AVAILABLE_METADATA_KEY,
|
||||
WEBSOCKET_MODE_METADATA_KEY, WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[test]
|
||||
fn provider_fact_normalization_preserves_trimmed_byte_limit() {
|
||||
for normalize in [
|
||||
normalize_provider_reasoning_effort as fn(&str) -> Option<String>,
|
||||
normalize_provider_service_tier,
|
||||
] {
|
||||
assert_eq!(normalize(" \t\r\n"), None);
|
||||
assert_eq!(normalize(" HIGH\n"), Some("high".to_string()));
|
||||
assert_eq!(normalize(&"A".repeat(64)), Some("a".repeat(64)));
|
||||
assert_eq!(
|
||||
normalize(&format!(" \t{}\n", "A".repeat(64))),
|
||||
Some("a".repeat(64))
|
||||
);
|
||||
assert_eq!(normalize(&"A".repeat(65)), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_fact_normalization_preserves_non_ascii_case_and_byte_count() {
|
||||
for normalize in [
|
||||
normalize_provider_reasoning_effort as fn(&str) -> Option<String>,
|
||||
normalize_provider_service_tier,
|
||||
] {
|
||||
let accepted = format!("{}A", "\u{00c9}".repeat(31));
|
||||
assert_eq!(
|
||||
normalize(&accepted),
|
||||
Some(format!("{}a", "\u{00c9}".repeat(31)))
|
||||
);
|
||||
assert_eq!(
|
||||
normalize(&"\u{00c9}".repeat(32)),
|
||||
Some("\u{00c9}".repeat(32))
|
||||
);
|
||||
assert_eq!(normalize(&format!("{}A", "\u{00c9}".repeat(32))), None);
|
||||
assert_eq!(normalize("\u{2003}FAST\u{2003}"), Some("fast".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_fact_normalization_rejects_large_input_before_copying() {
|
||||
let oversized = "A".repeat(4 * 1024 * 1024);
|
||||
assert_eq!(normalize_provider_reasoning_effort(&oversized), None);
|
||||
assert_eq!(normalize_provider_service_tier(&oversized), None);
|
||||
}
|
||||
|
||||
fn sample_usage() -> StoredRequestUsageAudit {
|
||||
StoredRequestUsageAudit::new(
|
||||
"usage-1".to_string(),
|
||||
@@ -2700,6 +2880,7 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_invalid_upsert_payload() {
|
||||
let mut record = UpsertUsageRecord {
|
||||
capture_retention: Default::default(),
|
||||
request_id: "".to_string(),
|
||||
user_id: None,
|
||||
api_key_id: None,
|
||||
|
||||
Reference in New Issue
Block a user