Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ pub(crate) struct CompactConversationRequestSettings {
pub(crate) service_tier: Option<String>,
}

fn session_telemetry_for_request(
Comment thread
daniel-oai marked this conversation as resolved.
session_telemetry: &SessionTelemetry,
request: &ResponsesApiRequest,
) -> SessionTelemetry {
session_telemetry.clone().with_inference_request(
request.service_tier.as_deref(),
request
.reasoning
.as_ref()
.and_then(|reasoning| reasoning.effort.as_ref()),
)
}

/// Session-scoped state shared by all [`ModelClient`] clones.
///
/// This is intentionally kept minimal so `ModelClient` does not need to hold a full `Config`. Most
Expand Down Expand Up @@ -1318,6 +1331,8 @@ impl ModelClientSession {
let store = request.store;
self.client
.prepare_response_items_for_request(&mut request.input, store);
let request_session_telemetry =
session_telemetry_for_request(session_telemetry, &request);
let inference_trace_attempt = inference_trace.start_attempt();
inference_trace_attempt.add_request_headers(&mut options.extra_headers);
inference_trace_attempt.record_started(&request);
Expand All @@ -1333,7 +1348,7 @@ impl ModelClientSession {
Ok(stream) => {
let (stream, _) = map_response_stream(
stream,
session_telemetry.clone(),
request_session_telemetry,
inference_trace_attempt,
);
return Ok(stream);
Expand Down Expand Up @@ -1423,6 +1438,12 @@ impl ModelClientSession {
service_tier.clone(),
responses_metadata,
)?;
let request_session_telemetry = if warmup {
// `generate=false` prewarm is connection setup, not an inference request.
session_telemetry.clone()
} else {
session_telemetry_for_request(session_telemetry, &request)
};
let mut client_metadata = self
.client
.build_ws_client_metadata(responses_metadata, model_info.use_responses_lite);
Expand Down Expand Up @@ -1525,7 +1546,7 @@ impl ModelClientSession {
})?;
let (stream, last_request_rx) = map_response_stream(
stream_result,
session_telemetry.clone(),
request_session_telemetry,
inference_trace_attempt,
);
self.websocket_session.last_response_rx = Some(last_request_rx);
Expand Down
32 changes: 30 additions & 2 deletions codex-rs/core/tests/suite/otel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use codex_features::Feature;
use codex_otel::SessionTelemetry;
use codex_otel::TelemetryAuthMode;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
Expand Down Expand Up @@ -114,9 +115,17 @@ fn extract_log_field_does_not_confuse_similar_keys() {
async fn responses_api_emits_api_request_event() {
let server = start_mock_server().await;

mount_sse_once(&server, sse(vec![ev_completed("done")])).await;
let response_mock = mount_sse_once(&server, sse(vec![ev_completed("done")])).await;

let TestCodex { codex, .. } = test_codex().build(&server).await.unwrap();
let TestCodex { codex, .. } = test_codex()
.with_model("gpt-5.4")
.with_config(|config| {
config.service_tier = Some(ServiceTier::Fast.request_value().to_string());
config.model_reasoning_effort = Some(ReasoningEffort::High);
})
.build(&server)
.await
.unwrap();

codex
.submit(Op::UserInput {
Expand All @@ -134,6 +143,10 @@ async fn responses_api_emits_api_request_event() {

wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

let request_body = response_mock.single_request().body_json();
assert_eq!(request_body["service_tier"].as_str(), Some("priority"));
assert_eq!(request_body["reasoning"]["effort"].as_str(), Some("high"));

logs_assert(|lines: &[&str]| {
lines
.iter()
Expand All @@ -142,6 +155,21 @@ async fn responses_api_emits_api_request_event() {
.unwrap_or_else(|| Err("expected codex.api_request event".to_string()))
});

logs_assert(|lines: &[&str]| {
lines
.iter()
.find(|line| {
line.contains("codex.sse_event")
&& line.contains("event.kind=response.completed")
&& line.contains("service_tier=\"priority\"")
&& line.contains("model_reasoning_effort=\"high\"")
})
.map(|_| Ok(()))
.unwrap_or_else(|| {
Err("expected response.completed event with inference attributes".to_string())
})
});

logs_assert(|lines: &[&str]| {
lines
.iter()
Expand Down
16 changes: 16 additions & 0 deletions codex-rs/otel/src/events/session_telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ pub struct SessionTelemetryMetadata {
pub(crate) session_source: String,
pub(crate) model: String,
pub(crate) slug: String,
pub(crate) service_tier: Option<String>,
pub(crate) model_reasoning_effort: Option<String>,
pub(crate) log_user_prompts: bool,
pub(crate) app_version: &'static str,
pub(crate) terminal_type: String,
Expand All @@ -118,6 +120,16 @@ impl SessionTelemetry {
self
}

pub fn with_inference_request(
mut self,
service_tier: Option<&str>,
model_reasoning_effort: Option<&ReasoningEffort>,
) -> Self {
self.metadata.service_tier = service_tier.map(str::to_owned);
self.metadata.model_reasoning_effort = model_reasoning_effort.map(ToString::to_string);
self
}

pub fn with_metrics_service_name(mut self, service_name: &str) -> Self {
self.metadata.service_name = Some(sanitize_metric_tag_value(service_name));
self
Expand Down Expand Up @@ -389,6 +401,8 @@ impl SessionTelemetry {
session_source: session_source.to_string(),
model: model.to_owned(),
slug: slug.to_owned(),
service_tier: None,
model_reasoning_effort: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we populate these values upstream? service_tier and model_reasoning_effort are thread-level settings, similar to model

that way we can get rid of with_inference_request

log_user_prompts,
app_version: env!("CARGO_PKG_VERSION"),
terminal_type,
Expand Down Expand Up @@ -932,6 +946,8 @@ impl SessionTelemetry {
cached_token_count = cached_token_count,
reasoning_token_count = reasoning_token_count,
tool_token_count = %tool_token_count,
service_tier = self.metadata.service_tier.as_deref(),
model_reasoning_effort = self.metadata.model_reasoning_effort.as_deref(),
},
log: {},
trace: {},
Expand Down
Loading