From d1ea58e87e8bd67a3dc8702c38067533dd3ac4f2 Mon Sep 17 00:00:00 2001 From: Anton Panasenko Date: Thu, 11 Sep 2025 15:43:45 -0700 Subject: [PATCH] [codex] implement codex.user_prompt trace events --- codex-rs/core/src/client.rs | 4 ++ codex-rs/core/src/codex.rs | 19 ++++--- codex-rs/core/src/config.rs | 7 +++ codex-rs/core/src/config_types.rs | 4 ++ .../core/tests/chat_completions_payload.rs | 1 + codex-rs/core/tests/chat_completions_sse.rs | 1 + codex-rs/otel/src/trace_manager.rs | 49 +++++++++++++++++++ 7 files changed, 77 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index d4d5dec889..f78f9d046b 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -941,6 +941,7 @@ mod tests { "test", None, AuthMode::ChatGPT, + false, "test".to_string(), ); @@ -1011,6 +1012,7 @@ mod tests { "test", None, AuthMode::ChatGPT, + false, "test".to_string(), ); @@ -1054,6 +1056,7 @@ mod tests { "test", None, AuthMode::ChatGPT, + false, "test".to_string(), ); @@ -1168,6 +1171,7 @@ mod tests { "test", None, AuthMode::ChatGPT, + false, "test".to_string(), ); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 4c95d86b99..534bb0e126 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -451,6 +451,7 @@ impl Session { config.model_family.slug.as_str(), auth_manager.auth().and_then(|a| a.get_account_id()), auth_manager.preferred_auth_method(), + config.otel.log_user_prompt, terminal::user_agent(), ); @@ -1231,10 +1232,10 @@ async fn submission_loop( updated_config.model_context_window = Some(model_info.context_window); } - let trace_manager = prev - .client - .get_trace_manager() - .with_model(updated_config.model.as_str(), updated_config.model_family.slug.as_str()); + let trace_manager = prev.client.get_trace_manager().with_model( + updated_config.model.as_str(), + updated_config.model_family.slug.as_str(), + ); let client = ModelClient::new( Arc::new(updated_config), @@ -1290,6 +1291,7 @@ async fn submission_loop( } } Op::UserInput { items } => { + let _ = turn_context.client.get_trace_manager().user_prompt(&items); // attempt to inject input into current task if let Err(items) = sess.inject_input(items).await { // no current task, spawn a new one @@ -1307,6 +1309,7 @@ async fn submission_loop( effort, summary, } => { + let _ = turn_context.client.get_trace_manager().user_prompt(&items); // attempt to inject input into current task if let Err(items) = sess.inject_input(items).await { // Derive a fresh TurnContext for this turn using the provided overrides. @@ -1325,10 +1328,10 @@ async fn submission_loop( per_turn_config.model_context_window = Some(model_info.context_window); } - let trace_manager = turn_context - .client - .get_trace_manager() - .with_model(per_turn_config.model.as_str(), per_turn_config.model_family.slug.as_str()); + let trace_manager = turn_context.client.get_trace_manager().with_model( + per_turn_config.model.as_str(), + per_turn_config.model_family.slug.as_str(), + ); // Build a new client with per‑turn reasoning settings. // Reuse the same provider and session id; auth defaults to env/API key. diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 62272cc0c8..4de04b20e2 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -1068,12 +1068,14 @@ impl Config { use crate::config_types::OtelSampler; let t: OtelConfigToml = cfg.otel.unwrap_or_default(); let enabled = t.enabled.unwrap_or(false); + let log_user_prompt = t.log_user_prompt.unwrap_or(false); let environment = t .environment .unwrap_or(DEFAULT_OTEL_ENVIRONMENT.to_string()); if !enabled { OtelConfig { enabled, + log_user_prompt, environment, sampler: OtelSampler::AlwaysOn, exporter: OtelExporterKind::None, @@ -1083,6 +1085,7 @@ impl Config { let exporter = t.exporter.unwrap_or(OtelExporterKind::OtlpFile); OtelConfig { enabled, + log_user_prompt, environment, sampler, exporter, @@ -1669,6 +1672,7 @@ model_verbosity = "high" tui_notifications: Default::default(), otel: crate::config_types::OtelConfig { enabled: false, + log_user_prompt: false, environment: DEFAULT_OTEL_ENVIRONMENT.to_string(), exporter: crate::config_types::OtelExporterKind::None, sampler: crate::config_types::OtelSampler::AlwaysOn, @@ -1733,6 +1737,7 @@ model_verbosity = "high" tui_notifications: Default::default(), otel: crate::config_types::OtelConfig { enabled: false, + log_user_prompt: false, environment: DEFAULT_OTEL_ENVIRONMENT.to_string(), exporter: crate::config_types::OtelExporterKind::None, sampler: crate::config_types::OtelSampler::AlwaysOn, @@ -1812,6 +1817,7 @@ model_verbosity = "high" tui_notifications: Default::default(), otel: crate::config_types::OtelConfig { enabled: false, + log_user_prompt: false, environment: DEFAULT_OTEL_ENVIRONMENT.to_string(), exporter: crate::config_types::OtelExporterKind::None, sampler: crate::config_types::OtelSampler::AlwaysOn, @@ -1877,6 +1883,7 @@ model_verbosity = "high" tui_notifications: Default::default(), otel: crate::config_types::OtelConfig { enabled: false, + log_user_prompt: false, environment: DEFAULT_OTEL_ENVIRONMENT.to_string(), exporter: crate::config_types::OtelExporterKind::None, sampler: crate::config_types::OtelSampler::AlwaysOn, diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 2c2dfc9b75..fdbaa673ab 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -117,6 +117,9 @@ pub struct OtelConfigToml { /// Enable or disable OTEL entirely. Defaults to false. pub enabled: Option, + /// Log user prompt in traces + pub log_user_prompt: Option, + /// Mark traces with environment (dev, staging, prod, test). Defaults to dev. pub environment: Option, @@ -131,6 +134,7 @@ pub struct OtelConfigToml { #[derive(Debug, Clone, PartialEq)] pub struct OtelConfig { pub enabled: bool, + pub log_user_prompt: bool, pub environment: String, pub sampler: OtelSampler, pub exporter: OtelExporterKind, diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index fabade7e70..e9feb1807d 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -80,6 +80,7 @@ async fn run_request(input: Vec) -> Value { config.model_family.slug.as_str(), None, AuthMode::ChatGPT, + false, "test".to_string(), ); diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index 2497e6f15c..66f72b22ac 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -73,6 +73,7 @@ async fn run_stream(sse_body: &str) -> Vec { config.model_family.slug.as_str(), None, AuthMode::ChatGPT, + false, "test".to_string(), ); diff --git a/codex-rs/otel/src/trace_manager.rs b/codex-rs/otel/src/trace_manager.rs index 5a74040b9e..7ce3e7928a 100644 --- a/codex-rs/otel/src/trace_manager.rs +++ b/codex-rs/otel/src/trace_manager.rs @@ -3,6 +3,7 @@ use chrono::Utc; use codex_protocol::mcp_protocol::AuthMode; use codex_protocol::mcp_protocol::ConversationId; use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::InputItem; use opentelemetry::propagation::TextMapPropagator; use opentelemetry_http::HeaderInjector; use opentelemetry_sdk::propagation::TraceContextPropagator; @@ -132,6 +133,39 @@ impl SSESpan { } } +pub struct UserPromptSpan(pub(crate) Span); + +impl UserPromptSpan { + pub fn new(metadata: TraceMetadata, prompt: &str) -> Self { + let prompt_to_log = if metadata.log_user_prompts { + prompt + } else { + "[REDACTED]" + }; + + let span = info_span!( + "codex.user_prompt", + session.id = %metadata.conversation_id, + app.version = %metadata.app_version, + user.account_id = tracing::field::Empty, + terminal.type = %metadata.terminal_type, + event.timestamp = %timestamp(), + prompt_length = %prompt.chars().count(), + prompt = %prompt_to_log, + ); + + if let Some(account_id) = &metadata.account_id { + span.record("user.account_id", account_id); + } + + Self(span) + } + + pub fn span(&self) -> Span { + self.0.clone() + } +} + #[derive(Debug, Clone)] pub struct TraceMetadata { conversation_id: ConversationId, @@ -139,6 +173,7 @@ pub struct TraceMetadata { account_id: Option, model: String, slug: String, + log_user_prompts: bool, app_version: &'static str, terminal_type: String, } @@ -155,6 +190,7 @@ impl TraceManager { slug: &str, account_id: Option, auth_mode: AuthMode, + log_user_prompts: bool, terminal_type: String, ) -> TraceManager { Self { @@ -164,6 +200,7 @@ impl TraceManager { account_id, model: model.to_owned(), slug: slug.to_owned(), + log_user_prompts, app_version: env!("CARGO_PKG_VERSION"), terminal_type, }, @@ -191,6 +228,18 @@ impl TraceManager { pub fn response(&self) -> SSESpan { SSESpan::new(self.metadata.clone()) } + + pub fn user_prompt(&self, items: &[InputItem]) -> UserPromptSpan { + let prompt = items + .iter() + .flat_map(|item| match item { + InputItem::Text { text } => Some(text.as_str()), + _ => None, + }) + .collect::(); + + UserPromptSpan::new(self.metadata.clone(), prompt.as_ref()) + } } fn timestamp() -> String {