mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +08:00
feat(usage): enrich audit metadata and detail views
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user