mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 00:47:48 +08:00
feat: expose upstream response model in usage records
This commit is contained in:
@@ -15,9 +15,9 @@ use super::{BorrowedUsageEventEnvelope, UsageEvent, UsageEventData, USAGE_EVENT_
|
||||
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, clear_client_request_body_metadata,
|
||||
clear_provider_request_body_metadata, request_body_derived_facts_action,
|
||||
RequestBodyDerivedFactsAction,
|
||||
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] = [
|
||||
@@ -238,6 +238,13 @@ impl WireOverrides {
|
||||
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.response_body.as_ref(),
|
||||
data.response_body_state,
|
||||
);
|
||||
// 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 {
|
||||
|
||||
@@ -5,9 +5,9 @@ use aether_data_contracts::DataLayerError;
|
||||
|
||||
use crate::request_metadata::{
|
||||
attach_client_request_body_metadata, attach_provider_request_body_metadata,
|
||||
clear_client_request_body_metadata, clear_provider_request_body_metadata,
|
||||
request_body_derived_facts_action, sanitize_usage_request_metadata,
|
||||
RequestBodyDerivedFactsAction,
|
||||
attach_provider_response_model_metadata, clear_client_request_body_metadata,
|
||||
clear_provider_request_body_metadata, request_body_derived_facts_action,
|
||||
sanitize_usage_request_metadata, RequestBodyDerivedFactsAction,
|
||||
};
|
||||
use crate::{UsageEvent, UsageEventType};
|
||||
|
||||
@@ -85,6 +85,14 @@ pub fn build_upsert_usage_record_from_event(
|
||||
}
|
||||
RequestBodyDerivedFactsAction::Preserve => {}
|
||||
}
|
||||
// 响应模型必须在 body 被裁剪前从客户端请求体和上游响应体共同派生;缺少权威 body 时保留已派生事实。
|
||||
data.request_metadata = attach_provider_response_model_metadata(
|
||||
data.request_metadata,
|
||||
data.request_body.as_ref(),
|
||||
data.request_body_state,
|
||||
data.response_body.as_ref(),
|
||||
data.response_body_state,
|
||||
);
|
||||
let now_unix_secs = event.timestamp_ms / 1_000;
|
||||
|
||||
Ok(UpsertUsageRecord {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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,
|
||||
extract_provider_reasoning_effort_from_body, extract_provider_response_model_from_bodies,
|
||||
extract_provider_service_tier_from_body, normalize_provider_service_tier,
|
||||
resolve_provider_cache_ttl_minutes,
|
||||
sanitize_usage_request_metadata as project_usage_request_metadata,
|
||||
sanitize_usage_request_metadata_object as project_usage_request_metadata_object,
|
||||
sanitize_usage_request_metadata_ref as project_usage_request_metadata_ref,
|
||||
UsageBodyCaptureState, PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY,
|
||||
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY, PROVIDER_REASONING_EFFORT_METADATA_KEY,
|
||||
usage_body_capture_is_authoritative, UsageBodyCaptureState,
|
||||
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY, PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
|
||||
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_RESPONSE_MODEL_METADATA_KEY,
|
||||
PROVIDER_SERVICE_TIER_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
||||
};
|
||||
use serde_json::{Map, Value};
|
||||
@@ -249,6 +251,75 @@ pub(crate) fn attach_provider_response_body_metadata(
|
||||
attach_provider_actual_service_tier_metadata(metadata, actual_service_tier.as_deref())
|
||||
}
|
||||
|
||||
pub(crate) fn attach_provider_response_model_metadata(
|
||||
metadata: Option<Value>,
|
||||
request_body: Option<&Value>,
|
||||
request_body_state: Option<UsageBodyCaptureState>,
|
||||
response_body: Option<&Value>,
|
||||
response_body_state: Option<UsageBodyCaptureState>,
|
||||
) -> Option<Value> {
|
||||
let both_bodies_are_authoritative =
|
||||
usage_body_capture_is_authoritative(request_body, request_body_state)
|
||||
&& usage_body_capture_is_authoritative(response_body, response_body_state);
|
||||
let response_model = extract_provider_response_model_from_bodies(
|
||||
request_body,
|
||||
request_body_state,
|
||||
response_body,
|
||||
response_body_state,
|
||||
);
|
||||
if !both_bodies_are_authoritative && response_model.is_none() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
let mut object = match metadata {
|
||||
Some(Value::Object(object)) => object,
|
||||
_ => Map::new(),
|
||||
};
|
||||
// 完整终态 body 是最终候选的权威事实;相同、无效或缺失模型都要清除旧候选值。
|
||||
if both_bodies_are_authoritative {
|
||||
object.remove(PROVIDER_RESPONSE_MODEL_METADATA_KEY);
|
||||
}
|
||||
if let Some(response_model) = response_model {
|
||||
object.insert(
|
||||
PROVIDER_RESPONSE_MODEL_METADATA_KEY.to_string(),
|
||||
Value::String(response_model),
|
||||
);
|
||||
}
|
||||
(!object.is_empty()).then_some(Value::Object(object))
|
||||
}
|
||||
|
||||
/// 终态候选无法完成比较时,显式清除旧响应模型,避免重试/故障转移残留。
|
||||
pub(crate) fn refresh_provider_response_model_metadata(
|
||||
metadata: Option<Value>,
|
||||
request_body: Option<&Value>,
|
||||
request_body_state: Option<UsageBodyCaptureState>,
|
||||
response_body: Option<&Value>,
|
||||
response_body_state: Option<UsageBodyCaptureState>,
|
||||
) -> Option<Value> {
|
||||
let mut object = match metadata {
|
||||
Some(Value::Object(object)) => object,
|
||||
_ => Map::new(),
|
||||
};
|
||||
object.remove(PROVIDER_RESPONSE_MODEL_METADATA_KEY);
|
||||
|
||||
if usage_body_capture_is_authoritative(request_body, request_body_state)
|
||||
&& usage_body_capture_is_authoritative(response_body, response_body_state)
|
||||
{
|
||||
if let Some(response_model) = extract_provider_response_model_from_bodies(
|
||||
request_body,
|
||||
request_body_state,
|
||||
response_body,
|
||||
response_body_state,
|
||||
) {
|
||||
object.insert(
|
||||
PROVIDER_RESPONSE_MODEL_METADATA_KEY.to_string(),
|
||||
Value::String(response_model),
|
||||
);
|
||||
}
|
||||
}
|
||||
(!object.is_empty()).then_some(Value::Object(object))
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -323,8 +394,9 @@ mod tests {
|
||||
use super::{
|
||||
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,
|
||||
attach_provider_response_model_metadata, build_usage_request_metadata_seed,
|
||||
merge_usage_request_metadata, merge_usage_request_metadata_owned,
|
||||
refresh_provider_response_body_metadata, refresh_provider_response_model_metadata,
|
||||
retain_first_byte_request_metadata, sanitize_usage_request_metadata,
|
||||
sanitize_usage_request_metadata_ref,
|
||||
};
|
||||
@@ -802,6 +874,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_model_metadata_is_independent_from_mapping_and_clears_stale_values() {
|
||||
let metadata = attach_provider_response_model_metadata(
|
||||
Some(json!({"provider_response_model": "old-model", "trace_id": "trace-1"})),
|
||||
Some(&json!({"model": "gpt-5"})),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
Some(&json!({"model": "gpt-5.1"})),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
)
|
||||
.expect("response model should be attached");
|
||||
assert_eq!(metadata["provider_response_model"], "gpt-5.1");
|
||||
assert_eq!(metadata["trace_id"], "trace-1");
|
||||
|
||||
let metadata = refresh_provider_response_model_metadata(
|
||||
Some(json!({"provider_response_model": "gpt-5.1"})),
|
||||
Some(&json!({"model": "gpt-5"})),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
Some(&json!({"model": "gpt-5"})),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
);
|
||||
assert!(metadata.is_none());
|
||||
|
||||
let metadata = refresh_provider_response_model_metadata(
|
||||
Some(json!({"provider_response_model": "gpt-5.1"})),
|
||||
None,
|
||||
Some(UsageBodyCaptureState::Disabled),
|
||||
Some(&json!({"model": "gpt-5.2"})),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
);
|
||||
assert!(metadata.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_response_refresh_replaces_stale_actual_tier() {
|
||||
let metadata = refresh_provider_response_body_metadata(
|
||||
|
||||
@@ -21,9 +21,10 @@ use crate::executor::spawn_on_usage_background_runtime;
|
||||
use crate::queue::is_permanent_enqueue_error;
|
||||
use crate::request_metadata::{
|
||||
attach_client_request_body_metadata, attach_provider_request_body_metadata,
|
||||
attach_provider_response_body_metadata, clear_client_request_body_metadata,
|
||||
clear_provider_request_body_metadata, request_body_derived_facts_action,
|
||||
retain_first_byte_request_metadata, RequestBodyDerivedFactsAction,
|
||||
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, retain_first_byte_request_metadata,
|
||||
RequestBodyDerivedFactsAction,
|
||||
};
|
||||
use crate::settlement::{
|
||||
reconcile_usage_policy_cost_for_event_with_result, settle_usage_with_reconciled_cost,
|
||||
@@ -5297,8 +5298,15 @@ fn preserve_request_facts_with_legacy_missing(
|
||||
|
||||
fn preserve_provider_response_facts(event: &mut UsageEvent) {
|
||||
let metadata = event.data.request_metadata.take();
|
||||
event.data.request_metadata =
|
||||
let metadata =
|
||||
attach_provider_response_body_metadata(metadata, event.data.response_body.as_ref());
|
||||
event.data.request_metadata = attach_provider_response_model_metadata(
|
||||
metadata,
|
||||
event.data.request_body.as_ref(),
|
||||
event.data.request_body_state,
|
||||
event.data.response_body.as_ref(),
|
||||
event.data.response_body_state,
|
||||
);
|
||||
}
|
||||
|
||||
impl UsageQueueHealthSnapshot {
|
||||
|
||||
@@ -17,9 +17,10 @@ use crate::body_capture::{
|
||||
};
|
||||
use crate::request_metadata::{
|
||||
attach_client_request_body_metadata, attach_provider_actual_service_tier_metadata,
|
||||
attach_provider_request_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,
|
||||
attach_provider_request_body_metadata, attach_provider_response_model_metadata,
|
||||
build_usage_request_metadata_seed, merge_usage_request_metadata,
|
||||
merge_usage_request_metadata_owned, refresh_provider_response_body_metadata,
|
||||
refresh_provider_response_model_metadata, sanitize_usage_request_metadata,
|
||||
sanitize_usage_request_metadata_ref,
|
||||
};
|
||||
use crate::{
|
||||
@@ -724,6 +725,13 @@ fn build_terminal_usage_event_from_seed_impl(
|
||||
Some(model.as_str()),
|
||||
provider_request.as_ref(),
|
||||
);
|
||||
let request_metadata = attach_provider_response_model_metadata(
|
||||
request_metadata,
|
||||
request_body.as_ref(),
|
||||
body_states.request_body_state,
|
||||
provider_response.as_ref(),
|
||||
body_states.response_body_state,
|
||||
);
|
||||
|
||||
let mut data = UsageEventData {
|
||||
user_id,
|
||||
@@ -1047,6 +1055,13 @@ pub fn build_sync_terminal_usage_seed(
|
||||
context_seed.request_metadata,
|
||||
provider_response_full.as_ref(),
|
||||
);
|
||||
let request_metadata = refresh_provider_response_model_metadata(
|
||||
request_metadata,
|
||||
context_seed.request_body.as_ref(),
|
||||
context_seed.body_states.request_body_state,
|
||||
provider_response_full.as_ref(),
|
||||
provider_response_body_state,
|
||||
);
|
||||
|
||||
TerminalUsageSeed {
|
||||
terminal_state,
|
||||
@@ -1225,6 +1240,13 @@ pub fn build_stream_terminal_usage_seed(
|
||||
context_seed.request_metadata,
|
||||
provider_response_full.as_ref(),
|
||||
);
|
||||
let request_metadata = refresh_provider_response_model_metadata(
|
||||
request_metadata,
|
||||
context_seed.request_body.as_ref(),
|
||||
context_seed.body_states.request_body_state,
|
||||
provider_response_full.as_ref(),
|
||||
provider_response_body_state,
|
||||
);
|
||||
// The parser's terminal summary is authoritative when a response body is truncated or the
|
||||
// body and summary disagree; attach it after the body refresh so it wins.
|
||||
let request_metadata = attach_provider_actual_service_tier_metadata(
|
||||
|
||||
Reference in New Issue
Block a user