feat(usage): enrich audit metadata and detail views

This commit is contained in:
elky
2026-07-17 19:20:16 +08:00
parent 0be380243b
commit 664c063a06
79 changed files with 6968 additions and 1294 deletions
@@ -6,9 +6,10 @@ use aether_contracts::ExecutionPlan;
use aether_data_contracts::repository::usage::{
extract_provider_actual_service_tier_from_response,
extract_provider_reasoning_effort_from_body, extract_provider_service_tier_from_body,
normalize_provider_service_tier, resolve_provider_cache_ttl_minutes,
normalize_provider_service_tier, resolve_provider_cache_ttl_minutes, UsageBodyCaptureState,
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY, PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
REQUESTED_REASONING_EFFORT_METADATA_KEY,
};
use serde_json::{json, Map, Value};
@@ -17,6 +18,53 @@ const MAX_USAGE_REQUEST_METADATA_NODES: usize = 4_000;
const MAX_USAGE_REQUEST_METADATA_BYTES: usize = 16 * 1024;
const MAX_USAGE_REQUEST_METADATA_STRING_BYTES: usize = 1_024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RequestBodyDerivedFactsAction {
Refresh,
Preserve,
Clear,
}
pub(crate) fn request_body_derived_facts_action(
request_body: Option<&Value>,
state: Option<UsageBodyCaptureState>,
) -> RequestBodyDerivedFactsAction {
// A typed `none` marker is produced by the final capture attempt. It must take precedence over
// a body value that may have survived from an earlier candidate. A missing marker (`None`)
// remains compatible with legacy events, where a present body is still authoritative.
if state == Some(UsageBodyCaptureState::None) {
return RequestBodyDerivedFactsAction::Clear;
}
if let Some(request_body) = request_body {
if matches!(
state,
Some(
UsageBodyCaptureState::Truncated
| UsageBodyCaptureState::Disabled
| UsageBodyCaptureState::Unavailable
)
) || request_body.as_object().is_some_and(|body| {
body.get("truncated").and_then(Value::as_bool) == Some(true)
&& body.get("reason").and_then(Value::as_str) == Some("body_capture_limit_exceeded")
}) {
return RequestBodyDerivedFactsAction::Preserve;
}
return RequestBodyDerivedFactsAction::Refresh;
}
match state {
Some(
UsageBodyCaptureState::Inline
| UsageBodyCaptureState::Reference
| UsageBodyCaptureState::Truncated
| UsageBodyCaptureState::Disabled
| UsageBodyCaptureState::Unavailable,
) => RequestBodyDerivedFactsAction::Preserve,
Some(UsageBodyCaptureState::None) | None => RequestBodyDerivedFactsAction::Clear,
}
}
pub(crate) fn build_usage_request_metadata_seed(
_plan: &ExecutionPlan,
context: Option<&Map<String, Value>>,
@@ -76,6 +124,32 @@ pub(crate) fn sanitize_usage_request_metadata_ref(value: Option<&Value>) -> Opti
(!filtered.is_empty()).then_some(Value::Object(filtered))
}
pub(crate) fn attach_client_request_body_metadata(
metadata: Option<Value>,
request_body: Option<&Value>,
) -> Option<Value> {
let request_body_is_object = request_body.and_then(Value::as_object).is_some();
let reasoning_effort = extract_provider_reasoning_effort_from_body(request_body);
if !request_body_is_object && reasoning_effort.is_none() {
return metadata;
}
let mut object = match metadata {
Some(Value::Object(object)) => object,
_ => Map::new(),
};
if request_body_is_object {
object.remove(REQUESTED_REASONING_EFFORT_METADATA_KEY);
}
if let Some(reasoning_effort) = reasoning_effort {
object.insert(
REQUESTED_REASONING_EFFORT_METADATA_KEY.to_string(),
Value::String(reasoning_effort),
);
}
(!object.is_empty()).then_some(Value::Object(object))
}
pub(crate) fn attach_provider_request_body_metadata(
metadata: Option<Value>,
provider_api_format: Option<&str>,
@@ -129,6 +203,31 @@ pub(crate) fn attach_provider_request_body_metadata(
(!object.is_empty()).then_some(Value::Object(object))
}
pub(crate) fn clear_client_request_body_metadata(metadata: Option<Value>) -> Option<Value> {
clear_request_metadata_fields(metadata, &[REQUESTED_REASONING_EFFORT_METADATA_KEY])
}
pub(crate) fn clear_provider_request_body_metadata(metadata: Option<Value>) -> Option<Value> {
clear_request_metadata_fields(
metadata,
&[
PROVIDER_REASONING_EFFORT_METADATA_KEY,
PROVIDER_SERVICE_TIER_METADATA_KEY,
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
],
)
}
fn clear_request_metadata_fields(metadata: Option<Value>, keys: &[&str]) -> Option<Value> {
let Some(Value::Object(mut object)) = metadata else {
return None;
};
for key in keys {
object.remove(*key);
}
(!object.is_empty()).then_some(Value::Object(object))
}
pub(crate) fn attach_provider_response_body_metadata(
metadata: Option<Value>,
provider_response_body: Option<&Value>,
@@ -148,6 +247,47 @@ pub(crate) fn attach_provider_response_body_metadata(
attach_provider_actual_service_tier_metadata(metadata, actual_service_tier.as_deref())
}
/// Refreshes the response-derived tier for a terminal snapshot. Complete response objects are
/// authoritative even when they contain no tier (which clears a stale candidate value). Capture
/// placeholders/absent bodies are not authoritative, so a terminal summary already present in
/// metadata is preserved for those cases.
pub(crate) fn refresh_provider_response_body_metadata(
metadata: Option<Value>,
provider_response_body: Option<&Value>,
) -> Option<Value> {
let is_capture_placeholder = provider_response_body
.and_then(Value::as_object)
.is_some_and(|body| {
body.get("truncated").and_then(Value::as_bool) == Some(true)
&& body.get("reason").and_then(Value::as_str) == Some("body_capture_limit_exceeded")
});
let body_is_complete_object =
provider_response_body.and_then(Value::as_object).is_some() && !is_capture_placeholder;
let actual_service_tier =
extract_provider_actual_service_tier_from_response(provider_response_body)
.and_then(|value| normalize_provider_service_tier(&value));
let Some(actual_service_tier) = actual_service_tier else {
if !body_is_complete_object {
return metadata;
}
let Some(Value::Object(mut object)) = metadata else {
return None;
};
object.remove(PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY);
return (!object.is_empty()).then_some(Value::Object(object));
};
let mut object = match metadata {
Some(Value::Object(object)) => object,
_ => Map::new(),
};
object.insert(
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY.to_string(),
Value::String(actual_service_tier),
);
(!object.is_empty()).then_some(Value::Object(object))
}
pub(crate) fn attach_provider_actual_service_tier_metadata(
metadata: Option<Value>,
actual_service_tier: Option<&str>,
@@ -179,6 +319,7 @@ fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<St
copy_non_empty_string(source, target, "request_path");
copy_non_empty_string(source, target, "request_query_string");
copy_non_empty_string(source, target, "request_path_and_query");
copy_non_empty_string(source, target, REQUESTED_REASONING_EFFORT_METADATA_KEY);
copy_non_empty_string(source, target, PROVIDER_REASONING_EFFORT_METADATA_KEY);
copy_non_empty_string(source, target, PROVIDER_SERVICE_TIER_METADATA_KEY);
copy_non_empty_string(source, target, PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY);
@@ -226,6 +367,7 @@ fn move_allowed_metadata_fields(mut source: Map<String, Value>, target: &mut Map
remove_non_empty_string(&mut source, target, "request_path");
remove_non_empty_string(&mut source, target, "request_query_string");
remove_non_empty_string(&mut source, target, "request_path_and_query");
remove_non_empty_string(&mut source, target, REQUESTED_REASONING_EFFORT_METADATA_KEY);
remove_non_empty_string(&mut source, target, PROVIDER_REASONING_EFFORT_METADATA_KEY);
remove_non_empty_string(&mut source, target, PROVIDER_SERVICE_TIER_METADATA_KEY);
remove_non_empty_string(
@@ -517,10 +659,16 @@ mod tests {
use serde_json::{json, Value};
use std::collections::BTreeMap;
use crate::{
apply_usage_body_capture_policy_to_event, UsageBodyCapturePolicy, UsageEvent,
UsageEventData, UsageEventType, UsageRequestRecordLevel,
};
use super::{
attach_provider_actual_service_tier_metadata, attach_provider_request_body_metadata,
attach_provider_response_body_metadata, build_usage_request_metadata_seed,
merge_usage_request_metadata, merge_usage_request_metadata_owned,
attach_client_request_body_metadata, attach_provider_actual_service_tier_metadata,
attach_provider_request_body_metadata, attach_provider_response_body_metadata,
build_usage_request_metadata_seed, merge_usage_request_metadata,
merge_usage_request_metadata_owned, refresh_provider_response_body_metadata,
sanitize_usage_request_metadata, sanitize_usage_request_metadata_ref,
MAX_USAGE_REQUEST_METADATA_BYTES, MAX_USAGE_REQUEST_METADATA_DEPTH,
MAX_USAGE_REQUEST_METADATA_NODES,
@@ -830,6 +978,27 @@ mod tests {
assert_eq!(metadata, None);
}
#[test]
fn client_request_body_metadata_preserves_requested_reasoning_mapping_source() {
let updated = attach_client_request_body_metadata(
Some(json!({
"trace_id": "trace-1",
"requested_reasoning_effort": "low"
})),
Some(&json!({
"reasoning": { "effort": "XHigh" }
})),
)
.expect("metadata should remain");
assert_eq!(updated["requested_reasoning_effort"], "xhigh");
let cleared =
attach_client_request_body_metadata(Some(updated), Some(&json!({ "model": "gpt-5" })))
.expect("trace metadata should remain");
assert!(cleared.get("requested_reasoning_effort").is_none());
}
#[test]
fn provider_request_body_metadata_uses_final_provider_body_as_source_of_truth() {
let metadata = Some(json!({
@@ -880,6 +1049,75 @@ mod tests {
);
}
#[test]
fn final_provider_request_tier_survives_basic_body_capture_as_derived_metadata() {
let request_body = json!({
"model": "gpt-5",
"reasoning": { "effort": "xhigh" }
});
let provider_request_body = json!({
"model": "gpt-5",
"reasoning": { "effort": "max" },
"service_tier": "priority"
});
let request_metadata = attach_client_request_body_metadata(None, Some(&request_body));
let request_metadata = attach_provider_request_body_metadata(
request_metadata,
Some("openai:responses"),
Some("gpt-5"),
Some("gpt-5"),
Some(&provider_request_body),
);
let mut event = UsageEvent::new(
UsageEventType::Completed,
"req-final-provider-tier",
UsageEventData {
provider_name: "OpenAI".to_string(),
model: "gpt-5".to_string(),
request_body: Some(request_body),
provider_request_body: Some(provider_request_body),
request_metadata,
..UsageEventData::default()
},
);
apply_usage_body_capture_policy_to_event(
UsageBodyCapturePolicy {
record_level: UsageRequestRecordLevel::Basic,
max_request_body_bytes: Some(1024),
max_response_body_bytes: Some(1024),
},
&mut event,
);
assert_eq!(event.data.request_body, None);
assert_eq!(event.data.provider_request_body, None);
assert_eq!(
event
.data
.request_metadata
.as_ref()
.and_then(|metadata| metadata.get("requested_reasoning_effort")),
Some(&json!("xhigh"))
);
assert_eq!(
event
.data
.request_metadata
.as_ref()
.and_then(|metadata| metadata.get("provider_reasoning_effort")),
Some(&json!("max"))
);
assert_eq!(
event
.data
.request_metadata
.as_ref()
.and_then(|metadata| metadata.get("provider_service_tier")),
Some(&json!("priority"))
);
}
#[test]
fn provider_response_metadata_preserves_terminal_actual_service_tier() {
let metadata = attach_provider_response_body_metadata(
@@ -902,6 +1140,39 @@ mod tests {
);
}
#[test]
fn terminal_response_refresh_replaces_stale_actual_tier() {
let metadata = refresh_provider_response_body_metadata(
Some(json!({"provider_actual_service_tier": "priority"})),
Some(&json!({"service_tier": "Default"})),
)
.expect("actual tier should remain");
assert_eq!(metadata["provider_actual_service_tier"], "default");
let metadata = refresh_provider_response_body_metadata(
Some(json!({
"trace_id": "trace-1",
"provider_actual_service_tier": "priority"
})),
Some(&json!({"id": "response-without-tier"})),
)
.expect("un-tiered complete response should remain auditable");
assert_eq!(metadata, json!({"trace_id": "trace-1"}));
}
#[test]
fn terminal_response_refresh_preserves_summary_over_capture_placeholder() {
let metadata = refresh_provider_response_body_metadata(
Some(json!({"provider_actual_service_tier": "priority"})),
Some(&json!({
"truncated": true,
"reason": "body_capture_limit_exceeded"
})),
)
.expect("summary should survive placeholder capture");
assert_eq!(metadata["provider_actual_service_tier"], "priority");
}
#[test]
fn terminal_summary_tier_uses_the_same_normalized_metadata_field() {
let metadata = attach_provider_actual_service_tier_metadata(