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:
fawney19
2026-04-30 11:26:56 +08:00
committed by GitHub
parent 2b5247b9a8
commit 7fcb9e6292
12 changed files with 242 additions and 6 deletions

View File

@@ -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>) {
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, "upstream_is_stream");
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>) {
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, "upstream_is_stream");
remove_bool(&mut source, target, "api_key_is_standalone");
@@ -384,6 +388,8 @@ mod tests {
"model": "gpt-5",
"candidate_index": 2,
"trace_id": "trace-1",
"client_ip": "203.0.113.8",
"user_agent": "Claude-Code/1.0",
"client_requested_stream": false,
"upstream_is_stream": true,
"api_key_is_standalone": true,
@@ -415,6 +421,8 @@ mod tests {
metadata,
json!({
"trace_id": "trace-1",
"client_ip": "203.0.113.8",
"user_agent": "Claude-Code/1.0",
"client_requested_stream": false,
"upstream_is_stream": true,
"api_key_is_standalone": true,
@@ -481,6 +489,8 @@ mod tests {
"model_id": "model-1",
"global_model_id": "global-model-1",
"global_model_name": "gpt-5",
"client_ip": "203.0.113.8",
"user_agent": "Claude-Code/1.0",
"billing_snapshot": {"status": "complete"}
})
.as_object()
@@ -498,6 +508,8 @@ mod tests {
"model_id": "model-1",
"global_model_id": "global-model-1",
"global_model_name": "gpt-5",
"client_ip": "203.0.113.8",
"user_agent": "Claude-Code/1.0",
"billing_snapshot": {"status": "complete"}
})
);

View File

@@ -1531,6 +1531,12 @@ fn build_runtime_request_metadata_seed_from_parts(
if let Some(trace_id) = context_string(context, "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") {
metadata.insert(
"client_requested_stream".to_string(),
@@ -2531,7 +2537,9 @@ mod tests {
let record = build_pending_usage_record(
&plan,
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,
)
@@ -2540,7 +2548,9 @@ mod tests {
assert_eq!(
record.request_metadata,
Some(json!({
"api_key_is_standalone": true
"api_key_is_standalone": true,
"client_ip": "203.0.113.8",
"user_agent": "Claude-Code/1.0"
}))
);
}