diff --git a/crates/keplor-server/src/routes.rs b/crates/keplor-server/src/routes.rs index d72a2da..70dabc7 100644 --- a/crates/keplor-server/src/routes.rs +++ b/crates/keplor-server/src/routes.rs @@ -224,6 +224,16 @@ pub struct EventResponse { pub streaming: bool, pub error: Option, pub metadata: Option, + /// Client source IP as a string. Surfaced for SDK / geographic + /// breakdowns; never None in practice — events without a recorded + /// IP show as `null`. Consumers should bucket to /24 for IPv4 + /// before display rather than render raw addresses. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_ip: Option, + /// Caller-supplied User-Agent string. Used by clients to derive + /// coarse SDK identification (e.g. `openai-python/1.42`). + #[serde(skip_serializing_if = "Option::is_none")] + pub user_agent: Option, } /// Token usage in query response. @@ -317,6 +327,8 @@ pub async fn query_events( endpoint: e.endpoint, streaming: e.streaming, error: e.error_type, + client_ip: e.client_ip, + user_agent: e.user_agent, metadata, } }) @@ -506,6 +518,8 @@ fn llm_event_to_response(ev: keplor_core::LlmEvent) -> EventResponse { .to_owned() }), metadata: ev.metadata, + client_ip: ev.client_ip.map(|ip| ip.to_string()), + user_agent: ev.user_agent.map(|ua| ua.to_string()), } } @@ -1024,6 +1038,8 @@ pub async fn export_events( .metadata_json .as_deref() .and_then(|s| serde_json::from_str(s).ok()), + client_ip: event.client_ip, + user_agent: event.user_agent, }; if let Ok(json) = serde_json::to_string(&resp) { lines.push(json); diff --git a/crates/keplor-store/src/kdb_store.rs b/crates/keplor-store/src/kdb_store.rs index 54e17a5..a4f82b4 100644 --- a/crates/keplor-store/src/kdb_store.rs +++ b/crates/keplor-store/src/kdb_store.rs @@ -817,6 +817,8 @@ fn llm_to_summary(ev: LlmEvent) -> EventSummary { source: ev.source.map(|s| s.to_string()), error_type: ev.error.as_ref().map(|e| provider_error_type_key(e).to_owned()), metadata_json: ev.metadata.as_ref().map(|v| v.to_string()), + client_ip: ev.client_ip.map(|ip| ip.to_string()), + user_agent: ev.user_agent.map(|ua| ua.to_string()), } } diff --git a/crates/keplor-store/src/types.rs b/crates/keplor-store/src/types.rs index 3416ba9..5711fdf 100644 --- a/crates/keplor-store/src/types.rs +++ b/crates/keplor-store/src/types.rs @@ -79,6 +79,14 @@ pub struct EventSummary { pub error_type: Option, /// Arbitrary metadata as JSON text. pub metadata_json: Option, + /// Client source IP as a string ("203.0.113.1" or "2001:db8::1"). + /// Already accepted on ingest and stored in the label table — + /// surfaced here so consumers can derive geographic / network + /// breakdowns without re-querying the raw event row. + pub client_ip: Option, + /// Caller-supplied User-Agent header. Used to derive coarse SDK + /// identification (e.g. "openai-python", "anthropic-sdk-typescript"). + pub user_agent: Option, } /// Cost + event count from a quota query.