mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 00:17:45 +08:00
1009 lines
40 KiB
Rust
1009 lines
40 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::io::{self, Write};
|
|
|
|
use aether_data_contracts::repository::usage::{
|
|
resolve_provider_cache_ttl_minutes, UsageBodyCaptureState,
|
|
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY, PROVIDER_REASONING_EFFORT_METADATA_KEY,
|
|
PROVIDER_SERVICE_TIER_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
|
};
|
|
use aether_data_contracts::DataLayerError;
|
|
use serde::ser::{Impossible, SerializeMap, SerializeStruct};
|
|
use serde::{Serialize, Serializer};
|
|
use serde_json::Value;
|
|
|
|
use super::{BorrowedUsageEventEnvelope, UsageEvent, UsageEventData, USAGE_EVENT_VERSION};
|
|
use crate::body_capture::mark_usage_event_capture_truncated;
|
|
use crate::request_metadata::{
|
|
attach_client_request_body_metadata, attach_provider_request_body_metadata,
|
|
attach_provider_response_body_metadata, attach_provider_response_model_metadata,
|
|
clear_client_request_body_metadata, clear_provider_request_body_metadata,
|
|
request_body_derived_facts_action, RequestBodyDerivedFactsAction,
|
|
};
|
|
|
|
const DIAGNOSTIC_FIELDS: [&str; 8] = [
|
|
"request_body",
|
|
"provider_request_body",
|
|
"response_body",
|
|
"client_response_body",
|
|
"request_headers",
|
|
"provider_request_headers",
|
|
"response_headers",
|
|
"client_response_headers",
|
|
];
|
|
const BODY_STATE_FIELDS: [&str; 4] = [
|
|
"request_body_state",
|
|
"provider_request_body_state",
|
|
"response_body_state",
|
|
"client_response_body_state",
|
|
];
|
|
const BODY_METADATA_KEYS: [&str; 4] =
|
|
["request", "provider_request", "response", "client_response"];
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct EncodedUsageEvent {
|
|
pub(crate) fields: BTreeMap<String, String>,
|
|
pub(crate) diagnostics_omitted: bool,
|
|
}
|
|
|
|
pub(super) fn encode(
|
|
event: &UsageEvent,
|
|
max_bytes: usize,
|
|
) -> Result<EncodedUsageEvent, DataLayerError> {
|
|
let mut writer = BoundedJsonWriter::new(max_bytes);
|
|
if writer.serialize(&envelope(event, &event.data))? {
|
|
return writer.into_event(false);
|
|
}
|
|
|
|
// Conservatively reject oversized original metadata before cloning it, even
|
|
// when later fact normalization could make that metadata smaller.
|
|
let core = ProjectedData {
|
|
data: &event.data,
|
|
overrides: None,
|
|
};
|
|
if !writer.serialize(&envelope(event, &core))? {
|
|
return Err(wire_limit_error(max_bytes));
|
|
}
|
|
|
|
let overrides = WireOverrides::new(&event.data)?;
|
|
let projected = ProjectedData {
|
|
data: &event.data,
|
|
overrides: Some(&overrides),
|
|
};
|
|
if !writer.serialize(&envelope(event, &projected))? {
|
|
return Err(wire_limit_error(max_bytes));
|
|
}
|
|
writer.into_event(true)
|
|
}
|
|
|
|
fn envelope<'a, T: Serialize + ?Sized>(
|
|
event: &'a UsageEvent,
|
|
data: &'a T,
|
|
) -> BorrowedUsageEventEnvelope<'a, T> {
|
|
BorrowedUsageEventEnvelope {
|
|
v: USAGE_EVENT_VERSION,
|
|
event_type: event.event_type,
|
|
request_id: &event.request_id,
|
|
timestamp_ms: event.timestamp_ms,
|
|
data,
|
|
}
|
|
}
|
|
|
|
fn wire_limit_error(max_bytes: usize) -> DataLayerError {
|
|
DataLayerError::InvalidInput(format!(
|
|
"usage event exceeds the {max_bytes}-byte wire limit after omitting diagnostic bodies and headers"
|
|
))
|
|
}
|
|
|
|
struct BoundedJsonWriter {
|
|
bytes: Vec<u8>,
|
|
max_bytes: usize,
|
|
exceeded: bool,
|
|
}
|
|
|
|
impl BoundedJsonWriter {
|
|
fn new(max_bytes: usize) -> Self {
|
|
Self {
|
|
bytes: Vec::new(),
|
|
max_bytes,
|
|
exceeded: false,
|
|
}
|
|
}
|
|
|
|
fn serialize<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<bool, DataLayerError> {
|
|
self.bytes.clear();
|
|
self.exceeded = false;
|
|
match serde_json::to_writer(&mut *self, value) {
|
|
Ok(()) => Ok(true),
|
|
Err(_) if self.exceeded => Ok(false),
|
|
Err(error) => Err(DataLayerError::UnexpectedValue(format!(
|
|
"failed to serialize usage event payload: {error}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn into_event(self, diagnostics_omitted: bool) -> Result<EncodedUsageEvent, DataLayerError> {
|
|
let payload = String::from_utf8(self.bytes).map_err(|error| {
|
|
DataLayerError::UnexpectedValue(format!("usage event JSON was not UTF-8: {error}"))
|
|
})?;
|
|
Ok(EncodedUsageEvent {
|
|
fields: BTreeMap::from([("payload".to_string(), payload)]),
|
|
diagnostics_omitted,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Write for BoundedJsonWriter {
|
|
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
|
if bytes.len() > self.max_bytes.saturating_sub(self.bytes.len()) {
|
|
self.exceeded = true;
|
|
return Err(io::Error::other("usage event wire limit exceeded"));
|
|
}
|
|
let required = self.bytes.len() + bytes.len();
|
|
if required > self.bytes.capacity() {
|
|
let capacity = required
|
|
.max(self.bytes.capacity().saturating_mul(2))
|
|
.min(self.max_bytes);
|
|
self.bytes
|
|
.try_reserve_exact(capacity - self.bytes.len())
|
|
.map_err(|error| {
|
|
io::Error::other(format!("usage event wire allocation failed: {error}"))
|
|
})?;
|
|
}
|
|
self.bytes.extend_from_slice(bytes);
|
|
Ok(bytes.len())
|
|
}
|
|
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct WireOverrides {
|
|
truncated: [bool; 4],
|
|
metadata: Option<Value>,
|
|
}
|
|
|
|
impl WireOverrides {
|
|
fn new(data: &UsageEventData) -> Result<Self, DataLayerError> {
|
|
// The full v1 consumer decodes a JSON null Option<Value> as no body.
|
|
let request_body = data.request_body.as_ref().filter(|body| !body.is_null());
|
|
let provider_request_body = data
|
|
.provider_request_body
|
|
.as_ref()
|
|
.filter(|body| !body.is_null());
|
|
let body_cache_ttl = resolve_provider_cache_ttl_minutes(
|
|
data.endpoint_api_format
|
|
.as_deref()
|
|
.or(data.api_format.as_deref()),
|
|
data.target_model.as_deref().or(Some(data.model.as_str())),
|
|
Some(data.model.as_str()),
|
|
provider_request_body,
|
|
);
|
|
if body_cache_ttl.is_some()
|
|
&& data.provider_request_body_state == Some(UsageBodyCaptureState::None)
|
|
{
|
|
return Err(DataLayerError::InvalidInput(
|
|
"usage event cannot omit a provider request body whose cache TTL would be cleared by its explicit none capture state"
|
|
.to_string(),
|
|
));
|
|
}
|
|
let mut metadata = data.request_metadata.clone();
|
|
match request_body_derived_facts_action(request_body, data.request_body_state) {
|
|
RequestBodyDerivedFactsAction::Refresh => {
|
|
if request_body.is_some_and(|body| !body.is_object()) {
|
|
if let Some(Value::Object(object)) = metadata.as_mut() {
|
|
object.remove(REQUESTED_REASONING_EFFORT_METADATA_KEY);
|
|
}
|
|
} else {
|
|
metadata = attach_client_request_body_metadata(metadata, request_body);
|
|
}
|
|
}
|
|
RequestBodyDerivedFactsAction::Clear
|
|
if request_body.is_some() || data.request_body_state.is_some() =>
|
|
{
|
|
metadata = clear_client_request_body_metadata(metadata);
|
|
}
|
|
RequestBodyDerivedFactsAction::Clear | RequestBodyDerivedFactsAction::Preserve => {}
|
|
}
|
|
match request_body_derived_facts_action(
|
|
provider_request_body,
|
|
data.provider_request_body_state,
|
|
) {
|
|
RequestBodyDerivedFactsAction::Refresh => {
|
|
if provider_request_body.is_some_and(|body| !body.is_object()) {
|
|
// An authoritative scalar/array has no tier or reasoning,
|
|
// but billing still falls back to the metadata's cache TTL.
|
|
if let Some(Value::Object(object)) = metadata.as_mut() {
|
|
object.remove(PROVIDER_REASONING_EFFORT_METADATA_KEY);
|
|
object.remove(PROVIDER_SERVICE_TIER_METADATA_KEY);
|
|
}
|
|
} else {
|
|
metadata = attach_provider_request_body_metadata(
|
|
metadata,
|
|
data.endpoint_api_format
|
|
.as_deref()
|
|
.or(data.api_format.as_deref()),
|
|
data.target_model.as_deref().or(Some(data.model.as_str())),
|
|
Some(data.model.as_str()),
|
|
provider_request_body,
|
|
);
|
|
}
|
|
}
|
|
RequestBodyDerivedFactsAction::Clear
|
|
if provider_request_body.is_some()
|
|
|| data.provider_request_body_state.is_some() =>
|
|
{
|
|
metadata = clear_provider_request_body_metadata(metadata);
|
|
}
|
|
RequestBodyDerivedFactsAction::Clear | RequestBodyDerivedFactsAction::Preserve => {}
|
|
}
|
|
metadata = attach_provider_response_body_metadata(metadata, data.response_body.as_ref());
|
|
metadata = attach_provider_response_model_metadata(
|
|
metadata,
|
|
request_body,
|
|
data.request_body_state,
|
|
data.api_format.as_deref(),
|
|
data.response_body.as_ref(),
|
|
data.response_body_state,
|
|
data.endpoint_api_format.as_deref(),
|
|
);
|
|
// Billing reads raw-body TTL before metadata regardless of capture state.
|
|
// Preserve that precedence independently of reasoning and tier authority.
|
|
if let Some(cache_ttl) = body_cache_ttl {
|
|
let object = metadata
|
|
.get_or_insert_with(|| Value::Object(serde_json::Map::new()))
|
|
.as_object_mut()
|
|
.ok_or_else(|| {
|
|
DataLayerError::InvalidInput(
|
|
"usage event cannot preserve provider cache TTL in non-object metadata after omitting diagnostic bodies"
|
|
.to_string(),
|
|
)
|
|
})?;
|
|
object.insert(
|
|
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY.to_string(),
|
|
Value::Number(cache_ttl.into()),
|
|
);
|
|
}
|
|
let truncated = [
|
|
(data.request_body.as_ref(), data.request_body_state),
|
|
(
|
|
data.provider_request_body.as_ref(),
|
|
data.provider_request_body_state,
|
|
),
|
|
(data.response_body.as_ref(), data.response_body_state),
|
|
(
|
|
data.client_response_body.as_ref(),
|
|
data.client_response_body_state,
|
|
),
|
|
]
|
|
.map(|(body, state)| {
|
|
body.is_some_and(|body| !body.is_null())
|
|
&& !matches!(
|
|
state,
|
|
Some(
|
|
UsageBodyCaptureState::None
|
|
| UsageBodyCaptureState::Disabled
|
|
| UsageBodyCaptureState::Unavailable
|
|
)
|
|
)
|
|
});
|
|
for (truncated, key) in truncated.into_iter().zip(BODY_METADATA_KEYS) {
|
|
if !truncated {
|
|
continue;
|
|
}
|
|
mark_usage_event_capture_truncated(&mut metadata, key);
|
|
if let Some(entry) = metadata
|
|
.as_mut()
|
|
.and_then(|value| value.get_mut("body_capture"))
|
|
.and_then(|value| value.get_mut(key))
|
|
.and_then(Value::as_object_mut)
|
|
{
|
|
entry.insert(
|
|
"reason".to_string(),
|
|
Value::String("wire_limit_exceeded".to_string()),
|
|
);
|
|
}
|
|
}
|
|
Ok(Self {
|
|
truncated,
|
|
metadata,
|
|
})
|
|
}
|
|
}
|
|
|
|
struct ProjectedData<'a> {
|
|
data: &'a UsageEventData,
|
|
overrides: Option<&'a WireOverrides>,
|
|
}
|
|
|
|
impl Serialize for ProjectedData<'_> {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
self.data.serialize(FieldProjectionSerializer {
|
|
map: serializer.serialize_map(None)?,
|
|
overrides: self.overrides,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Reuse UsageEventData's derived field traversal, including future fields and
|
|
// skip_serializing_if rules. Only diagnostic fields and explicit overrides differ.
|
|
struct FieldProjectionSerializer<'a, M> {
|
|
map: M,
|
|
overrides: Option<&'a WireOverrides>,
|
|
}
|
|
|
|
impl<M: SerializeMap> SerializeStruct for FieldProjectionSerializer<'_, M> {
|
|
type Ok = M::Ok;
|
|
type Error = M::Error;
|
|
|
|
fn serialize_field<T: Serialize + ?Sized>(
|
|
&mut self,
|
|
key: &'static str,
|
|
value: &T,
|
|
) -> Result<(), Self::Error> {
|
|
if DIAGNOSTIC_FIELDS.contains(&key) {
|
|
return Ok(());
|
|
}
|
|
if let Some(overrides) = self.overrides {
|
|
if key == "request_metadata"
|
|
|| BODY_STATE_FIELDS
|
|
.iter()
|
|
.zip(overrides.truncated)
|
|
.any(|(state, truncated)| *state == key && truncated)
|
|
{
|
|
return Ok(());
|
|
}
|
|
}
|
|
self.map.serialize_entry(key, value)
|
|
}
|
|
|
|
fn end(mut self) -> Result<Self::Ok, Self::Error> {
|
|
if let Some(overrides) = self.overrides {
|
|
for (key, truncated) in BODY_STATE_FIELDS.into_iter().zip(overrides.truncated) {
|
|
if truncated {
|
|
self.map
|
|
.serialize_entry(key, &UsageBodyCaptureState::Truncated)?;
|
|
}
|
|
}
|
|
if let Some(metadata) = overrides.metadata.as_ref() {
|
|
self.map.serialize_entry("request_metadata", metadata)?;
|
|
}
|
|
}
|
|
self.map.end()
|
|
}
|
|
}
|
|
|
|
fn expected_struct<E: serde::ser::Error, T>() -> Result<T, E> {
|
|
Err(E::custom("usage event data must serialize as a struct"))
|
|
}
|
|
|
|
macro_rules! reject_scalar_serialization {
|
|
($($name:ident($value:ident: $ty:ty)),* $(,)?) => {
|
|
$(fn $name(self, $value: $ty) -> Result<Self::Ok, Self::Error> {
|
|
let _ = $value;
|
|
expected_struct()
|
|
})*
|
|
};
|
|
}
|
|
|
|
impl<M: SerializeMap> Serializer for FieldProjectionSerializer<'_, M> {
|
|
type Ok = M::Ok;
|
|
type Error = M::Error;
|
|
type SerializeSeq = Impossible<Self::Ok, Self::Error>;
|
|
type SerializeTuple = Impossible<Self::Ok, Self::Error>;
|
|
type SerializeTupleStruct = Impossible<Self::Ok, Self::Error>;
|
|
type SerializeTupleVariant = Impossible<Self::Ok, Self::Error>;
|
|
type SerializeMap = Impossible<Self::Ok, Self::Error>;
|
|
type SerializeStruct = Self;
|
|
type SerializeStructVariant = Impossible<Self::Ok, Self::Error>;
|
|
|
|
reject_scalar_serialization! {
|
|
serialize_bool(value: bool), serialize_i8(value: i8), serialize_i16(value: i16),
|
|
serialize_i32(value: i32), serialize_i64(value: i64), serialize_i128(value: i128),
|
|
serialize_u8(value: u8), serialize_u16(value: u16), serialize_u32(value: u32),
|
|
serialize_u64(value: u64), serialize_u128(value: u128), serialize_f32(value: f32),
|
|
serialize_f64(value: f64), serialize_char(value: char), serialize_str(value: &str),
|
|
serialize_bytes(value: &[u8]),
|
|
}
|
|
|
|
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_some<T: Serialize + ?Sized>(self, _: &T) -> Result<Self::Ok, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_unit_struct(self, _: &'static str) -> Result<Self::Ok, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_unit_variant(
|
|
self,
|
|
_: &'static str,
|
|
_: u32,
|
|
_: &'static str,
|
|
) -> Result<Self::Ok, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_newtype_struct<T: Serialize + ?Sized>(
|
|
self,
|
|
_: &'static str,
|
|
_: &T,
|
|
) -> Result<Self::Ok, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_newtype_variant<T: Serialize + ?Sized>(
|
|
self,
|
|
_: &'static str,
|
|
_: u32,
|
|
_: &'static str,
|
|
_: &T,
|
|
) -> Result<Self::Ok, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_seq(self, _: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_tuple(self, _: usize) -> Result<Self::SerializeTuple, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_tuple_struct(
|
|
self,
|
|
_: &'static str,
|
|
_: usize,
|
|
) -> Result<Self::SerializeTupleStruct, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_tuple_variant(
|
|
self,
|
|
_: &'static str,
|
|
_: u32,
|
|
_: &'static str,
|
|
_: usize,
|
|
) -> Result<Self::SerializeTupleVariant, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_map(self, _: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
fn serialize_struct(
|
|
self,
|
|
_: &'static str,
|
|
_: usize,
|
|
) -> Result<Self::SerializeStruct, Self::Error> {
|
|
Ok(self)
|
|
}
|
|
fn serialize_struct_variant(
|
|
self,
|
|
_: &'static str,
|
|
_: u32,
|
|
_: &'static str,
|
|
_: usize,
|
|
) -> Result<Self::SerializeStructVariant, Self::Error> {
|
|
expected_struct()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::Arc;
|
|
|
|
use serde_json::json;
|
|
|
|
use super::*;
|
|
use crate::event::UsageEventType;
|
|
use crate::event_capture_budget::EventCaptureMemoryBudget;
|
|
|
|
fn event() -> UsageEvent {
|
|
UsageEvent {
|
|
event_type: UsageEventType::Completed,
|
|
request_id: "wire-request".to_string(),
|
|
timestamp_ms: 1_234_567,
|
|
data: UsageEventData {
|
|
provider_name: "provider".to_string(),
|
|
model: "gpt-5.6-sol".to_string(),
|
|
endpoint_api_format: Some("openai:responses".to_string()),
|
|
user_id: Some("user-id".to_string()),
|
|
api_key_id: Some("key-id".to_string()),
|
|
provider_id: Some("provider-id".to_string()),
|
|
provider_endpoint_id: Some("endpoint-id".to_string()),
|
|
provider_api_key_id: Some("provider-key-id".to_string()),
|
|
input_tokens: Some(100),
|
|
output_tokens: Some(500),
|
|
total_tokens: Some(600),
|
|
cache_read_input_tokens: Some(0),
|
|
cache_creation_input_tokens: Some(25),
|
|
cache_creation_ephemeral_5m_input_tokens: Some(0),
|
|
cache_creation_ephemeral_1h_input_tokens: Some(25),
|
|
cache_read_cost_usd: Some(0.0),
|
|
total_cost_usd: Some(1.25),
|
|
actual_total_cost_usd: Some(1.25),
|
|
status_code: Some(200),
|
|
is_stream: Some(false),
|
|
candidate_index: Some(0),
|
|
first_byte_time_ms: Some(0),
|
|
error_message: Some(String::new()),
|
|
request_metadata: Some(json!({
|
|
"plan_usage_reservation_token": "550e8400-e29b-41d4-a716-446655440000",
|
|
"dimensions": {"image_count": 2, "size": "1024x1024", "quality": "high"},
|
|
"usage_available": true,
|
|
"usage_pricing_available": true
|
|
})),
|
|
..UsageEventData::default()
|
|
},
|
|
}
|
|
}
|
|
|
|
fn wire_value(encoded: &EncodedUsageEvent) -> Value {
|
|
serde_json::from_str(&encoded.fields["payload"]).expect("complete JSON wire payload")
|
|
}
|
|
|
|
#[test]
|
|
fn event_wire_exact_limit_accounts_for_json_escaping_and_utf8() {
|
|
let mut event = event();
|
|
event.request_id = "escaped\0\n\r\t\"\\\u{03bb}\u{1f600}".to_string();
|
|
let original = event.to_stream_fields().expect("original wire payload");
|
|
let length = original["payload"].len();
|
|
for limit in [length, length + 1] {
|
|
let encoded = event.to_bounded_stream_fields(limit).expect("exact fit");
|
|
assert_eq!(encoded.fields, original);
|
|
assert!(!encoded.diagnostics_omitted);
|
|
}
|
|
for limit in [0, 1, length - 1] {
|
|
assert!(matches!(
|
|
event.to_bounded_stream_fields(limit),
|
|
Err(DataLayerError::InvalidInput(_))
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn event_wire_writer_does_not_append_past_limit_and_reuses_failed_buffer() {
|
|
let mut writer = BoundedJsonWriter::new(5);
|
|
writer.write_all(b"12345").expect("exact fit");
|
|
assert!(writer.write_all(b"6").is_err());
|
|
assert_eq!(writer.bytes, b"12345");
|
|
assert!(writer.exceeded);
|
|
assert!(!writer.serialize(&"\u{0000}").expect("size rejection"));
|
|
assert!(writer.bytes.len() <= 5);
|
|
assert!(writer.serialize(&"\u{03bb}").expect("valid UTF-8 retry"));
|
|
assert_eq!(writer.bytes, "\"\u{03bb}\"".as_bytes());
|
|
assert!(!writer.exceeded);
|
|
}
|
|
|
|
#[test]
|
|
fn event_wire_projection_transparently_forwards_derived_fields() {
|
|
#[derive(Serialize)]
|
|
struct FutureFields<'a> {
|
|
request_body: &'a Value,
|
|
new_billing_field: &'a Value,
|
|
zero_count: u64,
|
|
enabled: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
missing_field: Option<&'a str>,
|
|
}
|
|
let diagnostic = json!({"large": "x".repeat(8_192)});
|
|
let billing = json!({"nested": [0, false, "unchanged"]});
|
|
let data = FutureFields {
|
|
request_body: &diagnostic,
|
|
new_billing_field: &billing,
|
|
zero_count: 0,
|
|
enabled: false,
|
|
missing_field: None,
|
|
};
|
|
let mut bytes = Vec::new();
|
|
let mut serializer = serde_json::Serializer::new(&mut bytes);
|
|
data.serialize(FieldProjectionSerializer {
|
|
map: (&mut serializer)
|
|
.serialize_map(None)
|
|
.expect("object serializer"),
|
|
overrides: None,
|
|
})
|
|
.expect("project derived fields");
|
|
assert_eq!(
|
|
serde_json::from_slice::<Value>(&bytes).expect("projected JSON"),
|
|
json!({"new_billing_field": billing, "zero_count": 0, "enabled": false})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn event_wire_omission_preserves_billing_facts_refs_and_source_ownership() {
|
|
let mut event = event();
|
|
let padding = "x".repeat(16_384);
|
|
event.data.request_body = Some(json!({"reasoning": {"effort": "high"}, "input": padding}));
|
|
event.data.provider_request_body = Some(
|
|
json!({"model": "gpt-5.6-sol", "reasoning": {"effort": "medium"}, "service_tier": "priority", "input": padding}),
|
|
);
|
|
event.data.response_body = Some(json!({"service_tier": "Default", "output": padding}));
|
|
event.data.client_response_body = Some(json!({"output": padding}));
|
|
event.data.request_headers = Some(json!({"x-request": padding}));
|
|
event.data.provider_request_headers = Some(json!({"x-provider": padding}));
|
|
event.data.response_headers = Some(json!({"x-response": padding}));
|
|
event.data.client_response_headers = Some(json!({"x-client": padding}));
|
|
event.data.request_body_ref = Some("usage://wire-request/request_body".to_string());
|
|
event.data.request_body_state = Some(UsageBodyCaptureState::Inline);
|
|
event.data.provider_request_body_state = Some(UsageBodyCaptureState::Inline);
|
|
event.data.response_body_state = Some(UsageBodyCaptureState::Inline);
|
|
event.data.request_metadata.as_mut().unwrap()["body_capture"] = json!({
|
|
"response": {"state": "inline", "source_bytes": 123_456}
|
|
});
|
|
let weight = event.data.capture_heap_estimate();
|
|
let budget = Arc::new(EventCaptureMemoryBudget::new(weight));
|
|
event.data.apply_capture_memory_budget(Arc::clone(&budget));
|
|
let before = serde_json::to_value(&event).expect("source snapshot");
|
|
let original_wire: Value =
|
|
serde_json::from_str(&event.to_stream_fields().unwrap()["payload"]).unwrap();
|
|
let encoded = event
|
|
.to_bounded_stream_fields(8_192)
|
|
.expect("diagnostic omission");
|
|
assert!(encoded.diagnostics_omitted);
|
|
assert!(encoded.fields["payload"].len() <= 8_192);
|
|
let value = wire_value(&encoded);
|
|
for field in DIAGNOSTIC_FIELDS {
|
|
assert!(
|
|
value["data"].get(field).is_none(),
|
|
"{field} must be omitted"
|
|
);
|
|
}
|
|
for (field, original) in original_wire["data"].as_object().unwrap() {
|
|
if !DIAGNOSTIC_FIELDS.contains(&field.as_str())
|
|
&& !BODY_STATE_FIELDS.contains(&field.as_str())
|
|
&& field != "request_metadata"
|
|
{
|
|
assert_eq!(&value["data"][field], original, "core field {field}");
|
|
}
|
|
}
|
|
for (field, key) in BODY_STATE_FIELDS.into_iter().zip(BODY_METADATA_KEYS) {
|
|
assert_eq!(value["data"][field], "truncated");
|
|
assert_eq!(
|
|
value["data"]["request_metadata"]["body_capture"][key]["reason"],
|
|
"wire_limit_exceeded"
|
|
);
|
|
assert_eq!(
|
|
value["data"]["request_metadata"]["body_capture"][key]["stored_bytes"],
|
|
0
|
|
);
|
|
}
|
|
let metadata = &value["data"]["request_metadata"];
|
|
assert_eq!(metadata["requested_reasoning_effort"], "high");
|
|
assert_eq!(metadata["provider_reasoning_effort"], "medium");
|
|
assert_eq!(metadata["provider_service_tier"], "priority");
|
|
assert_eq!(metadata["provider_actual_service_tier"], "default");
|
|
assert_eq!(metadata["provider_cache_ttl_minutes"], 30);
|
|
assert_eq!(
|
|
metadata["body_capture"]["response"]["source_bytes"],
|
|
123_456
|
|
);
|
|
assert_eq!(
|
|
metadata["dimensions"],
|
|
before["data"]["request_metadata"]["dimensions"]
|
|
);
|
|
assert_eq!(serde_json::to_value(&event).unwrap(), before);
|
|
assert_eq!(budget.retained_bytes(), weight);
|
|
assert_eq!(budget.downgraded_total(), 0);
|
|
|
|
let decoded = UsageEvent::from_stream_fields(&encoded.fields).expect("wire decode");
|
|
let record = crate::build_upsert_usage_record_from_event(&decoded).expect("record mapping");
|
|
assert_eq!(record.input_tokens, Some(100));
|
|
assert_eq!(record.output_tokens, Some(500));
|
|
assert_eq!(record.cache_read_input_tokens, Some(0));
|
|
assert_eq!(record.cache_creation_ephemeral_5m_input_tokens, Some(0));
|
|
assert_eq!(record.cache_creation_ephemeral_1h_input_tokens, Some(25));
|
|
assert_eq!(record.error_message.as_deref(), Some(""));
|
|
assert_eq!(record.request_body_ref, event.data.request_body_ref);
|
|
assert_eq!(
|
|
record.request_body_state,
|
|
Some(UsageBodyCaptureState::Truncated)
|
|
);
|
|
let metadata = record.request_metadata.expect("record billing metadata");
|
|
assert_eq!(metadata["provider_cache_ttl_minutes"], 30);
|
|
assert_eq!(metadata["dimensions"]["image_count"], 2);
|
|
drop(event);
|
|
assert_eq!(budget.retained_bytes(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn event_wire_preserves_explicit_capture_states_and_legacy_metadata() {
|
|
for state in [
|
|
None,
|
|
Some(UsageBodyCaptureState::None),
|
|
Some(UsageBodyCaptureState::Disabled),
|
|
Some(UsageBodyCaptureState::Unavailable),
|
|
Some(UsageBodyCaptureState::Reference),
|
|
] {
|
|
let mut event = event();
|
|
event.data.request_body_state = state;
|
|
event.data.provider_request_body_state = state;
|
|
event.data.response_body_state = state;
|
|
event.data.client_response_body_state = state;
|
|
event.data.request_body_ref = Some("usage://wire-request/request_body".to_string());
|
|
event.data.response_headers = Some(json!({"large-header": "x".repeat(16_384)}));
|
|
event.data.request_metadata = Some(json!({
|
|
"requested_reasoning_effort": "high",
|
|
"provider_reasoning_effort": "medium",
|
|
"provider_service_tier": "priority",
|
|
"provider_cache_ttl_minutes": 60,
|
|
"provider_actual_service_tier": "flex"
|
|
}));
|
|
let before = serde_json::to_value(&event).unwrap();
|
|
let encoded = event
|
|
.to_bounded_stream_fields(4_096)
|
|
.expect("headers omitted");
|
|
let value = wire_value(&encoded);
|
|
for field in BODY_STATE_FIELDS {
|
|
assert_eq!(value["data"].get(field), before["data"].get(field));
|
|
}
|
|
let metadata = &value["data"]["request_metadata"];
|
|
if state == Some(UsageBodyCaptureState::None) {
|
|
assert!(metadata.get("requested_reasoning_effort").is_none());
|
|
assert!(metadata.get("provider_cache_ttl_minutes").is_none());
|
|
} else {
|
|
assert_eq!(metadata["requested_reasoning_effort"], "high");
|
|
assert_eq!(metadata["provider_cache_ttl_minutes"], 60);
|
|
}
|
|
assert_eq!(metadata["provider_actual_service_tier"], "flex");
|
|
assert_eq!(
|
|
value["data"]["request_body_ref"],
|
|
before["data"]["request_body_ref"]
|
|
);
|
|
assert_eq!(serde_json::to_value(&event).unwrap(), before);
|
|
if matches!(
|
|
state,
|
|
Some(
|
|
UsageBodyCaptureState::None
|
|
| UsageBodyCaptureState::Disabled
|
|
| UsageBodyCaptureState::Unavailable
|
|
)
|
|
) {
|
|
event.data.endpoint_api_format = Some("claude:messages".to_string());
|
|
event.data.request_body = Some(json!({"reasoning": {"effort": "low"}}));
|
|
event.data.provider_request_body =
|
|
Some(json!({"reasoning": {"effort": "low"}, "service_tier": "default"}));
|
|
event.data.response_body = Some(json!({"service_tier": "priority"}));
|
|
event.data.client_response_body = Some(json!({"stale": true}));
|
|
let baseline = UsageEvent::from_stream_fields_with_capture_budget(
|
|
&event.to_stream_fields().expect("full v1 fields"),
|
|
Arc::new(EventCaptureMemoryBudget::new(usize::MAX)),
|
|
)
|
|
.expect("full v1 consumer baseline");
|
|
let with_stale_bodies = wire_value(
|
|
&event
|
|
.to_bounded_stream_fields(4_096)
|
|
.expect("explicit capture states override stale bodies"),
|
|
);
|
|
for field in BODY_STATE_FIELDS {
|
|
assert_eq!(
|
|
with_stale_bodies["data"].get(field),
|
|
value["data"].get(field)
|
|
);
|
|
}
|
|
assert_eq!(
|
|
with_stale_bodies["data"]["request_metadata"],
|
|
serde_json::to_value(&baseline.data.request_metadata).unwrap(),
|
|
"wire omission must preserve the full v1 consumer's billing facts"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn event_wire_preserves_raw_body_cache_ttl_across_capture_states() {
|
|
use aether_data_contracts::repository::usage::extract_provider_cache_ttl_minutes_from_metadata;
|
|
|
|
for state in [
|
|
None,
|
|
Some(UsageBodyCaptureState::Inline),
|
|
Some(UsageBodyCaptureState::Reference),
|
|
Some(UsageBodyCaptureState::Truncated),
|
|
Some(UsageBodyCaptureState::Disabled),
|
|
Some(UsageBodyCaptureState::Unavailable),
|
|
] {
|
|
let mut event = event();
|
|
event.data.provider_request_body_state = state;
|
|
event.data.provider_request_body = Some(json!({
|
|
"prompt_cache_options": {"ttl": "30m"},
|
|
"service_tier": "default",
|
|
"reasoning": {"effort": "low"}
|
|
}));
|
|
event.data.response_headers = Some(json!({"large": "x".repeat(16_384)}));
|
|
event.data.request_metadata = Some(json!({
|
|
"provider_cache_ttl_minutes": 60,
|
|
"provider_service_tier": "priority",
|
|
"provider_reasoning_effort": "high"
|
|
}));
|
|
let before = serde_json::to_value(&event).unwrap();
|
|
let baseline = UsageEvent::from_stream_fields_with_capture_budget(
|
|
&event.to_stream_fields().expect("full v1 fields"),
|
|
Arc::new(EventCaptureMemoryBudget::new(usize::MAX)),
|
|
)
|
|
.expect("full v1 consumer baseline");
|
|
let encoded = event.to_bounded_stream_fields(4_096).expect("body omitted");
|
|
assert!(encoded.diagnostics_omitted);
|
|
let decoded = UsageEvent::from_stream_fields_with_capture_budget(
|
|
&encoded.fields,
|
|
Arc::new(EventCaptureMemoryBudget::new(usize::MAX)),
|
|
)
|
|
.expect("projected consumer event");
|
|
assert!(decoded.data.provider_request_body.is_none());
|
|
assert_eq!(
|
|
extract_provider_cache_ttl_minutes_from_metadata(
|
|
decoded.data.request_metadata.as_ref()
|
|
),
|
|
Some(30),
|
|
"raw body TTL must win over stale metadata for {state:?}"
|
|
);
|
|
for field in ["provider_service_tier", "provider_reasoning_effort"] {
|
|
assert_eq!(
|
|
decoded.data.request_metadata.as_ref().unwrap().get(field),
|
|
baseline.data.request_metadata.as_ref().unwrap().get(field),
|
|
"TTL preservation must not change {field} authority for {state:?}"
|
|
);
|
|
}
|
|
assert_eq!(serde_json::to_value(&event).unwrap(), before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn event_wire_non_object_bodies_match_full_consumer_facts() {
|
|
use aether_data_contracts::repository::usage::{
|
|
extract_provider_cache_ttl_minutes_from_metadata,
|
|
resolve_provider_service_tier_from_request_capture,
|
|
};
|
|
|
|
for body in [
|
|
json!(null),
|
|
json!("opaque"),
|
|
json!([]),
|
|
json!(42),
|
|
json!(true),
|
|
] {
|
|
for state in [
|
|
None,
|
|
Some(UsageBodyCaptureState::None),
|
|
Some(UsageBodyCaptureState::Inline),
|
|
Some(UsageBodyCaptureState::Reference),
|
|
Some(UsageBodyCaptureState::Truncated),
|
|
Some(UsageBodyCaptureState::Disabled),
|
|
Some(UsageBodyCaptureState::Unavailable),
|
|
] {
|
|
let mut event = event();
|
|
event.data.request_body = Some(body.clone());
|
|
event.data.provider_request_body = Some(body.clone());
|
|
event.data.request_body_state = state;
|
|
event.data.provider_request_body_state = state;
|
|
event.data.response_headers = Some(json!({"large": "x".repeat(16_384)}));
|
|
event.data.request_metadata = Some(json!({
|
|
"requested_reasoning_effort": "medium",
|
|
"provider_reasoning_effort": "high",
|
|
"provider_service_tier": "priority",
|
|
"provider_cache_ttl_minutes": 60,
|
|
"unrelated": {"preserved": true}
|
|
}));
|
|
let original = event.to_stream_fields().expect("full v1 fields");
|
|
let baseline = UsageEvent::from_stream_fields_with_capture_budget(
|
|
&original,
|
|
Arc::new(EventCaptureMemoryBudget::new(usize::MAX)),
|
|
)
|
|
.expect("full v1 consumer baseline");
|
|
let encoded = event
|
|
.to_bounded_stream_fields(4_096)
|
|
.expect("omit diagnostics");
|
|
assert!(encoded.diagnostics_omitted);
|
|
let decoded = UsageEvent::from_stream_fields_with_capture_budget(
|
|
&encoded.fields,
|
|
Arc::new(EventCaptureMemoryBudget::new(usize::MAX)),
|
|
)
|
|
.expect("projected consumer event");
|
|
let tier = |data: &UsageEventData| {
|
|
resolve_provider_service_tier_from_request_capture(
|
|
data.provider_request_body.as_ref(),
|
|
data.provider_request_body_state,
|
|
data.request_metadata.as_ref(),
|
|
)
|
|
};
|
|
assert_eq!(
|
|
tier(&decoded.data),
|
|
tier(&baseline.data),
|
|
"provider tier for {body:?}, {state:?}"
|
|
);
|
|
let metadata = decoded.data.request_metadata.as_ref().unwrap();
|
|
let baseline_metadata = baseline.data.request_metadata.as_ref().unwrap();
|
|
assert_eq!(
|
|
extract_provider_cache_ttl_minutes_from_metadata(Some(metadata)),
|
|
extract_provider_cache_ttl_minutes_from_metadata(Some(baseline_metadata)),
|
|
"metadata TTL fallback for {body:?}, {state:?}"
|
|
);
|
|
let authoritative = !body.is_null()
|
|
&& matches!(
|
|
state,
|
|
None | Some(
|
|
UsageBodyCaptureState::Inline | UsageBodyCaptureState::Reference
|
|
)
|
|
);
|
|
for field in [
|
|
REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
|
PROVIDER_REASONING_EFFORT_METADATA_KEY,
|
|
] {
|
|
assert_eq!(
|
|
metadata.get(field),
|
|
if authoritative {
|
|
None
|
|
} else {
|
|
baseline_metadata.get(field)
|
|
},
|
|
"reasoning authority for {body:?}, {state:?}, {field}"
|
|
);
|
|
}
|
|
assert_eq!(metadata["unrelated"], baseline_metadata["unrelated"]);
|
|
if body.is_null() {
|
|
assert_eq!(
|
|
decoded.data.request_body_state,
|
|
baseline.data.request_body_state
|
|
);
|
|
assert_eq!(
|
|
decoded.data.provider_request_body_state,
|
|
baseline.data.provider_request_body_state
|
|
);
|
|
}
|
|
assert_eq!(event.to_stream_fields().unwrap(), original);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn event_wire_rejects_cache_ttl_loss_from_explicit_none_capture_state() {
|
|
let mut event = event();
|
|
event.data.provider_request_body_state = Some(UsageBodyCaptureState::None);
|
|
event.data.provider_request_body = Some(json!({
|
|
"prompt_cache_options": {"ttl": "30m"},
|
|
"large": "x".repeat(16_384)
|
|
}));
|
|
let original = event.to_stream_fields().expect("full v1 fields");
|
|
let full = event
|
|
.to_bounded_stream_fields(original["payload"].len())
|
|
.expect("complete diagnostics remain representable");
|
|
assert_eq!(full.fields, original);
|
|
assert!(!full.diagnostics_omitted);
|
|
assert!(matches!(
|
|
event.to_bounded_stream_fields(4_096),
|
|
Err(DataLayerError::InvalidInput(_))
|
|
));
|
|
assert_eq!(event.to_stream_fields().unwrap(), original);
|
|
}
|
|
|
|
#[test]
|
|
fn event_wire_rejects_oversized_core_and_post_projection_metadata() {
|
|
for field in ["request_metadata", "error_message"] {
|
|
let mut event = event();
|
|
if field == "request_metadata" {
|
|
event.data.request_metadata = Some(json!({"large": "x".repeat(32_768)}));
|
|
} else {
|
|
event.data.error_message = Some("x".repeat(32_768));
|
|
}
|
|
let error = event
|
|
.to_bounded_stream_fields(1_024)
|
|
.expect_err("oversized core must fail");
|
|
assert!(matches!(error, DataLayerError::InvalidInput(_)));
|
|
assert!(
|
|
error.to_string().len() < 200,
|
|
"errors must not include payload contents"
|
|
);
|
|
}
|
|
let mut event = event();
|
|
event.data.response_body = Some(json!({"large": "x".repeat(8_192)}));
|
|
let core = ProjectedData {
|
|
data: &event.data,
|
|
overrides: None,
|
|
};
|
|
let core_size = serde_json::to_vec(&envelope(&event, &core)).unwrap().len();
|
|
assert!(
|
|
matches!(
|
|
event.to_bounded_stream_fields(core_size),
|
|
Err(DataLayerError::InvalidInput(_))
|
|
),
|
|
"added capture metadata must also fit the exact wire limit"
|
|
);
|
|
}
|
|
}
|