mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 08:57:46 +08:00
feat: expose upstream response model in usage records
This commit is contained in:
+3
@@ -183,6 +183,7 @@ SELECT
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'provider_actual_service_tier'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'provider_response_model'), '') IS NOT NULL
|
||||
OR ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false')
|
||||
OR ("usage".request_metadata->>'upstream_is_stream') IN ('true', 'false')
|
||||
OR ("usage".request_metadata->>'websocket_mode') IN ('true', 'false')
|
||||
@@ -208,6 +209,8 @@ SELECT
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), ''),
|
||||
'provider_actual_service_tier',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_actual_service_tier'), ''),
|
||||
'provider_response_model',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_response_model'), ''),
|
||||
'client_requested_stream',
|
||||
CASE
|
||||
WHEN ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false')
|
||||
|
||||
@@ -183,6 +183,7 @@ SELECT
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'provider_actual_service_tier'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'provider_response_model'), '') IS NOT NULL
|
||||
OR ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false')
|
||||
OR ("usage".request_metadata->>'upstream_is_stream') IN ('true', 'false')
|
||||
OR ("usage".request_metadata->>'websocket_mode') IN ('true', 'false')
|
||||
@@ -208,6 +209,8 @@ SELECT
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), ''),
|
||||
'provider_actual_service_tier',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_actual_service_tier'), ''),
|
||||
'provider_response_model',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_response_model'), ''),
|
||||
'client_requested_stream',
|
||||
CASE
|
||||
WHEN ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false')
|
||||
|
||||
@@ -3611,6 +3611,8 @@ fn usage_sql_uses_json_null_placeholders_for_usage_payload_columns() {
|
||||
assert!(sql.contains("request_metadata->>'provider_reasoning_effort'"));
|
||||
assert!(sql.contains("request_metadata->>'provider_service_tier'"));
|
||||
assert!(sql.contains("request_metadata->>'provider_actual_service_tier'"));
|
||||
assert!(sql.contains("request_metadata->>'provider_response_model'"));
|
||||
assert!(sql.contains("'provider_response_model'"));
|
||||
assert!(sql.contains("request_metadata->>'websocket_mode'"));
|
||||
assert!(sql.contains("'websocket_mode'"));
|
||||
assert!(sql.contains("AS client_family"));
|
||||
|
||||
@@ -8,9 +8,10 @@ use serde_json::{Map, Value};
|
||||
use crate::repository::candidates::sanitize_request_candidate_skip_reason;
|
||||
|
||||
use super::{
|
||||
LIVE_SESSION_METADATA_KEY, PLAN_USAGE_RESERVATION_DEFERRED_METADATA_KEY,
|
||||
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY, PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
|
||||
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
|
||||
normalize_provider_response_model, LIVE_SESSION_METADATA_KEY,
|
||||
PLAN_USAGE_RESERVATION_DEFERRED_METADATA_KEY, 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,
|
||||
REALTIME_SESSION_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
||||
ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY, ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY,
|
||||
USAGE_AVAILABLE_METADATA_KEY, USAGE_PRICING_AVAILABLE_METADATA_KEY,
|
||||
@@ -94,6 +95,12 @@ pub fn sanitize_usage_request_metadata_object(source: &Map<String, Value>) -> Op
|
||||
] {
|
||||
insert_known_string(source, &mut target, key, sanitize_service_tier);
|
||||
}
|
||||
insert_known_string(
|
||||
source,
|
||||
&mut target,
|
||||
PROVIDER_RESPONSE_MODEL_METADATA_KEY,
|
||||
normalize_provider_response_model,
|
||||
);
|
||||
insert_bounded_u64(
|
||||
source,
|
||||
&mut target,
|
||||
@@ -1279,6 +1286,23 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistence_projection_keeps_bounded_response_model_only_as_a_string() {
|
||||
let metadata = sanitize_usage_request_metadata(Some(json!({
|
||||
"provider_response_model": " GPT-5.1 "
|
||||
})))
|
||||
.expect("response model should remain");
|
||||
assert_eq!(metadata["provider_response_model"], "GPT-5.1");
|
||||
assert!(sanitize_usage_request_metadata(Some(json!({
|
||||
"provider_response_model": 42
|
||||
})))
|
||||
.is_none());
|
||||
assert!(sanitize_usage_request_metadata(Some(json!({
|
||||
"provider_response_model": "x".repeat(257)
|
||||
})))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistence_projection_keeps_only_bounded_settlement_facts() {
|
||||
let metadata = sanitize_usage_request_metadata(Some(json!({
|
||||
|
||||
@@ -15,11 +15,12 @@ pub use policy::*;
|
||||
pub use types::{
|
||||
canonical_usage_body_ref_for, extract_provider_actual_service_tier_from_response,
|
||||
extract_provider_cache_ttl_minutes_from_metadata, extract_provider_reasoning_effort_from_body,
|
||||
extract_provider_service_tier_from_body, normalize_provider_service_tier, parse_usage_body_ref,
|
||||
extract_provider_response_model_from_bodies, extract_provider_service_tier_from_body,
|
||||
normalize_provider_response_model, normalize_provider_service_tier, parse_usage_body_ref,
|
||||
resolve_provider_cache_ttl_minutes, resolve_provider_service_tier_from_request_capture,
|
||||
usage_body_ref, usage_request_metadata_client_family, ApiKeyLastUsedDelta,
|
||||
ManagementTokenCounterDelta, PendingUsageCleanupSummary, ProviderApiKeyWindowUsageRequest,
|
||||
ProxyNodeCounterDelta, StoredProviderApiKeyUsageSummary,
|
||||
usage_body_capture_is_authoritative, usage_body_ref, usage_request_metadata_client_family,
|
||||
ApiKeyLastUsedDelta, ManagementTokenCounterDelta, PendingUsageCleanupSummary,
|
||||
ProviderApiKeyWindowUsageRequest, ProxyNodeCounterDelta, StoredProviderApiKeyUsageSummary,
|
||||
StoredProviderApiKeyWindowUsageSummary, StoredProviderUsageSummary, StoredProviderUsageWindow,
|
||||
StoredRequestUsageAudit, StoredUsageAuditAggregation, StoredUsageAuditSummary,
|
||||
StoredUsageBodyPayload, StoredUsageBreakdownSummaryRow, StoredUsageCacheAffinityHitSummary,
|
||||
@@ -46,9 +47,9 @@ pub use types::{
|
||||
UsageTimeSeriesQuery, UsageWriteRepository, LIVE_SESSION_METADATA_KEY,
|
||||
PLAN_USAGE_RESERVATION_DEFERRED_METADATA_KEY, PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY,
|
||||
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY, PROVIDER_REASONING_EFFORT_METADATA_KEY,
|
||||
PROVIDER_SERVICE_TIER_METADATA_KEY, REALTIME_SESSION_METADATA_KEY,
|
||||
REQUESTED_REASONING_EFFORT_METADATA_KEY, ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY,
|
||||
ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY, USAGE_AVAILABLE_METADATA_KEY,
|
||||
USAGE_PRICING_AVAILABLE_METADATA_KEY, WEBSOCKET_MODE_METADATA_KEY,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
PROVIDER_RESPONSE_MODEL_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
|
||||
REALTIME_SESSION_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
||||
ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY, ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY,
|
||||
USAGE_AVAILABLE_METADATA_KEY, USAGE_PRICING_AVAILABLE_METADATA_KEY,
|
||||
WEBSOCKET_MODE_METADATA_KEY, WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ pub const PROVIDER_REASONING_EFFORT_METADATA_KEY: &str = "provider_reasoning_eff
|
||||
pub const REQUESTED_REASONING_EFFORT_METADATA_KEY: &str = "requested_reasoning_effort";
|
||||
pub const PROVIDER_SERVICE_TIER_METADATA_KEY: &str = "provider_service_tier";
|
||||
pub const PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY: &str = "provider_actual_service_tier";
|
||||
pub const PROVIDER_RESPONSE_MODEL_METADATA_KEY: &str = "provider_response_model";
|
||||
pub const PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY: &str = "provider_cache_ttl_minutes";
|
||||
pub const ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY: &str = "routing_candidate_skip_reason";
|
||||
pub const ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY: &str = "routing_failure_diagnostic";
|
||||
@@ -119,6 +120,42 @@ pub fn normalize_provider_service_tier(value: &str) -> Option<String> {
|
||||
Some(value.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// 清洗响应体顶层 `model`,保留大小写,只去除首尾空白。
|
||||
pub fn normalize_provider_response_model(value: &str) -> Option<String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() || value.len() > 256 {
|
||||
return None;
|
||||
}
|
||||
Some(value.to_string())
|
||||
}
|
||||
|
||||
/// 只有请求体和响应体都可作为完整事实时,才计算响应模型,避免用截断内容猜测。
|
||||
pub fn extract_provider_response_model_from_bodies(
|
||||
request_body: Option<&Value>,
|
||||
request_body_state: Option<UsageBodyCaptureState>,
|
||||
response_body: Option<&Value>,
|
||||
response_body_state: Option<UsageBodyCaptureState>,
|
||||
) -> Option<String> {
|
||||
if !usage_body_capture_is_authoritative(request_body, request_body_state)
|
||||
|| !usage_body_capture_is_authoritative(response_body, response_body_state)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let request_model = request_body
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|body| body.get("model"))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(normalize_provider_response_model)?;
|
||||
let response_model = response_body
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|body| body.get("model"))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(normalize_provider_response_model)?;
|
||||
|
||||
(request_model != response_model).then_some(response_model)
|
||||
}
|
||||
|
||||
/// Resolves a provider processing tier exclusively from the final upstream request.
|
||||
///
|
||||
/// A complete captured body is authoritative, including when it contains no tier. The metadata
|
||||
@@ -129,7 +166,7 @@ pub fn resolve_provider_service_tier_from_request_capture(
|
||||
provider_request_body_state: Option<UsageBodyCaptureState>,
|
||||
request_metadata: Option<&Value>,
|
||||
) -> Option<String> {
|
||||
if request_body_capture_is_authoritative(provider_request_body, provider_request_body_state) {
|
||||
if usage_body_capture_is_authoritative(provider_request_body, provider_request_body_state) {
|
||||
return extract_provider_service_tier_from_body(provider_request_body);
|
||||
}
|
||||
|
||||
@@ -153,7 +190,7 @@ pub fn resolve_provider_service_tier_from_request_capture(
|
||||
.and_then(normalize_provider_service_tier)
|
||||
}
|
||||
|
||||
fn request_body_capture_is_authoritative(
|
||||
pub fn usage_body_capture_is_authoritative(
|
||||
request_body: Option<&Value>,
|
||||
request_body_state: Option<UsageBodyCaptureState>,
|
||||
) -> bool {
|
||||
@@ -184,7 +221,7 @@ fn resolve_reasoning_effort_from_request_capture(
|
||||
request_metadata: Option<&Value>,
|
||||
metadata_key: &str,
|
||||
) -> Option<String> {
|
||||
if request_body_capture_is_authoritative(request_body, request_body_state) {
|
||||
if usage_body_capture_is_authoritative(request_body, request_body_state) {
|
||||
return extract_provider_reasoning_effort_from_body(request_body);
|
||||
}
|
||||
|
||||
@@ -701,6 +738,11 @@ impl StoredRequestUsageAudit {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn provider_response_model(&self) -> Option<String> {
|
||||
self.request_metadata_string(PROVIDER_RESPONSE_MODEL_METADATA_KEY)
|
||||
.and_then(normalize_provider_response_model)
|
||||
}
|
||||
|
||||
pub fn provider_cache_ttl_minutes(&self) -> Option<i64> {
|
||||
resolve_provider_cache_ttl_minutes(
|
||||
self.endpoint_api_format
|
||||
@@ -2646,7 +2688,8 @@ fn parse_timestamp(value: i64, field_name: &str) -> Result<u64, crate::DataLayer
|
||||
mod tests {
|
||||
use super::{
|
||||
canonical_usage_body_ref_for, extract_provider_actual_service_tier_from_response,
|
||||
extract_provider_service_tier_from_body, normalize_provider_reasoning_effort,
|
||||
extract_provider_response_model_from_bodies, extract_provider_service_tier_from_body,
|
||||
normalize_provider_reasoning_effort, normalize_provider_response_model,
|
||||
normalize_provider_service_tier, resolve_provider_cache_ttl_minutes, usage_body_ref,
|
||||
StoredRequestUsageAudit, UpsertUsageRecord, UsageBodyCaptureState, UsageBodyCaptureStorage,
|
||||
UsageBodyField, UsageProviderPerformanceQuery, REALTIME_SESSION_METADATA_KEY,
|
||||
@@ -2672,6 +2715,58 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_model_requires_authoritative_different_top_level_models() {
|
||||
let request = json!({"model": " gpt-5 "});
|
||||
let response = json!({"model": " gpt-5.1 "});
|
||||
assert_eq!(
|
||||
extract_provider_response_model_from_bodies(
|
||||
Some(&request),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
Some(&response),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
),
|
||||
Some("gpt-5.1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
extract_provider_response_model_from_bodies(
|
||||
Some(&request),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
Some(&json!({"model": "gpt-5"})),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
extract_provider_response_model_from_bodies(
|
||||
Some(&request),
|
||||
Some(UsageBodyCaptureState::Truncated),
|
||||
Some(&response),
|
||||
Some(UsageBodyCaptureState::Inline),
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_model_keeps_case_and_rejects_invalid_values() {
|
||||
assert_eq!(
|
||||
normalize_provider_response_model(" GPT-5.1 "),
|
||||
Some("GPT-5.1".to_string())
|
||||
);
|
||||
assert_eq!(normalize_provider_response_model(" "), None);
|
||||
assert_eq!(normalize_provider_response_model(&"x".repeat(257)), None);
|
||||
assert_eq!(
|
||||
extract_provider_response_model_from_bodies(
|
||||
Some(&json!({"model": "gpt-5"})),
|
||||
None,
|
||||
Some(&json!({"model": 42})),
|
||||
None,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_fact_normalization_preserves_non_ascii_case_and_byte_count() {
|
||||
for normalize in [
|
||||
|
||||
Reference in New Issue
Block a user