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
@@ -733,6 +733,110 @@ async fn admin_monitoring_trace_request_exposes_failed_candidate_upstream_respon
assert!(extra.get("provider_response").is_none());
}
#[tokio::test]
async fn admin_monitoring_trace_request_prefers_ref_backed_usage_response_body() {
let mut candidate = sample_candidate(
"cand-used",
"request-ref-body",
0,
RequestCandidateStatus::Failed,
Some(101),
Some(33),
Some(400),
);
candidate.extra_data = Some(json!({
"upstream_response": {
"status_code": 400,
"headers": {
"content-type": "text/event-stream",
"x-request-id": "stale-request-like-body"
},
"body": {
"model": "gpt-5.6-sol",
"input": [{"role": "user", "content": "request prompt"}]
}
}
}));
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![candidate]));
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider()],
vec![sample_endpoint()],
vec![sample_key()],
));
let mut usage = sample_usage(
"request-ref-body",
"provider-1",
"OpenAI",
0,
0.0,
"failed",
Some(400),
100,
);
usage.candidate_id = Some("cand-used".to_string());
usage.response_headers = Some(json!({
"content-type": "application/json",
"x-request-id": "req_usage-cyber-risk-demo"
}));
usage.response_body = Some(json!({
"error": {
"type": "invalid_request",
"message": "This content was flagged for possible cybersecurity risk.",
"code": 400
}
}));
usage.response_body_state = Some(UsageBodyCaptureState::Reference);
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed_with_detached_bodies(
vec![usage],
));
let data_state =
crate::data::GatewayDataState::with_request_candidate_and_usage_repository_for_tests(
request_candidates,
usage_repository,
)
.with_provider_catalog_reader(provider_catalog);
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(data_state);
let context = request_context(
http::Method::GET,
"/api/admin/monitoring/trace/request-ref-body",
);
let response = local_monitoring_response(&state, &context)
.await
.expect("handler should not error")
.expect("route should be handled locally");
assert_eq!(response.status(), http::StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body should read");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
let upstream_response = &payload["candidates"][0]["extra_data"]["upstream_response"];
assert_eq!(
upstream_response["headers"],
json!({
"content-type": "application/json",
"x-request-id": "req_usage-cyber-risk-demo"
})
);
assert_eq!(
upstream_response["body"]["error"],
json!({
"type": "invalid_request",
"message": "This content was flagged for possible cybersecurity risk.",
"code": 400
})
);
assert!(upstream_response["body"].get("input").is_none());
assert_eq!(
upstream_response["body_ref"],
json!("usage://request/request-ref-body/response_body")
);
}
#[tokio::test]
async fn admin_monitoring_trace_request_decodes_connect_json_response_body_refs() {
let mut candidate = sample_candidate(
@@ -30,6 +30,25 @@ struct ResolvedAdminMonitoringTrace {
usage: Option<StoredRequestUsageAudit>,
}
async fn hydrate_admin_monitoring_trace_response_body(
state: &AdminAppState<'_>,
mut usage: StoredRequestUsageAudit,
) -> Result<StoredRequestUsageAudit, GatewayError> {
let is_error_node = !usage.status.eq_ignore_ascii_case("completed")
|| usage
.status_code
.is_some_and(|status| !(200..300).contains(&status));
let response_body_ref = if is_error_node && usage.response_body.is_none() {
usage.response_body_ref.clone()
} else {
None
};
if let Some(body_ref) = response_body_ref.as_deref() {
usage.response_body = state.resolve_request_usage_body_ref(body_ref).await?;
}
Ok(usage)
}
pub(super) async fn build_admin_monitoring_trace_request_response(
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
@@ -92,6 +111,10 @@ async fn resolve_admin_monitoring_trace(
.read_request_usage_audit_shallow(request_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let usage = match usage {
Some(usage) => Some(hydrate_admin_monitoring_trace_response_body(state, usage).await?),
None => None,
};
return Ok(Some(ResolvedAdminMonitoringTrace { trace, usage }));
}
@@ -102,9 +125,10 @@ async fn resolve_admin_monitoring_trace(
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?
{
usage_candidates.push(usage);
usage_candidates.push(hydrate_admin_monitoring_trace_response_body(state, usage).await?);
}
if let Some(usage) = state.find_request_usage_by_id(request_id).await? {
let usage = hydrate_admin_monitoring_trace_response_body(state, usage).await?;
if !usage_candidates.iter().any(|item| item.id == usage.id) {
usage_candidates.push(usage);
}
@@ -35,13 +35,16 @@ pub(crate) async fn build_admin_create_user_api_key_response(
let Some(user_id) = admin_user_id_from_api_keys_path(request_context.path()) else {
return Ok(build_admin_users_bad_request_response("缺少 user_id"));
};
if state.find_user_auth_by_id(&user_id).await?.is_none() {
let Some(target_user) = state.find_user_auth_by_id(&user_id).await? else {
return Ok((
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "用户不存在" })),
)
.into_response());
}
};
// The authenticated principal authorizes this admin operation, but ownership and inherited
// policies must always come from the user selected in the request path.
let target_user_id = target_user.id;
let Some(request_body) = request_body else {
return Ok((
@@ -148,7 +151,7 @@ pub(crate) async fn build_admin_create_user_api_key_response(
let Some(created) = state
.create_user_api_key(aether_data::repository::auth::CreateUserApiKeyRecord {
user_id: user_id.clone(),
user_id: target_user_id.clone(),
api_key_id: uuid::Uuid::new_v4().to_string(),
key_hash: hash_admin_user_api_key(&plaintext_key),
key_encrypted: Some(key_encrypted),
@@ -174,7 +177,11 @@ pub(crate) async fn build_admin_create_user_api_key_response(
let created = if allowed_providers.is_some() {
match state
.set_user_api_key_allowed_providers(&user_id, &created.api_key_id, allowed_providers)
.set_user_api_key_allowed_providers(
&target_user_id,
&created.api_key_id,
allowed_providers,
)
.await?
{
Some(updated) => updated,
@@ -186,7 +193,7 @@ pub(crate) async fn build_admin_create_user_api_key_response(
let created = if feature_settings.is_some() {
match state
.set_user_api_key_feature_settings(
&user_id,
&target_user_id,
&created.api_key_id,
feature_settings.clone(),
)
@@ -506,6 +506,9 @@ fn build_users_me_usage_record_payload(
if let Some(reasoning_effort) = item.provider_reasoning_effort() {
payload["reasoning_effort"] = json!(reasoning_effort);
}
if let Some(requested_reasoning_effort) = item.requested_reasoning_effort() {
payload["requested_reasoning_effort"] = json!(requested_reasoning_effort);
}
if let Some(service_tier) = item.provider_service_tier() {
payload["service_tier"] = json!(service_tier);
}
@@ -577,6 +580,9 @@ fn build_users_me_usage_active_payload(item: &StoredRequestUsageAudit) -> serde_
if let Some(reasoning_effort) = item.provider_reasoning_effort() {
payload["reasoning_effort"] = json!(reasoning_effort);
}
if let Some(requested_reasoning_effort) = item.requested_reasoning_effort() {
payload["requested_reasoning_effort"] = json!(requested_reasoning_effort);
}
if let Some(service_tier) = item.provider_service_tier() {
payload["service_tier"] = json!(service_tier);
}
@@ -1655,6 +1661,27 @@ mod tests {
assert_eq!(payload["cache_creation_ephemeral_1h_input_tokens"], 6);
}
#[test]
fn user_usage_payloads_expose_requested_and_provider_reasoning_mapping() {
let item = StoredRequestUsageAudit {
request_body: Some(json!({
"reasoning": { "effort": "xhigh" }
})),
provider_request_body: Some(json!({
"reasoning": { "effort": "max" }
})),
..sample_usage("completed")
};
let record = build_users_me_usage_record_payload(&item, false, &BTreeMap::new(), false);
let active = build_users_me_usage_active_payload(&item);
assert_eq!(record["requested_reasoning_effort"], "xhigh");
assert_eq!(active["requested_reasoning_effort"], "xhigh");
assert_eq!(record["reasoning_effort"], "max");
assert_eq!(active["reasoning_effort"], "max");
}
#[test]
fn user_usage_active_override_uses_terminal_candidate_latency() {
let candidate = sample_candidate(