Export turn cost as an OTEL metric (#40488)

## What changed

- Emit `codex.turn.cost_microusd` as a counter with turn, conversation,
  interruption, speed, and reasoning-effort attributes. Convert the estimated
  USD string to microdollars, rounding to the nearest microdollar and skipping
  values that cannot be represented safely.
- Start turn-cost collection when an OTLP metrics exporter is configured, even
  when the OTEL log exporter is disabled.

## Testing

- Verify cost conversion, rounding, and metric attributes with an in-memory
  metrics snapshot.
- Verify that a metrics-only OTLP configuration starts the turn-cost worker.

GitOrigin-RevId: e60587079f097f10cc1b6d1bf889d3f7a0dc3d5f
This commit is contained in:
xli-oai authored and copyberry committed 2026-08-24 22:44:33 +00:00
1 parent fb9311db5c
commit 2497518490
7 files changed
+151 -2

No files matched your search

+65
View File
@@ -155,3 +155,68 @@ fn manager_snapshot_metrics_collects_without_shutdown() -> Result<()> {
Ok(())
}
#[test]
fn manager_turn_cost_records_microusd_metric() -> Result<()> {
let exporter = InMemoryMetricExporter::default();
let config = MetricsConfig::in_memory("test", "codex-cli", env!("CARGO_PKG_VERSION"), exporter)
.with_runtime_reader();
let metrics = MetricsClient::new(config)?;
let thread_id = ThreadId::new();
let conversation_id = thread_id.to_string();
let manager = SessionTelemetry::new(
thread_id,
"gpt-5.6",
"gpt-5.6",
/*account_id*/ None,
/*account_email*/ None,
Some(TelemetryAuthMode::ApiKey),
"test_originator".to_string(),
/*log_user_prompts*/ false,
"tty".to_string(),
SessionSource::Cli,
)
.with_metrics(metrics);
manager.record_turn_cost(
"turn-123",
"0.0001245",
/*interrupted*/ false,
Some("fast"),
Some("high"),
);
let snapshot = manager.snapshot_metrics()?;
let metric = find_metric(&snapshot, "codex.turn.cost_microusd")
.expect("turn-cost microdollar metric missing");
let point = match metric.data() {
AggregatedMetrics::U64(MetricData::Sum(sum)) => {
sum.data_points().next().expect("turn-cost data point")
}
_ => panic!("unexpected turn-cost metric data type"),
};
assert_eq!(point.value(), 125);
assert_eq!(
attributes_to_map(point.attributes()),
BTreeMap::from([
(
"app.version".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
),
(
"auth_mode".to_string(),
TelemetryAuthMode::ApiKey.to_string(),
),
("conversation.id".to_string(), conversation_id),
("model".to_string(), "gpt-5.6".to_string()),
("originator".to_string(), "test_originator".to_string()),
("reasoning_effort".to_string(), "high".to_string()),
("session_source".to_string(), "cli".to_string()),
("speed".to_string(), "fast".to_string()),
("turn.id".to_string(), "turn-123".to_string()),
("turn.interrupted".to_string(), "false".to_string()),
])
);
Ok(())
}