mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(usage): record request origin metadata (#362)
Record client IP and User-Agent in usage request_metadata for local execution requests without adding schema, API, or table fields. References #357 and supersedes #358. Co-authored-by: RWDai <27391645+RWDai@users.noreply.github.com>
This commit is contained in:
@@ -109,6 +109,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
|
|||||||
provider_request_method: Some(serde_json::Value::Null),
|
provider_request_method: Some(serde_json::Value::Null),
|
||||||
provider_request_headers: Some(&resolved.provider_request_headers),
|
provider_request_headers: Some(&resolved.provider_request_headers),
|
||||||
original_headers: &parts.headers,
|
original_headers: &parts.headers,
|
||||||
|
request_origin: Some(crate::headers::request_origin_from_parts(parts)),
|
||||||
original_request_body_json: Some(body_json),
|
original_request_body_json: Some(body_json),
|
||||||
original_request_body_base64: None,
|
original_request_body_base64: None,
|
||||||
client_requested_stream: body_json
|
client_requested_stream: body_json
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use serde_json::{Map, Value};
|
|||||||
|
|
||||||
use crate::ai_pipeline::contracts::ExecutionRuntimeAuthContext;
|
use crate::ai_pipeline::contracts::ExecutionRuntimeAuthContext;
|
||||||
use crate::ai_pipeline::planner::candidate_metadata::append_ranking_metadata_to_object;
|
use crate::ai_pipeline::planner::candidate_metadata::append_ranking_metadata_to_object;
|
||||||
|
use crate::headers::RequestOrigin;
|
||||||
use crate::orchestration::ExecutionAttemptIdentity;
|
use crate::orchestration::ExecutionAttemptIdentity;
|
||||||
|
|
||||||
pub(crate) struct LocalExecutionReportContextParts<'a> {
|
pub(crate) struct LocalExecutionReportContextParts<'a> {
|
||||||
@@ -32,6 +33,7 @@ pub(crate) struct LocalExecutionReportContextParts<'a> {
|
|||||||
pub(crate) provider_request_method: Option<Value>,
|
pub(crate) provider_request_method: Option<Value>,
|
||||||
pub(crate) provider_request_headers: Option<&'a BTreeMap<String, String>>,
|
pub(crate) provider_request_headers: Option<&'a BTreeMap<String, String>>,
|
||||||
pub(crate) original_headers: &'a http::HeaderMap,
|
pub(crate) original_headers: &'a http::HeaderMap,
|
||||||
|
pub(crate) request_origin: Option<RequestOrigin>,
|
||||||
pub(crate) original_request_body_json: Option<&'a Value>,
|
pub(crate) original_request_body_json: Option<&'a Value>,
|
||||||
pub(crate) original_request_body_base64: Option<&'a str>,
|
pub(crate) original_request_body_base64: Option<&'a str>,
|
||||||
pub(crate) client_requested_stream: bool,
|
pub(crate) client_requested_stream: bool,
|
||||||
@@ -131,6 +133,18 @@ pub(crate) fn build_local_execution_report_context(
|
|||||||
)
|
)
|
||||||
.unwrap_or(Value::Null),
|
.unwrap_or(Value::Null),
|
||||||
);
|
);
|
||||||
|
let RequestOrigin {
|
||||||
|
client_ip,
|
||||||
|
user_agent,
|
||||||
|
} = parts
|
||||||
|
.request_origin
|
||||||
|
.unwrap_or_else(|| crate::headers::request_origin_from_headers(parts.original_headers));
|
||||||
|
if let Some(client_ip) = client_ip {
|
||||||
|
object.insert("client_ip".to_string(), Value::String(client_ip));
|
||||||
|
}
|
||||||
|
if let Some(user_agent) = user_agent {
|
||||||
|
object.insert("user_agent".to_string(), Value::String(user_agent));
|
||||||
|
}
|
||||||
object.insert(
|
object.insert(
|
||||||
"client_requested_stream".to_string(),
|
"client_requested_stream".to_string(),
|
||||||
Value::Bool(parts.client_requested_stream),
|
Value::Bool(parts.client_requested_stream),
|
||||||
@@ -237,7 +251,17 @@ pub(crate) fn insert_provider_stream_event_api_format(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::provider_stream_event_api_format_for_provider_type;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
build_local_execution_report_context, provider_stream_event_api_format_for_provider_type,
|
||||||
|
LocalExecutionReportContextParts,
|
||||||
|
};
|
||||||
|
use crate::ai_pipeline::contracts::ExecutionRuntimeAuthContext;
|
||||||
|
use crate::headers::RequestOrigin;
|
||||||
|
use crate::orchestration::ExecutionAttemptIdentity;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codex_provider_uses_openai_responses_stream_event_format() {
|
fn codex_provider_uses_openai_responses_stream_event_format() {
|
||||||
@@ -262,4 +286,67 @@ mod tests {
|
|||||||
None
|
None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_execution_report_context_records_request_origin() {
|
||||||
|
let auth_context = ExecutionRuntimeAuthContext {
|
||||||
|
user_id: "user-1".to_string(),
|
||||||
|
api_key_id: "api-key-1".to_string(),
|
||||||
|
username: None,
|
||||||
|
api_key_name: None,
|
||||||
|
balance_remaining: None,
|
||||||
|
access_allowed: true,
|
||||||
|
api_key_is_standalone: false,
|
||||||
|
};
|
||||||
|
let original_headers = http::HeaderMap::new();
|
||||||
|
let provider_request_headers = BTreeMap::new();
|
||||||
|
|
||||||
|
let report_context =
|
||||||
|
build_local_execution_report_context(LocalExecutionReportContextParts {
|
||||||
|
auth_context: &auth_context,
|
||||||
|
request_id: "trace-1",
|
||||||
|
candidate_id: "candidate-1",
|
||||||
|
attempt_identity: ExecutionAttemptIdentity::new(0, 0),
|
||||||
|
model: "gpt-5",
|
||||||
|
provider_name: "OpenAI",
|
||||||
|
provider_id: "provider-1",
|
||||||
|
endpoint_id: "endpoint-1",
|
||||||
|
key_id: "key-1",
|
||||||
|
key_name: None,
|
||||||
|
model_id: None,
|
||||||
|
global_model_id: None,
|
||||||
|
global_model_name: None,
|
||||||
|
provider_api_format: "openai:chat",
|
||||||
|
client_api_format: "openai:chat",
|
||||||
|
mapped_model: None,
|
||||||
|
candidate_group_id: None,
|
||||||
|
ranking: None,
|
||||||
|
upstream_url: None,
|
||||||
|
header_rules: None,
|
||||||
|
body_rules: None,
|
||||||
|
provider_request_method: None,
|
||||||
|
provider_request_headers: Some(&provider_request_headers),
|
||||||
|
original_headers: &original_headers,
|
||||||
|
request_origin: Some(RequestOrigin {
|
||||||
|
client_ip: Some("203.0.113.8".to_string()),
|
||||||
|
user_agent: Some("Claude-Code/1.0".to_string()),
|
||||||
|
}),
|
||||||
|
original_request_body_json: Some(&json!({"model": "gpt-5"})),
|
||||||
|
original_request_body_base64: None,
|
||||||
|
client_requested_stream: false,
|
||||||
|
upstream_is_stream: false,
|
||||||
|
has_envelope: false,
|
||||||
|
needs_conversion: false,
|
||||||
|
extra_fields: Map::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
report_context["client_ip"],
|
||||||
|
Value::String("203.0.113.8".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report_context["user_agent"],
|
||||||
|
Value::String("Claude-Code/1.0".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
|
|||||||
provider_request_method: None,
|
provider_request_method: None,
|
||||||
provider_request_headers: None,
|
provider_request_headers: None,
|
||||||
original_headers: &parts.headers,
|
original_headers: &parts.headers,
|
||||||
|
request_origin: Some(crate::headers::request_origin_from_parts(parts)),
|
||||||
original_request_body_json: Some(body_json),
|
original_request_body_json: Some(body_json),
|
||||||
original_request_body_base64: resolved.provider_request_body_base64.as_deref(),
|
original_request_body_base64: resolved.provider_request_body_base64.as_deref(),
|
||||||
client_requested_stream: spec_metadata.require_streaming,
|
client_requested_stream: spec_metadata.require_streaming,
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
|
|||||||
provider_request_method: Some(serde_json::Value::String(parts.method.to_string())),
|
provider_request_method: Some(serde_json::Value::String(parts.method.to_string())),
|
||||||
provider_request_headers: Some(&resolved.provider_request_headers),
|
provider_request_headers: Some(&resolved.provider_request_headers),
|
||||||
original_headers: &parts.headers,
|
original_headers: &parts.headers,
|
||||||
|
request_origin: Some(crate::headers::request_origin_from_parts(parts)),
|
||||||
original_request_body_json: Some(body_json),
|
original_request_body_json: Some(body_json),
|
||||||
original_request_body_base64: body_base64,
|
original_request_body_base64: body_base64,
|
||||||
client_requested_stream: spec_metadata.require_streaming,
|
client_requested_stream: spec_metadata.require_streaming,
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
|
|||||||
provider_request_method: None,
|
provider_request_method: None,
|
||||||
provider_request_headers: None,
|
provider_request_headers: None,
|
||||||
original_headers: &parts.headers,
|
original_headers: &parts.headers,
|
||||||
|
request_origin: Some(crate::headers::request_origin_from_parts(parts)),
|
||||||
original_request_body_json: Some(body_json),
|
original_request_body_json: Some(body_json),
|
||||||
original_request_body_base64: None,
|
original_request_body_base64: None,
|
||||||
client_requested_stream: false,
|
client_requested_stream: false,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
|||||||
provider_request_method: Some(serde_json::Value::Null),
|
provider_request_method: Some(serde_json::Value::Null),
|
||||||
provider_request_headers: Some(&resolved.provider_request_headers),
|
provider_request_headers: Some(&resolved.provider_request_headers),
|
||||||
original_headers: &parts.headers,
|
original_headers: &parts.headers,
|
||||||
|
request_origin: Some(crate::headers::request_origin_from_parts(parts)),
|
||||||
original_request_body_json: Some(body_json),
|
original_request_body_json: Some(body_json),
|
||||||
original_request_body_base64: None,
|
original_request_body_base64: None,
|
||||||
client_requested_stream: body_json
|
client_requested_stream: body_json
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
|
|||||||
provider_request_method: Some(serde_json::Value::Null),
|
provider_request_method: Some(serde_json::Value::Null),
|
||||||
provider_request_headers: Some(&resolved.provider_request_headers),
|
provider_request_headers: Some(&resolved.provider_request_headers),
|
||||||
original_headers: &parts.headers,
|
original_headers: &parts.headers,
|
||||||
|
request_origin: Some(crate::headers::request_origin_from_parts(parts)),
|
||||||
original_request_body_json: Some(body_json),
|
original_request_body_json: Some(body_json),
|
||||||
original_request_body_base64: None,
|
original_request_body_base64: None,
|
||||||
client_requested_stream: body_json
|
client_requested_stream: body_json
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand
|
|||||||
provider_request_method: Some(serde_json::Value::Null),
|
provider_request_method: Some(serde_json::Value::Null),
|
||||||
provider_request_headers: Some(&resolved.provider_request_headers),
|
provider_request_headers: Some(&resolved.provider_request_headers),
|
||||||
original_headers: &parts.headers,
|
original_headers: &parts.headers,
|
||||||
|
request_origin: Some(crate::headers::request_origin_from_parts(parts)),
|
||||||
original_request_body_json: Some(body_json),
|
original_request_body_json: Some(body_json),
|
||||||
original_request_body_base64: None,
|
original_request_body_base64: None,
|
||||||
client_requested_stream: body_json
|
client_requested_stream: body_json
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ use crate::handlers::shared::{
|
|||||||
local_proxy_route_requires_buffered_body, request_enables_control_execute,
|
local_proxy_route_requires_buffered_body, request_enables_control_execute,
|
||||||
should_strip_forwarded_provider_credential_header, should_strip_forwarded_trusted_admin_header,
|
should_strip_forwarded_provider_credential_header, should_strip_forwarded_trusted_admin_header,
|
||||||
};
|
};
|
||||||
use crate::headers::{extract_or_generate_trace_id, should_skip_request_header};
|
use crate::headers::{
|
||||||
|
extract_or_generate_trace_id, request_origin_from_headers_and_remote_addr,
|
||||||
|
should_skip_request_header,
|
||||||
|
};
|
||||||
use crate::router::RequestAdmissionError;
|
use crate::router::RequestAdmissionError;
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, FrontdoorUserRpmOutcome, GatewayError, GatewayFallbackMetricKind,
|
AppState, FrontdoorUserRpmOutcome, GatewayError, GatewayFallbackMetricKind,
|
||||||
@@ -707,7 +710,13 @@ pub(crate) async fn proxy_request(
|
|||||||
)) => return Err(GatewayError::Internal(message)),
|
)) => return Err(GatewayError::Internal(message)),
|
||||||
};
|
};
|
||||||
let request_admission_ms = started_at.elapsed().as_millis() as u64;
|
let request_admission_ms = started_at.elapsed().as_millis() as u64;
|
||||||
let (parts, body) = request.into_parts();
|
let (mut parts, body) = request.into_parts();
|
||||||
|
parts
|
||||||
|
.extensions
|
||||||
|
.insert(request_origin_from_headers_and_remote_addr(
|
||||||
|
&parts.headers,
|
||||||
|
&remote_addr,
|
||||||
|
));
|
||||||
let trace_id = extract_or_generate_trace_id(&parts.headers);
|
let trace_id = extract_or_generate_trace_id(&parts.headers);
|
||||||
state.clear_local_execution_runtime_miss_diagnostic(&trace_id);
|
state.clear_local_execution_runtime_miss_diagnostic(&trace_id);
|
||||||
if request_hits_execution_loop_guard(&parts) {
|
if request_hits_execution_loop_guard(&parts) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::{collections::BTreeMap, net::SocketAddr};
|
||||||
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -20,6 +20,61 @@ pub(crate) fn header_value_u64(headers: &http::HeaderMap, key: &str) -> Option<u
|
|||||||
header_value_str(headers, key).and_then(|value| value.parse::<u64>().ok())
|
header_value_str(headers, key).and_then(|value| value.parse::<u64>().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub(crate) struct RequestOrigin {
|
||||||
|
pub(crate) client_ip: Option<String>,
|
||||||
|
pub(crate) user_agent: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn request_origin_from_headers(headers: &http::HeaderMap) -> RequestOrigin {
|
||||||
|
RequestOrigin {
|
||||||
|
client_ip: client_ip_from_headers(headers),
|
||||||
|
user_agent: header_value_str(headers, http::header::USER_AGENT.as_str())
|
||||||
|
.map(|value| truncate_chars(value.as_str(), 1_000)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn request_origin_from_headers_and_remote_addr(
|
||||||
|
headers: &http::HeaderMap,
|
||||||
|
remote_addr: &SocketAddr,
|
||||||
|
) -> RequestOrigin {
|
||||||
|
let mut origin = request_origin_from_headers(headers);
|
||||||
|
if origin.client_ip.is_none() {
|
||||||
|
origin.client_ip = Some(remote_addr.ip().to_string());
|
||||||
|
}
|
||||||
|
origin
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn request_origin_from_parts(parts: &http::request::Parts) -> RequestOrigin {
|
||||||
|
parts
|
||||||
|
.extensions
|
||||||
|
.get::<RequestOrigin>()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| request_origin_from_headers(&parts.headers))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_ip_from_headers(headers: &http::HeaderMap) -> Option<String> {
|
||||||
|
header_value_str(headers, "x-forwarded-for")
|
||||||
|
.and_then(|value| {
|
||||||
|
value
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.find(|segment| !segment.is_empty() && !segment.eq_ignore_ascii_case("unknown"))
|
||||||
|
.map(|segment| truncate_chars(segment, 45))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
header_value_str(headers, "x-real-ip").and_then(|value| {
|
||||||
|
let value = value.trim();
|
||||||
|
(!value.is_empty() && !value.eq_ignore_ascii_case("unknown"))
|
||||||
|
.then(|| truncate_chars(value, 45))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_chars(value: &str, max_chars: usize) -> String {
|
||||||
|
value.chars().take(max_chars).collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn should_skip_request_header(name: &str) -> bool {
|
pub(crate) fn should_skip_request_header(name: &str) -> bool {
|
||||||
crate::provider_transport::should_skip_request_header(name)
|
crate::provider_transport::should_skip_request_header(name)
|
||||||
}
|
}
|
||||||
@@ -74,3 +129,59 @@ pub(crate) fn header_equals(
|
|||||||
.map(|value| value.eq_ignore_ascii_case(expected))
|
.map(|value| value.eq_ignore_ascii_case(expected))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
request_origin_from_headers, request_origin_from_headers_and_remote_addr, RequestOrigin,
|
||||||
|
};
|
||||||
|
use http::{HeaderMap, HeaderValue};
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_origin_prefers_first_forwarded_for_ip() {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"x-forwarded-for",
|
||||||
|
HeaderValue::from_static(" 203.0.113.8, 10.0.0.1 "),
|
||||||
|
);
|
||||||
|
headers.insert("x-real-ip", HeaderValue::from_static("198.51.100.4"));
|
||||||
|
headers.insert(
|
||||||
|
http::header::USER_AGENT,
|
||||||
|
HeaderValue::from_static("Claude-Code/1.0"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
request_origin_from_headers(&headers),
|
||||||
|
RequestOrigin {
|
||||||
|
client_ip: Some("203.0.113.8".to_string()),
|
||||||
|
user_agent: Some("Claude-Code/1.0".to_string()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_origin_uses_real_ip_after_empty_forwarded_for_segments() {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("x-forwarded-for", HeaderValue::from_static(" , unknown "));
|
||||||
|
headers.insert("x-real-ip", HeaderValue::from_static("198.51.100.4"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
request_origin_from_headers(&headers).client_ip.as_deref(),
|
||||||
|
Some("198.51.100.4")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_origin_falls_back_to_remote_addr() {
|
||||||
|
let headers = HeaderMap::new();
|
||||||
|
let remote_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10)), 443);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
request_origin_from_headers_and_remote_addr(&headers, &remote_addr)
|
||||||
|
.client_ip
|
||||||
|
.as_deref(),
|
||||||
|
Some("192.0.2.10")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ pub(crate) fn sanitize_usage_request_metadata_ref(value: Option<&Value>) -> Opti
|
|||||||
|
|
||||||
fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<String, Value>) {
|
fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<String, Value>) {
|
||||||
copy_non_empty_string(source, target, "trace_id");
|
copy_non_empty_string(source, target, "trace_id");
|
||||||
|
copy_non_empty_string(source, target, "client_ip");
|
||||||
|
copy_non_empty_string(source, target, "user_agent");
|
||||||
copy_bool(source, target, "client_requested_stream");
|
copy_bool(source, target, "client_requested_stream");
|
||||||
copy_bool(source, target, "upstream_is_stream");
|
copy_bool(source, target, "upstream_is_stream");
|
||||||
copy_bool(source, target, "api_key_is_standalone");
|
copy_bool(source, target, "api_key_is_standalone");
|
||||||
@@ -96,6 +98,8 @@ fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<St
|
|||||||
|
|
||||||
fn move_allowed_metadata_fields(mut source: Map<String, Value>, target: &mut Map<String, Value>) {
|
fn move_allowed_metadata_fields(mut source: Map<String, Value>, target: &mut Map<String, Value>) {
|
||||||
remove_non_empty_string(&mut source, target, "trace_id");
|
remove_non_empty_string(&mut source, target, "trace_id");
|
||||||
|
remove_non_empty_string(&mut source, target, "client_ip");
|
||||||
|
remove_non_empty_string(&mut source, target, "user_agent");
|
||||||
remove_bool(&mut source, target, "client_requested_stream");
|
remove_bool(&mut source, target, "client_requested_stream");
|
||||||
remove_bool(&mut source, target, "upstream_is_stream");
|
remove_bool(&mut source, target, "upstream_is_stream");
|
||||||
remove_bool(&mut source, target, "api_key_is_standalone");
|
remove_bool(&mut source, target, "api_key_is_standalone");
|
||||||
@@ -384,6 +388,8 @@ mod tests {
|
|||||||
"model": "gpt-5",
|
"model": "gpt-5",
|
||||||
"candidate_index": 2,
|
"candidate_index": 2,
|
||||||
"trace_id": "trace-1",
|
"trace_id": "trace-1",
|
||||||
|
"client_ip": "203.0.113.8",
|
||||||
|
"user_agent": "Claude-Code/1.0",
|
||||||
"client_requested_stream": false,
|
"client_requested_stream": false,
|
||||||
"upstream_is_stream": true,
|
"upstream_is_stream": true,
|
||||||
"api_key_is_standalone": true,
|
"api_key_is_standalone": true,
|
||||||
@@ -415,6 +421,8 @@ mod tests {
|
|||||||
metadata,
|
metadata,
|
||||||
json!({
|
json!({
|
||||||
"trace_id": "trace-1",
|
"trace_id": "trace-1",
|
||||||
|
"client_ip": "203.0.113.8",
|
||||||
|
"user_agent": "Claude-Code/1.0",
|
||||||
"client_requested_stream": false,
|
"client_requested_stream": false,
|
||||||
"upstream_is_stream": true,
|
"upstream_is_stream": true,
|
||||||
"api_key_is_standalone": true,
|
"api_key_is_standalone": true,
|
||||||
@@ -481,6 +489,8 @@ mod tests {
|
|||||||
"model_id": "model-1",
|
"model_id": "model-1",
|
||||||
"global_model_id": "global-model-1",
|
"global_model_id": "global-model-1",
|
||||||
"global_model_name": "gpt-5",
|
"global_model_name": "gpt-5",
|
||||||
|
"client_ip": "203.0.113.8",
|
||||||
|
"user_agent": "Claude-Code/1.0",
|
||||||
"billing_snapshot": {"status": "complete"}
|
"billing_snapshot": {"status": "complete"}
|
||||||
})
|
})
|
||||||
.as_object()
|
.as_object()
|
||||||
@@ -498,6 +508,8 @@ mod tests {
|
|||||||
"model_id": "model-1",
|
"model_id": "model-1",
|
||||||
"global_model_id": "global-model-1",
|
"global_model_id": "global-model-1",
|
||||||
"global_model_name": "gpt-5",
|
"global_model_name": "gpt-5",
|
||||||
|
"client_ip": "203.0.113.8",
|
||||||
|
"user_agent": "Claude-Code/1.0",
|
||||||
"billing_snapshot": {"status": "complete"}
|
"billing_snapshot": {"status": "complete"}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1531,6 +1531,12 @@ fn build_runtime_request_metadata_seed_from_parts(
|
|||||||
if let Some(trace_id) = context_string(context, "trace_id") {
|
if let Some(trace_id) = context_string(context, "trace_id") {
|
||||||
metadata.insert("trace_id".to_string(), Value::String(trace_id));
|
metadata.insert("trace_id".to_string(), Value::String(trace_id));
|
||||||
}
|
}
|
||||||
|
if let Some(client_ip) = context_string(context, "client_ip") {
|
||||||
|
metadata.insert("client_ip".to_string(), Value::String(client_ip));
|
||||||
|
}
|
||||||
|
if let Some(user_agent) = context_string(context, "user_agent") {
|
||||||
|
metadata.insert("user_agent".to_string(), Value::String(user_agent));
|
||||||
|
}
|
||||||
if let Some(client_requested_stream) = context_bool(context, "client_requested_stream") {
|
if let Some(client_requested_stream) = context_bool(context, "client_requested_stream") {
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
"client_requested_stream".to_string(),
|
"client_requested_stream".to_string(),
|
||||||
@@ -2531,7 +2537,9 @@ mod tests {
|
|||||||
let record = build_pending_usage_record(
|
let record = build_pending_usage_record(
|
||||||
&plan,
|
&plan,
|
||||||
Some(&json!({
|
Some(&json!({
|
||||||
"api_key_is_standalone": true
|
"api_key_is_standalone": true,
|
||||||
|
"client_ip": "203.0.113.8",
|
||||||
|
"user_agent": "Claude-Code/1.0"
|
||||||
})),
|
})),
|
||||||
1_700_000_000,
|
1_700_000_000,
|
||||||
)
|
)
|
||||||
@@ -2540,7 +2548,9 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
record.request_metadata,
|
record.request_metadata,
|
||||||
Some(json!({
|
Some(json!({
|
||||||
"api_key_is_standalone": true
|
"api_key_is_standalone": true,
|
||||||
|
"client_ip": "203.0.113.8",
|
||||||
|
"user_agent": "Claude-Code/1.0"
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user