diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index afeb27696a..7951770aa0 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -16737,6 +16737,12 @@ "null" ] }, + "log_sse_deltas": { + "type": [ + "boolean", + "null" + ] + }, "log_user_prompt": { "type": [ "boolean", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index b6a76228a4..170870f46d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -12729,6 +12729,12 @@ "null" ] }, + "log_sse_deltas": { + "type": [ + "boolean", + "null" + ] + }, "log_user_prompt": { "type": [ "boolean", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index caa30bcab5..5f9311e3d7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -1278,6 +1278,12 @@ "null" ] }, + "log_sse_deltas": { + "type": [ + "boolean", + "null" + ] + }, "log_user_prompt": { "type": [ "boolean", diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 5a2d0584e8..9f0c5f19b4 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 24ce1c7ad9..a4070a59a9 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/OtelRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/OtelRequirements.ts index 7279fe8746..588d9993ee 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/OtelRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/OtelRequirements.ts @@ -4,4 +4,4 @@ import type { OtelExporter } from "./OtelExporter"; import type { OtelToolResultLogConfig } from "./OtelToolResultLogConfig"; -export type OtelRequirements = { tool_result: OtelToolResultLogConfig | null, log_user_prompt: boolean | null, log_agent_responses: boolean | null, log_guardian_assessments: boolean | null, environment: string | null, exporter: OtelExporter | null, trace_exporter: OtelExporter | null, metrics_exporter: OtelExporter | null, span_attributes: { [key in string]?: string } | null, tracestate: { [key in string]?: { [key in string]?: string } } | null, }; +export type OtelRequirements = { tool_result: OtelToolResultLogConfig | null, log_sse_deltas: boolean | null, log_user_prompt: boolean | null, log_agent_responses: boolean | null, log_guardian_assessments: boolean | null, environment: string | null, exporter: OtelExporter | null, trace_exporter: OtelExporter | null, metrics_exporter: OtelExporter | null, span_attributes: { [key in string]?: string } | null, tracestate: { [key in string]?: { [key in string]?: string } } | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index 177d000775..09006baf7e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -506,6 +506,7 @@ pub struct FeedbackRequirements { #[ts(export_to = "v2/")] pub struct OtelRequirements { pub tool_result: Option, + pub log_sse_deltas: Option, pub log_user_prompt: Option, pub log_agent_responses: Option, pub log_guardian_assessments: Option, diff --git a/codex-rs/app-server/src/config_manager_service_tests.rs b/codex-rs/app-server/src/config_manager_service_tests.rs index a0738698fc..36bd51fc42 100644 --- a/codex-rs/app-server/src/config_manager_service_tests.rs +++ b/codex-rs/app-server/src/config_manager_service_tests.rs @@ -1870,6 +1870,7 @@ tracestate = { managed = { tenant = "enterprise" }, empty = {} }"#, otel, &serde_json::json!({ "tool_result": { "max_bytes": 1234 }, + "log_sse_deltas": null, "log_user_prompt": false, "log_agent_responses": null, "log_guardian_assessments": null, diff --git a/codex-rs/app-server/src/otel_config.rs b/codex-rs/app-server/src/otel_config.rs index c299989908..f9a1915003 100644 --- a/codex-rs/app-server/src/otel_config.rs +++ b/codex-rs/app-server/src/otel_config.rs @@ -18,6 +18,7 @@ pub(crate) fn map_otel_requirements_to_api(otel: OtelRequirementsToml) -> OtelRe tool_result: otel.tool_result.map(|value| OtelToolResultLogConfig { max_bytes: value.max_bytes, }), + log_sse_deltas: otel.log_sse_deltas, log_user_prompt: otel.log_user_prompt, log_agent_responses: otel.log_agent_responses, log_guardian_assessments: otel.log_guardian_assessments, diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index 8f21fc0edf..a80ce64350 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -717,6 +717,7 @@ async fn config_requirements_read_redacts_otel_exporter_headers( r#"[otel] tool_result = { max_bytes = 1024 } log_user_prompt = false +log_sse_deltas = false log_agent_responses = true log_guardian_assessments = false environment = "managed" @@ -736,6 +737,7 @@ tracestate = { managed = { tenant = "enterprise" } }"# json!({ "tool_result": { "max_bytes": 1024 }, "log_user_prompt": false, + "log_sse_deltas": false, "log_agent_responses": true, "log_guardian_assessments": false, "environment": "managed", diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 0d1fcfdf98..e90c6ac020 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -1115,6 +1115,7 @@ pub struct ConfigRequirementsToml { #[serde(deny_unknown_fields)] pub struct OtelRequirementsToml { pub tool_result: Option, + pub log_sse_deltas: Option, pub log_user_prompt: Option, pub log_agent_responses: Option, pub log_guardian_assessments: Option, @@ -1129,6 +1130,7 @@ pub struct OtelRequirementsToml { impl OtelRequirementsToml { pub(crate) fn is_empty(&self) -> bool { self.tool_result.is_none() + && self.log_sse_deltas.is_none() && self.log_user_prompt.is_none() && self.log_agent_responses.is_none() && self.log_guardian_assessments.is_none() @@ -1772,7 +1774,7 @@ impl ConfigRequirementsToml { return Some("model_providers"); } let otel = self.otel.as_ref(); - let managed_fields: [(bool, &[&str], &'static str); 17] = [ + let managed_fields: [(bool, &[&str], &'static str); 18] = [ ( self.model_provider.is_some(), &["model_provider"], @@ -1823,6 +1825,11 @@ impl ConfigRequirementsToml { &["otel", "log_user_prompt"], "otel.log_user_prompt", ), + ( + otel.is_some_and(|otel| otel.log_sse_deltas.is_some()), + &["otel", "log_sse_deltas"], + "otel.log_sse_deltas", + ), ( otel.is_some_and(|otel| otel.log_agent_responses.is_some()), &["otel", "log_agent_responses"], @@ -1914,6 +1921,7 @@ macro_rules! otel_merge_functions { otel_merge_functions!( tool_result, + log_sse_deltas, log_user_prompt, log_agent_responses, log_guardian_assessments, @@ -2563,6 +2571,7 @@ mod tests { #[test_case::test_case(&["otel", "tool_result"], Some("otel.tool_result"); "tool_result")] #[test_case::test_case(&["otel", "tool_result", "max_bytes"], Some("otel.tool_result"); "tool_result_max_bytes")] #[test_case::test_case(&["otel", "log_user_prompt"], Some("otel.log_user_prompt"); "log_user_prompt")] + #[test_case::test_case(&["otel", "log_sse_deltas"], Some("otel.log_sse_deltas"); "log_sse_deltas")] #[test_case::test_case(&["otel", "environment"], Some("otel.environment"); "environment")] #[test_case::test_case(&["otel", "exporter", "otlp-http", "endpoint"], Some("otel.exporter"); "exporter_endpoint")] #[test_case::test_case(&["otel", "trace_exporter"], Some("otel.trace_exporter"); "trace_exporter")] @@ -2586,6 +2595,7 @@ mod tests { tool_result = { max_bytes = 1024 } log_user_prompt = false environment = "managed" +log_sse_deltas = false exporter = "none" trace_exporter = "none" metrics_exporter = "none" @@ -2599,6 +2609,7 @@ tracestate = { managed = { tenant = "enterprise" }, empty = {} } tool_result = { max_bytes = 4096 } log_user_prompt = true environment = "user" +log_sse_deltas = true exporter = "statsig" trace_exporter = { otlp-grpc = { endpoint = "https://user.example" } } metrics_exporter = "statsig" @@ -2616,6 +2627,7 @@ tracestate = { managed = { tenant = "user", region = "us" }, other = { keep = "y tool_result = { max_bytes = 1024 } log_user_prompt = false environment = "managed" +log_sse_deltas = false exporter = "none" trace_exporter = "none" metrics_exporter = "none" diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 6ec41a99aa..8948588519 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -639,6 +639,9 @@ pub enum OtelExporterKind { pub struct OtelConfigToml { /// Byte limit for tool-result log output; independent of model-visible output. pub tool_result: Option, + /// Log successful SSE events whose kind ends in `.delta`. Defaults to true. + /// Disabling this preserves errors, completion events, and metrics. + pub log_sse_deltas: Option, /// Log user prompt in traces pub log_user_prompt: Option, /// Opt in to logging final main-agent and spawned-subagent responses to an OTLP log exporter. @@ -672,6 +675,7 @@ pub struct OtelConfigToml { #[derive(Debug, Clone, PartialEq)] pub struct OtelConfig { pub tool_result: codex_protocol::config_types::ToolResultLogConfig, + pub log_sse_deltas: bool, pub log_user_prompt: bool, pub log_agent_responses: bool, pub log_guardian_assessments: bool, @@ -687,6 +691,7 @@ impl Default for OtelConfig { fn default() -> Self { OtelConfig { tool_result: Default::default(), + log_sse_deltas: true, log_user_prompt: false, log_agent_responses: false, log_guardian_assessments: false, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 6596cd18e9..16ec1c1f42 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -3780,6 +3780,10 @@ "description": "Opt in to logging completed Guardian assessments to an OTLP log exporter. Defaults to false. Rationales can be sensitive and are capped at 64 KiB.", "type": "boolean" }, + "log_sse_deltas": { + "description": "Log successful SSE events whose kind ends in `.delta`. Defaults to true. Disabling this preserves errors, completion events, and metrics.", + "type": "boolean" + }, "log_user_prompt": { "description": "Log user prompt in traces", "type": "boolean" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 309e2d3d54..70cc395671 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -10327,6 +10327,7 @@ async fn metrics_exporter_defaults_to_statsig_when_missing() -> std::io::Result< .await?; assert_eq!(config.otel.metrics_exporter, OtelExporterKind::Statsig); + assert!(config.otel.log_sse_deltas); assert!(!config.otel.agent_response_logging_enabled()); assert!(!config.otel.guardian_assessment_logging_enabled()); Ok(()) @@ -10338,6 +10339,7 @@ async fn trace_exporter_defaults_to_none_when_log_exporter_is_set() -> std::io:: let mut cfg = fixture.cfg.clone(); cfg.otel = Some(OtelConfigToml { tool_result: Some(toml::from_str("max_bytes = 8192").expect("tool-result logging config")), + log_sse_deltas: Some(false), log_agent_responses: Some(true), log_guardian_assessments: Some(true), exporter: Some(OtelExporterKind::OtlpHttp { @@ -10361,6 +10363,7 @@ async fn trace_exporter_defaults_to_none_when_log_exporter_is_set() -> std::io:: .await?; assert_eq!(config.otel.tool_result.max_bytes, 8192); + assert!(!config.otel.log_sse_deltas); assert!(config.otel.agent_response_logging_enabled()); assert!(config.otel.guardian_assessment_logging_enabled()); assert!(matches!( @@ -13975,6 +13978,10 @@ tracestate = { vendor = { tenant = "managed", region = "us" }, other = { keep = "[otel]\nlog_agent_responses = true\nlog_guardian_assessments = false", "[otel]\nlog_agent_responses = true\nlog_guardian_assessments = false", true; "managed_logging_flags" )] +#[test_case::test_case( + "[otel]\nlog_sse_deltas = true", "[otel]\nlog_sse_deltas = false", + "[otel]\nlog_sse_deltas = false", true; "managed_sse_delta_logs" +)] #[tokio::test] async fn otel_requirements_resolve_config( user_config: &str, diff --git a/codex-rs/core/src/config/otel.rs b/codex-rs/core/src/config/otel.rs index dd75994063..9195e8c066 100644 --- a/codex-rs/core/src/config/otel.rs +++ b/codex-rs/core/src/config/otel.rs @@ -28,6 +28,7 @@ pub(crate) fn resolve_config( OtelConfig { tool_result: config.tool_result.unwrap_or_default(), + log_sse_deltas: config.log_sse_deltas.unwrap_or(true), log_user_prompt, log_agent_responses: config.log_agent_responses.unwrap_or(false), log_guardian_assessments: config.log_guardian_assessments.unwrap_or(false), diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index a41f1471e4..37c22bc42d 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1381,6 +1381,9 @@ impl Session { .with_auth_env(auth_env_telemetry.to_otel_metadata()) .with_user_id(telemetry_auth.and_then(CodexAuth::get_chatgpt_user_id)) .with_tool_result_log_config(config.otel.tool_result); + if !config.otel.log_sse_deltas { + session_telemetry = session_telemetry.without_sse_delta_logs(); + } if let Some(metrics) = thread_extension_data.get::() { session_telemetry = session_telemetry.with_metrics(metrics.as_ref().clone()); } diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index 386312a2bf..82fec7895e 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -518,13 +518,14 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() { #[tokio::test] #[traced_test] -async fn process_sse_emits_completed_telemetry() { +async fn process_sse_emits_completed_telemetry_with_delta_logs_disabled() { let server = start_mock_server().await; - mount_sse_once( + let mock = mount_sse_once( &server, sse(vec![ ev_reasoning_item_added("reasoning-1", &[]), + ev_reasoning_summary_text_delta("Thinking"), serde_json::json!({ "type": "response.completed", "response": { @@ -545,7 +546,13 @@ async fn process_sse_emits_completed_telemetry() { ) .await; - let TestCodex { codex, .. } = test_codex().build(&server).await.unwrap(); + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.otel.log_sse_deltas = false; + }) + .build_with_auto_env(&server) + .await + .unwrap(); codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { @@ -557,6 +564,19 @@ async fn process_sse_emits_completed_telemetry() { wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + let _request = mock.single_request(); + logs_assert(|lines: &[&str]| { + let delta_logged = lines.iter().any(|line| { + line.contains("codex.sse_event") + && line.contains("event.kind=response.reasoning_summary_text.delta") + }); + if delta_logged { + Err("unexpected successful SSE delta log".to_string()) + } else { + Ok(()) + } + }); + logs_assert(|lines: &[&str]| { lines .iter() diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 03caf89fb6..042642df82 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -119,7 +119,7 @@ fn build_session_telemetry( &config.model_provider, auth_manager.codex_api_key_env_enabled(), ); - SessionTelemetry::new( + let mut session_telemetry = SessionTelemetry::new( thread_id, model, model, @@ -131,7 +131,11 @@ fn build_session_telemetry( user_agent(), source, ) - .with_auth_env(auth_env_telemetry.to_otel_metadata()) + .with_auth_env(auth_env_telemetry.to_otel_metadata()); + if !config.otel.log_sse_deltas { + session_telemetry = session_telemetry.without_sse_delta_logs(); + } + session_telemetry } impl MemoryStartupContext { diff --git a/codex-rs/otel/src/events/session_telemetry.rs b/codex-rs/otel/src/events/session_telemetry.rs index ced543ac75..43affe902d 100644 --- a/codex-rs/otel/src/events/session_telemetry.rs +++ b/codex-rs/otel/src/events/session_telemetry.rs @@ -120,6 +120,7 @@ pub struct SessionTelemetryMetadata { #[derive(Debug, Clone)] pub struct SessionTelemetry { tool_result_log_config: ToolResultLogConfig, + log_sse_deltas: bool, pub(crate) metadata: SessionTelemetryMetadata, pub(crate) metrics: Option, pub(crate) metrics_use_metadata_tags: bool, @@ -142,6 +143,12 @@ pub fn bounded_product_sku(product_sku: Option<&str>) -> Option<&'static str> { } impl SessionTelemetry { + /// Suppress successful SSE delta logs while retaining errors and all metrics. + pub fn without_sse_delta_logs(mut self) -> Self { + self.log_sse_deltas = false; + self + } + pub fn with_tool_result_log_config(mut self, config: ToolResultLogConfig) -> Self { self.tool_result_log_config = config; self @@ -609,6 +616,7 @@ impl SessionTelemetry { }); Self { tool_result_log_config: ToolResultLogConfig::default(), + log_sse_deltas: true, metadata: SessionTelemetryMetadata { conversation_id, agent_name, @@ -1065,6 +1073,9 @@ impl SessionTelemetry { duration, &[("kind", kind), ("success", "true")], ); + if !self.log_sse_deltas && kind.ends_with(".delta") { + return; + } log_event!( self, event.name = "codex.sse_event", diff --git a/codex-rs/otel/tests/suite/otel_export_routing_policy.rs b/codex-rs/otel/tests/suite/otel_export_routing_policy.rs index a943f71680..897e3f420c 100644 --- a/codex-rs/otel/tests/suite/otel_export_routing_policy.rs +++ b/codex-rs/otel/tests/suite/otel_export_routing_policy.rs @@ -1,6 +1,12 @@ +use crate::harness::attributes_to_map; +use crate::harness::build_metrics_with_defaults; +use crate::harness::find_metric; +use crate::harness::latest_metrics; use codex_api::AgentIdentityTelemetry; use codex_otel::AuthEnvTelemetryMetadata; use codex_otel::OtelProvider; +use codex_otel::SSE_EVENT_COUNT_METRIC; +use codex_otel::SSE_EVENT_DURATION_METRIC; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; use opentelemetry::KeyValue; @@ -9,12 +15,15 @@ use opentelemetry::trace::TracerProvider as _; use opentelemetry_sdk::logs::InMemoryLogExporter; use opentelemetry_sdk::logs::SdkLogRecord; use opentelemetry_sdk::logs::SdkLoggerProvider; +use opentelemetry_sdk::metrics::data::AggregatedMetrics; +use opentelemetry_sdk::metrics::data::MetricData; use opentelemetry_sdk::trace::InMemorySpanExporter; use opentelemetry_sdk::trace::SdkTracerProvider; use pretty_assertions::assert_eq; use std::borrow::Cow; use std::collections::BTreeMap; use std::path::PathBuf; +use std::time::Duration; use tracing_subscriber::Layer; use tracing_subscriber::filter::filter_fn; use tracing_subscriber::layer::SubscriberExt; @@ -32,7 +41,9 @@ use codex_protocol::protocol::GuardianAssessmentStatus; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use codex_protocol::protocol::TokenUsage; use codex_protocol::user_input::UserInput; +use eventsource_stream::Event as StreamEvent; fn log_attributes(record: &SdkLogRecord) -> BTreeMap { record @@ -100,6 +111,154 @@ fn auth_env_metadata() -> AuthEnvTelemetryMetadata { } } +#[test] +fn suppressing_sse_delta_logs_preserves_errors_completions_and_metrics() -> codex_otel::Result<()> { + for log_sse_deltas in [true, false] { + let log_exporter = InMemoryLogExporter::default(); + let logger_provider = SdkLoggerProvider::builder() + .with_simple_exporter(log_exporter.clone()) + .build(); + let subscriber = tracing_subscriber::registry().with( + opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new( + &logger_provider, + ) + .with_filter(filter_fn(OtelProvider::log_export_filter)), + ); + let (metrics, metric_exporter) = build_metrics_with_defaults(&[])?; + let mut manager = SessionTelemetry::new( + ThreadId::new(), + "gpt-5.1", + "gpt-5.1", + /*account_id*/ None, + /*account_email*/ None, + /*auth_mode*/ None, + "test_originator".to_string(), + /*log_user_prompts*/ false, + "tty".to_string(), + SessionSource::Cli, + ) + .with_metrics_without_metadata_tags(metrics); + if !log_sse_deltas { + manager = manager.without_sse_delta_logs(); + } + let events = [ + ("response.output_text.delta", "{}", true), + ("response.reasoning_summary_text.delta", "{}", true), + ("response.custom_tool_call_input.delta", "{}", true), + ("response.output_text.done", "{}", true), + ("response.completed", "{}", true), + ("response.output_text.delta", "{", false), + ("response.failed", r#"{"error":"failed"}"#, false), + ]; + tracing::subscriber::with_default(subscriber, || { + tracing::callsite::rebuild_interest_cache(); + for (kind, data, _) in events { + manager.log_sse_event::<&str>( + &Ok(Some(Ok(StreamEvent { + event: kind.to_string(), + data: data.to_string(), + id: String::new(), + retry: None, + }))), + Duration::from_millis(10), + ); + } + manager.sse_event_completed( + &TokenUsage { + input_tokens: 7, + ..Default::default() + }, + /*ttft_ms*/ Some(10), + ); + }); + logger_provider.force_flush().expect("flush logs"); + let logs = log_exporter.get_emitted_logs().expect("exported logs"); + let exported_events = logs + .iter() + .map(|log| { + let attributes = log_attributes(&log.record); + ( + attributes["event.kind"].clone(), + attributes.contains_key("error.message"), + attributes.get("input_token_count").cloned(), + ) + }) + .collect::>(); + let mut expected_events = Vec::new(); + if log_sse_deltas { + expected_events.extend([ + ("response.output_text.delta".to_string(), false, None), + ( + "response.reasoning_summary_text.delta".to_string(), + false, + None, + ), + ( + "response.custom_tool_call_input.delta".to_string(), + false, + None, + ), + ]); + } + expected_events.extend([ + ("response.output_text.done".to_string(), false, None), + ("response.completed".to_string(), false, None), + ("response.output_text.delta".to_string(), true, None), + ("response.failed".to_string(), true, None), + ( + "response.completed".to_string(), + false, + Some("7".to_string()), + ), + ]); + assert_eq!(exported_events, expected_events); + + manager.shutdown_metrics()?; + let resource_metrics = latest_metrics(&metric_exporter); + let expected_counts = events + .iter() + .map(|(kind, _, success)| { + ( + BTreeMap::from([ + ("kind".to_string(), kind.to_string()), + ("success".to_string(), success.to_string()), + ]), + 1, + ) + }) + .collect::>(); + let counter = find_metric(&resource_metrics, SSE_EVENT_COUNT_METRIC).expect("SSE counter"); + let AggregatedMetrics::U64(MetricData::Sum(sum)) = counter.data() else { + panic!("expected SSE counter"); + }; + assert_eq!( + sum.data_points() + .map(|point| (attributes_to_map(point.attributes()), point.value())) + .collect::>(), + expected_counts, + ); + let duration = + find_metric(&resource_metrics, SSE_EVENT_DURATION_METRIC).expect("SSE duration"); + let AggregatedMetrics::F64(MetricData::Histogram(histogram)) = duration.data() else { + panic!("expected SSE duration histogram"); + }; + assert_eq!( + histogram + .data_points() + .map(|point| ( + attributes_to_map(point.attributes()), + (point.count(), point.sum()) + )) + .collect::>(), + expected_counts + .into_keys() + .map(|tags| (tags, (1, 10.0))) + .collect(), + ); + } + Ok(()) +} + #[test] fn otel_export_routing_policy_routes_prompt_and_guardian_payloads() { let log_exporter = InMemoryLogExporter::default(); diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index c3a2c63d9c..82a5c8c088 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -9257,6 +9257,7 @@ class OtelRequirements(BaseModel): exporter: OtelExporter | None = None log_agent_responses: bool | None = None log_guardian_assessments: bool | None = None + log_sse_deltas: bool | None = None log_user_prompt: bool | None = None metrics_exporter: OtelExporter | None = None span_attributes: dict[str, Any] | None = None