feat(otel, core): record turn TTFT and TTFM metrics in codex-core (#13630)

### Summary
This adds turn-level latency metrics for the first model output and the
first completed agent message.
- `codex.turn.ttft.duration_ms` starts at turn start and records on the
first output signal we see from the model. That includes normal
assistant text, reasoning deltas, and non-text outputs like tool-call
items.
- `codex.turn.ttfm.duration_ms` also starts at turn start, but it
records when the first agent message finishes streaming rather than when
its first delta arrives.

### Implementation notes
The timing is tracked in codex-core, not app-server, so the definition
stays consistent across CLI, TUI, and app-server clients.

I reused the existing turn lifecycle boundary that already drives
`codex.turn.e2e_duration_ms`, stored the turn start timestamp in turn
state, and record each metric once per turn.

I also wired the new metric names into the OTEL runtime metrics summary
so they show up in the same in-memory/debug snapshot path as the
existing timing metrics.
This commit is contained in:
Owen Lin authored and GitHub committed 2026-03-06 10:23:48 -08:00
1 parent 6c98a59dbd
commit 3449e00bc9
8 files changed
+348 -9

No files matched your search

+2
View File
@@ -20,3 +20,5 @@ pub(crate) const RESPONSES_API_ENGINE_IAPI_TBT_DURATION_METRIC: &str =
"codex.responses_api_engine_iapi_tbt.duration_ms";
pub(crate) const RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC: &str =
"codex.responses_api_engine_service_tbt.duration_ms";
pub(crate) const TURN_TTFT_DURATION_METRIC: &str = "codex.turn.ttft.duration_ms";
pub(crate) const TURN_TTFM_DURATION_METRIC: &str = "codex.turn.ttfm.duration_ms";
@@ -10,6 +10,8 @@ use crate::metrics::names::SSE_EVENT_COUNT_METRIC;
use crate::metrics::names::SSE_EVENT_DURATION_METRIC;
use crate::metrics::names::TOOL_CALL_COUNT_METRIC;
use crate::metrics::names::TOOL_CALL_DURATION_METRIC;
use crate::metrics::names::TURN_TTFM_DURATION_METRIC;
use crate::metrics::names::TURN_TTFT_DURATION_METRIC;
use crate::metrics::names::WEBSOCKET_EVENT_COUNT_METRIC;
use crate::metrics::names::WEBSOCKET_EVENT_DURATION_METRIC;
use crate::metrics::names::WEBSOCKET_REQUEST_COUNT_METRIC;
@@ -49,6 +51,8 @@ pub struct RuntimeMetricsSummary {
pub responses_api_engine_service_ttft_ms: u64,
pub responses_api_engine_iapi_tbt_ms: u64,
pub responses_api_engine_service_tbt_ms: u64,
pub turn_ttft_ms: u64,
pub turn_ttfm_ms: u64,
}
impl RuntimeMetricsSummary {
@@ -64,6 +68,8 @@ impl RuntimeMetricsSummary {
&& self.responses_api_engine_service_ttft_ms == 0
&& self.responses_api_engine_iapi_tbt_ms == 0
&& self.responses_api_engine_service_tbt_ms == 0
&& self.turn_ttft_ms == 0
&& self.turn_ttfm_ms == 0
}
pub fn merge(&mut self, other: Self) {
@@ -90,6 +96,12 @@ impl RuntimeMetricsSummary {
if other.responses_api_engine_service_tbt_ms > 0 {
self.responses_api_engine_service_tbt_ms = other.responses_api_engine_service_tbt_ms;
}
if other.turn_ttft_ms > 0 {
self.turn_ttft_ms = other.turn_ttft_ms;
}
if other.turn_ttfm_ms > 0 {
self.turn_ttfm_ms = other.turn_ttfm_ms;
}
}
pub fn responses_api_summary(&self) -> RuntimeMetricsSummary {
@@ -137,6 +149,8 @@ impl RuntimeMetricsSummary {
sum_histogram_ms(snapshot, RESPONSES_API_ENGINE_IAPI_TBT_DURATION_METRIC);
let responses_api_engine_service_tbt_ms =
sum_histogram_ms(snapshot, RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC);
let turn_ttft_ms = sum_histogram_ms(snapshot, TURN_TTFT_DURATION_METRIC);
let turn_ttfm_ms = sum_histogram_ms(snapshot, TURN_TTFM_DURATION_METRIC);
Self {
tool_calls,
api_calls,
@@ -149,6 +163,8 @@ impl RuntimeMetricsSummary {
responses_api_engine_service_ttft_ms,
responses_api_engine_iapi_tbt_ms,
responses_api_engine_service_tbt_ms,
turn_ttft_ms,
turn_ttfm_ms,
}
}
}