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