1use super::*;
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4#[serde(tag = "type", rename_all = "snake_case")]
5pub enum ControlTraceDetail {
6 RequestCompleted {
7 method: Option<String>,
8 path: Option<String>,
9 status_code: Option<u16>,
10 duration_ms: Option<u64>,
11 station_name: Option<String>,
12 provider_id: Option<String>,
13 upstream_base_url: Option<String>,
14 service_tier: ServiceTierLog,
15 },
16 RetryOptions {
17 upstream_max_attempts: Option<u32>,
18 provider_max_attempts: Option<u32>,
19 allow_cross_station_before_first_output: Option<bool>,
20 },
21 AttemptSelect {
22 station_name: Option<String>,
23 upstream_index: Option<u64>,
24 upstream_base_url: Option<String>,
25 provider_id: Option<String>,
26 endpoint_id: Option<String>,
27 provider_endpoint_key: Option<String>,
28 preference_group: Option<u64>,
29 model: Option<String>,
30 },
31 LoadBalancerSelection {
32 mode: Option<String>,
33 pinned_source: Option<String>,
34 pinned_name: Option<String>,
35 selected_station: Option<String>,
36 selected_stations: Vec<String>,
37 active_station: Option<String>,
38 note: Option<String>,
39 },
40 ProviderRuntimeOverride {
41 provider_name: Option<String>,
42 endpoint_name: Option<String>,
43 base_urls: Vec<String>,
44 enabled: Option<bool>,
45 clear_enabled: bool,
46 runtime_state: Option<String>,
47 clear_runtime_state: bool,
48 },
49 RouteGraphSelectionExplain {
50 request_model: Option<String>,
51 continuity_class: Option<String>,
52 provider_failover_allowed: Option<bool>,
53 provider_failover_blocked_reason: Option<String>,
54 balance_signal_authoritative: Option<bool>,
55 affinity_policy: Option<String>,
56 affinity_source: Option<String>,
57 affinity_provider_endpoint_key: Option<String>,
58 selected_matches_affinity: Option<bool>,
59 selected_provider_id: Option<String>,
60 selected_endpoint_id: Option<String>,
61 selected_provider_endpoint_key: Option<String>,
62 selected_preference_group: Option<u64>,
63 skipped_higher_priority_groups: Vec<u64>,
64 skipped_higher_priority_candidates: Vec<ControlTraceRouteGraphSkippedCandidate>,
65 },
66 RetryEvent {
67 event_name: String,
68 station_name: Option<String>,
69 upstream_base_url: Option<String>,
70 mode: Option<String>,
71 note: Option<String>,
72 },
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76pub struct ControlTraceRouteGraphSkippedCandidate {
77 pub provider_id: Option<String>,
78 pub endpoint_id: Option<String>,
79 pub provider_endpoint_key: Option<String>,
80 pub preference_group: Option<u64>,
81 pub route_path: Vec<String>,
82 pub reasons: Vec<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ControlTraceLogEntry {
87 pub ts_ms: u64,
88 pub kind: String,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub service: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub request_id: Option<u64>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub trace_id: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub event: Option<String>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub detail: Option<ControlTraceDetail>,
99 #[serde(default)]
100 pub payload: JsonValue,
101}
102
103impl ControlTraceLogEntry {
104 pub fn resolved_trace_id(&self) -> Option<String> {
105 derive_control_trace_id(
106 self.trace_id.as_deref(),
107 self.service.as_deref(),
108 self.request_id,
109 &self.payload,
110 )
111 }
112
113 pub fn resolved_detail(&self) -> Option<ControlTraceDetail> {
114 self.detail.clone().or_else(|| {
115 infer_control_trace_detail(self.kind.as_str(), self.event.as_deref(), &self.payload)
116 })
117 }
118}
119
120pub fn control_trace_path() -> PathBuf {
121 std::env::var("CODEX_HELPER_CONTROL_TRACE_PATH")
122 .ok()
123 .map(|s| s.trim().to_string())
124 .filter(|s| !s.is_empty())
125 .map(PathBuf::from)
126 .unwrap_or_else(|| proxy_home_dir().join("logs").join("control_trace.jsonl"))
127}
128
129fn control_trace_enabled() -> bool {
130 static ENABLED: OnceLock<bool> = OnceLock::new();
131 *ENABLED.get_or_init(|| env_bool_default("CODEX_HELPER_CONTROL_TRACE", true))
132}
133
134fn retry_trace_enabled() -> bool {
135 static ENABLED: OnceLock<bool> = OnceLock::new();
136 *ENABLED.get_or_init(|| env_bool("CODEX_HELPER_RETRY_TRACE"))
137}
138
139fn retry_trace_path() -> PathBuf {
140 std::env::var("CODEX_HELPER_RETRY_TRACE_PATH")
141 .ok()
142 .map(|s| s.trim().to_string())
143 .filter(|s| !s.is_empty())
144 .map(PathBuf::from)
145 .unwrap_or_else(|| proxy_home_dir().join("logs").join("retry_trace.jsonl"))
146}
147
148fn control_trace_read_window(limit: usize) -> usize {
149 limit.saturating_mul(4).clamp(80, 400)
150}
151
152pub fn read_recent_control_trace_entries(
153 limit: usize,
154) -> anyhow::Result<Vec<ControlTraceLogEntry>> {
155 read_recent_control_trace_entries_from_path(
156 &control_trace_path(),
157 limit,
158 request_log_retention(),
159 )
160}
161
162fn read_recent_control_trace_entries_from_path(
163 path: &std::path::Path,
164 limit: usize,
165 retention: LogRetention,
166) -> anyhow::Result<Vec<ControlTraceLogEntry>> {
167 use std::collections::VecDeque;
168 use std::io::{BufRead, BufReader};
169
170 crate::local_log_store::repair_log(path, retention);
171 if !path.exists() {
172 return Ok(Vec::new());
173 }
174
175 let file = fs::File::open(path)?;
176 let reader = BufReader::new(file);
177 let mut ring = VecDeque::with_capacity(control_trace_read_window(limit));
178 for line in reader.lines() {
179 let line = line?;
180 if line.trim().is_empty() {
181 continue;
182 }
183 if ring.len() == ring.capacity() {
184 ring.pop_front();
185 }
186 ring.push_back(line);
187 }
188
189 let mut out = Vec::new();
190 for line in ring {
191 let Ok(entry) = serde_json::from_str::<ControlTraceLogEntry>(&line) else {
192 continue;
193 };
194 out.push(hydrate_control_trace_entry(entry));
195 }
196 out.sort_by_key(|entry| std::cmp::Reverse(entry.ts_ms));
197 Ok(out)
198}
199
200fn hydrate_control_trace_entry(mut entry: ControlTraceLogEntry) -> ControlTraceLogEntry {
201 if entry.trace_id.is_none() {
202 entry.trace_id = entry.resolved_trace_id();
203 }
204 if entry.detail.is_none() {
205 entry.detail =
206 infer_control_trace_detail(entry.kind.as_str(), entry.event.as_deref(), &entry.payload);
207 }
208 entry
209}
210
211fn json_u64_field(value: &JsonValue, key: &str) -> Option<u64> {
212 value.get(key).and_then(|value| match value {
213 JsonValue::Number(number) => number.as_u64(),
214 JsonValue::String(text) => text.trim().parse::<u64>().ok(),
215 _ => None,
216 })
217}
218
219fn json_u16_field(value: &JsonValue, key: &str) -> Option<u16> {
220 json_u64_field(value, key).and_then(|value| u16::try_from(value).ok())
221}
222
223fn json_string_field(value: &JsonValue, key: &str) -> Option<String> {
224 value
225 .get(key)
226 .and_then(|value| value.as_str())
227 .map(str::trim)
228 .filter(|value| !value.is_empty())
229 .map(ToOwned::to_owned)
230}
231
232fn json_bool_field(value: &JsonValue, key: &str) -> Option<bool> {
233 value.get(key).and_then(|value| value.as_bool())
234}
235
236fn json_nested_string_field(value: &JsonValue, path: &[&str]) -> Option<String> {
237 let mut current = value;
238 for segment in path {
239 current = current.get(*segment)?;
240 }
241 current
242 .as_str()
243 .map(str::trim)
244 .filter(|value| !value.is_empty())
245 .map(ToOwned::to_owned)
246}
247
248fn json_nested_u64_field(value: &JsonValue, path: &[&str]) -> Option<u64> {
249 let mut current = value;
250 for segment in path {
251 current = current.get(*segment)?;
252 }
253 match current {
254 JsonValue::Number(number) => number.as_u64(),
255 JsonValue::String(text) => text.trim().parse::<u64>().ok(),
256 _ => None,
257 }
258}
259
260fn json_string_vec_field(value: &JsonValue, key: &str) -> Vec<String> {
261 value
262 .get(key)
263 .and_then(|value| value.as_array())
264 .map(|items| {
265 items
266 .iter()
267 .filter_map(|item| item.as_str())
268 .map(str::trim)
269 .filter(|item| !item.is_empty())
270 .map(ToOwned::to_owned)
271 .collect::<Vec<_>>()
272 })
273 .unwrap_or_default()
274}
275
276fn json_u64_vec_field(value: &JsonValue, key: &str) -> Vec<u64> {
277 value
278 .get(key)
279 .and_then(|value| value.as_array())
280 .map(|items| {
281 items
282 .iter()
283 .filter_map(|item| match item {
284 JsonValue::Number(number) => number.as_u64(),
285 JsonValue::String(text) => text.trim().parse::<u64>().ok(),
286 _ => None,
287 })
288 .collect::<Vec<_>>()
289 })
290 .unwrap_or_default()
291}
292
293fn json_array_field<'a>(value: &'a JsonValue, key: &str) -> &'a [JsonValue] {
294 value
295 .get(key)
296 .and_then(|value| value.as_array())
297 .map(Vec::as_slice)
298 .unwrap_or(&[])
299}
300
301fn derive_control_trace_id(
302 trace_id: Option<&str>,
303 service: Option<&str>,
304 request_id: Option<u64>,
305 payload: &JsonValue,
306) -> Option<String> {
307 trace_id
308 .map(str::trim)
309 .filter(|value| !value.is_empty())
310 .map(ToOwned::to_owned)
311 .or_else(|| json_string_field(payload, "trace_id"))
312 .or_else(|| {
313 let request_id = request_id.or_else(|| json_u64_field(payload, "request_id"))?;
314 let service = service
315 .map(str::to_string)
316 .or_else(|| json_string_field(payload, "service"))
317 .unwrap_or_default();
318 Some(request_trace_id(service.as_str(), request_id))
319 })
320}
321
322fn json_service_tier_field(value: &JsonValue) -> ServiceTierLog {
323 value
324 .get("service_tier")
325 .cloned()
326 .and_then(|value| serde_json::from_value::<ServiceTierLog>(value).ok())
327 .unwrap_or_default()
328}
329
330fn infer_control_trace_detail(
331 kind: &str,
332 event: Option<&str>,
333 payload: &JsonValue,
334) -> Option<ControlTraceDetail> {
335 match kind {
336 "request_completed" => Some(ControlTraceDetail::RequestCompleted {
337 method: json_string_field(payload, "method"),
338 path: json_string_field(payload, "path"),
339 status_code: json_u16_field(payload, "status_code"),
340 duration_ms: json_u64_field(payload, "duration_ms"),
341 station_name: json_string_field(payload, "station_name"),
342 provider_id: json_string_field(payload, "provider_id"),
343 upstream_base_url: json_string_field(payload, "upstream_base_url"),
344 service_tier: json_service_tier_field(payload),
345 }),
346 "retry_trace" => infer_retry_control_trace_detail(event, payload),
347 _ => None,
348 }
349}
350
351fn infer_retry_control_trace_detail(
352 event: Option<&str>,
353 payload: &JsonValue,
354) -> Option<ControlTraceDetail> {
355 let event_name = event
356 .map(str::to_string)
357 .or_else(|| json_string_field(payload, "event"))
358 .unwrap_or_else(|| "retry_trace".to_string());
359
360 match event_name.as_str() {
361 "attempt_select" => Some(ControlTraceDetail::AttemptSelect {
362 station_name: json_nested_string_field(payload, &["compatibility", "station_name"])
363 .or_else(|| json_string_field(payload, "station_name")),
364 upstream_index: json_nested_u64_field(payload, &["compatibility", "upstream_index"])
365 .or_else(|| json_u64_field(payload, "upstream_index")),
366 upstream_base_url: json_string_field(payload, "upstream_base_url"),
367 provider_id: json_string_field(payload, "provider_id"),
368 endpoint_id: json_string_field(payload, "endpoint_id"),
369 provider_endpoint_key: json_string_field(payload, "provider_endpoint_key"),
370 preference_group: json_u64_field(payload, "preference_group"),
371 model: json_string_field(payload, "model"),
372 }),
373 "retry_options" => Some(ControlTraceDetail::RetryOptions {
374 upstream_max_attempts: json_nested_u64_field(payload, &["upstream", "max_attempts"])
375 .and_then(|value| u32::try_from(value).ok()),
376 provider_max_attempts: json_nested_u64_field(payload, &["provider", "max_attempts"])
377 .and_then(|value| u32::try_from(value).ok()),
378 allow_cross_station_before_first_output: json_bool_field(
379 payload,
380 "allow_cross_station_before_first_output",
381 ),
382 }),
383 "lbs_for_request" => Some(ControlTraceDetail::LoadBalancerSelection {
384 mode: json_string_field(payload, "mode"),
385 pinned_source: json_string_field(payload, "pinned_source"),
386 pinned_name: json_string_field(payload, "pinned_name"),
387 selected_station: json_string_field(payload, "selected_station"),
388 selected_stations: json_string_vec_field(payload, "selected_stations"),
389 active_station: json_string_field(payload, "active_station"),
390 note: json_string_field(payload, "note"),
391 }),
392 "provider_runtime_override" => Some(ControlTraceDetail::ProviderRuntimeOverride {
393 provider_name: json_string_field(payload, "provider_name"),
394 endpoint_name: json_string_field(payload, "endpoint_name"),
395 base_urls: json_string_vec_field(payload, "base_urls"),
396 enabled: json_bool_field(payload, "enabled"),
397 clear_enabled: json_bool_field(payload, "clear_enabled").unwrap_or(false),
398 runtime_state: json_string_field(payload, "runtime_state"),
399 clear_runtime_state: json_bool_field(payload, "clear_runtime_state").unwrap_or(false),
400 }),
401 "route_graph_selection_explain" => Some(route_graph_selection_explain_detail(payload)),
402 _ => Some(ControlTraceDetail::RetryEvent {
403 event_name,
404 station_name: json_string_field(payload, "station_name")
405 .or_else(|| json_string_field(payload, "selected_station")),
406 upstream_base_url: json_string_field(payload, "upstream_base_url"),
407 mode: json_string_field(payload, "mode"),
408 note: json_string_field(payload, "note"),
409 }),
410 }
411}
412
413fn route_graph_selection_explain_detail(payload: &JsonValue) -> ControlTraceDetail {
414 ControlTraceDetail::RouteGraphSelectionExplain {
415 request_model: json_string_field(payload, "request_model"),
416 continuity_class: json_nested_string_field(payload, &["continuity", "class"]),
417 provider_failover_allowed: payload
418 .get("continuity")
419 .and_then(|continuity| json_bool_field(continuity, "provider_failover_allowed")),
420 provider_failover_blocked_reason: json_nested_string_field(
421 payload,
422 &["continuity", "provider_failover_blocked_reason"],
423 ),
424 balance_signal_authoritative: payload
425 .get("continuity")
426 .and_then(|continuity| json_bool_field(continuity, "balance_signal_authoritative")),
427 affinity_policy: json_nested_string_field(payload, &["affinity", "policy"]),
428 affinity_source: json_nested_string_field(payload, &["affinity", "source"]),
429 affinity_provider_endpoint_key: json_nested_string_field(
430 payload,
431 &["affinity", "provider_endpoint_key"],
432 ),
433 selected_matches_affinity: payload
434 .get("affinity")
435 .and_then(|affinity| json_bool_field(affinity, "selected_matches_affinity")),
436 selected_provider_id: json_nested_string_field(payload, &["selected", "provider_id"]),
437 selected_endpoint_id: json_nested_string_field(payload, &["selected", "endpoint_id"]),
438 selected_provider_endpoint_key: json_nested_string_field(
439 payload,
440 &["selected", "provider_endpoint_key"],
441 ),
442 selected_preference_group: json_nested_u64_field(
443 payload,
444 &["selected", "preference_group"],
445 ),
446 skipped_higher_priority_groups: json_u64_vec_field(
447 payload,
448 "skipped_higher_priority_groups",
449 ),
450 skipped_higher_priority_candidates: json_array_field(
451 payload,
452 "skipped_higher_priority_candidates",
453 )
454 .iter()
455 .map(|candidate| ControlTraceRouteGraphSkippedCandidate {
456 provider_id: json_string_field(candidate, "provider_id"),
457 endpoint_id: json_string_field(candidate, "endpoint_id"),
458 provider_endpoint_key: json_string_field(candidate, "provider_endpoint_key"),
459 preference_group: json_u64_field(candidate, "preference_group"),
460 route_path: json_string_vec_field(candidate, "route_path"),
461 reasons: json_string_vec_field(candidate, "reasons"),
462 })
463 .collect(),
464 }
465}
466
467pub(super) fn append_control_trace_payload(
468 opt: RequestLogOptions,
469 kind: &'static str,
470 service: Option<&str>,
471 request_id: Option<u64>,
472 event: Option<&str>,
473 ts_ms: u64,
474 payload: JsonValue,
475) {
476 if !control_trace_enabled() {
477 return;
478 }
479 let path = control_trace_path();
480 let entry = make_control_trace_entry(kind, service, request_id, event, ts_ms, payload);
481 if let Ok(line) = serde_json::to_string(&entry) {
482 let _ = append_json_line(&path, opt, &line);
483 }
484}
485
486fn make_control_trace_entry(
487 kind: &'static str,
488 service: Option<&str>,
489 request_id: Option<u64>,
490 event: Option<&str>,
491 ts_ms: u64,
492 payload: JsonValue,
493) -> ControlTraceLogEntry {
494 let detail = infer_control_trace_detail(kind, event, &payload);
495 let trace_id = derive_control_trace_id(None, service, request_id, &payload);
496 ControlTraceLogEntry {
497 ts_ms,
498 kind: kind.to_string(),
499 service: service.map(str::to_string),
500 request_id,
501 trace_id,
502 event: event.map(str::to_string),
503 detail,
504 payload,
505 }
506}
507
508pub fn log_retry_trace(mut event: JsonValue) {
509 let legacy_enabled = retry_trace_enabled();
510 let unified_enabled = control_trace_enabled();
511 if !legacy_enabled && !unified_enabled {
512 return;
513 }
514
515 if let JsonValue::Object(ref mut obj) = event {
516 obj.entry("ts_ms".to_string())
517 .or_insert_with(|| JsonValue::Number(serde_json::Number::from(now_ms())));
518 }
519
520 let ts_ms = json_u64_field(&event, "ts_ms").unwrap_or_else(now_ms);
521 let service = json_string_field(&event, "service");
522 let request_id = json_u64_field(&event, "request_id");
523 let event_name = json_string_field(&event, "event");
524 let opt = request_log_options();
525 let _guard = match log_lock().lock() {
526 Ok(g) => g,
527 Err(e) => e.into_inner(),
528 };
529
530 if legacy_enabled {
531 let path = retry_trace_path();
532 if let Ok(line) = serde_json::to_string(&event) {
533 let _ = append_json_line(&path, opt, &line);
534 }
535 }
536
537 append_control_trace_payload(
538 opt,
539 "retry_trace",
540 service.as_deref(),
541 request_id,
542 event_name.as_deref(),
543 ts_ms,
544 event,
545 );
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 fn temp_control_trace_path(test_name: &str) -> std::path::PathBuf {
553 let path = std::env::temp_dir().join(format!(
554 "codex-helper-control-trace-{test_name}-{}-{}.jsonl",
555 std::process::id(),
556 now_ms()
557 ));
558 let _ = std::fs::remove_file(&path);
559 path
560 }
561
562 #[test]
563 fn json_field_helpers_extract_string_and_numeric_values() {
564 let event = serde_json::json!({
565 "service": "codex",
566 "request_id": "42",
567 "ts_ms": 99,
568 });
569
570 assert_eq!(
571 json_string_field(&event, "service").as_deref(),
572 Some("codex")
573 );
574 assert_eq!(json_u64_field(&event, "request_id"), Some(42));
575 assert_eq!(json_u64_field(&event, "ts_ms"), Some(99));
576 }
577
578 #[test]
579 fn make_control_trace_entry_keeps_kind_event_and_request_id() {
580 let entry = make_control_trace_entry(
581 "retry_trace",
582 Some("codex"),
583 Some(7),
584 Some("attempt_select"),
585 123,
586 serde_json::json!({
587 "event": "attempt_select",
588 "service": "codex",
589 "request_id": 7,
590 "provider_id": "monthly",
591 "endpoint_id": "default",
592 "provider_endpoint_key": "codex/monthly/default",
593 "preference_group": 0,
594 "compatibility": {
595 "station_name": "routing",
596 "upstream_index": 0
597 },
598 }),
599 );
600
601 let value = serde_json::to_value(entry).expect("serialize control trace entry");
602 assert_eq!(value["kind"].as_str(), Some("retry_trace"));
603 assert_eq!(value["event"].as_str(), Some("attempt_select"));
604 assert_eq!(value["request_id"].as_u64(), Some(7));
605 assert_eq!(value["trace_id"].as_str(), Some("codex-7"));
606 assert_eq!(value["service"].as_str(), Some("codex"));
607 assert_eq!(value["payload"]["event"].as_str(), Some("attempt_select"));
608 assert_eq!(value["detail"]["type"].as_str(), Some("attempt_select"));
609 assert_eq!(
610 value["detail"]["provider_endpoint_key"].as_str(),
611 Some("codex/monthly/default")
612 );
613 assert_eq!(value["detail"]["preference_group"].as_u64(), Some(0));
614 assert_eq!(value["detail"]["station_name"].as_str(), Some("routing"));
615 assert_eq!(value["detail"]["upstream_index"].as_u64(), Some(0));
616 }
617
618 #[test]
619 fn hydrate_control_trace_entry_adds_trace_id_to_legacy_event() {
620 let entry: ControlTraceLogEntry = serde_json::from_value(serde_json::json!({
621 "ts_ms": 1,
622 "kind": "retry_trace",
623 "service": "codex",
624 "request_id": 7,
625 "event": "attempt_select",
626 "payload": {
627 "event": "attempt_select",
628 "station_name": "right"
629 }
630 }))
631 .expect("deserialize legacy control trace");
632
633 let entry = hydrate_control_trace_entry(entry);
634
635 assert_eq!(entry.trace_id.as_deref(), Some("codex-7"));
636 assert_eq!(
637 entry.detail,
638 Some(ControlTraceDetail::AttemptSelect {
639 station_name: Some("right".to_string()),
640 upstream_index: None,
641 upstream_base_url: None,
642 provider_id: None,
643 endpoint_id: None,
644 provider_endpoint_key: None,
645 preference_group: None,
646 model: None,
647 })
648 );
649 }
650
651 #[test]
652 fn read_recent_control_trace_repairs_oversized_active_log_before_scan() {
653 let path = temp_control_trace_path("read-repair");
654 std::fs::write(&path, vec![b'x'; 32]).expect("seed oversized control trace");
655
656 let entries =
657 read_recent_control_trace_entries_from_path(&path, 10, LogRetention::new(16, 1))
658 .expect("read repaired control trace");
659
660 assert!(entries.is_empty());
661 assert!(
662 !path.exists(),
663 "oversized active control trace should be rotated away before reading"
664 );
665 assert!(
666 crate::local_log_store::collect_rotated_logs(&path).is_empty(),
667 "oversized rotated control trace should be pruned by retention budget"
668 );
669 let _ = std::fs::remove_file(path);
670 }
671
672 #[test]
673 fn resolved_trace_id_prefers_payload_trace_id() {
674 let entry: ControlTraceLogEntry = serde_json::from_value(serde_json::json!({
675 "ts_ms": 1,
676 "kind": "retry_trace",
677 "service": "codex",
678 "request_id": 7,
679 "payload": {
680 "trace_id": "external-123",
681 "service": "codex",
682 "request_id": 8
683 }
684 }))
685 .expect("deserialize control trace");
686
687 assert_eq!(entry.resolved_trace_id().as_deref(), Some("external-123"));
688 }
689
690 #[test]
691 fn control_trace_entry_resolved_detail_infers_request_completed_from_legacy_payload() {
692 let entry: ControlTraceLogEntry = serde_json::from_value(serde_json::json!({
693 "ts_ms": 1,
694 "kind": "request_completed",
695 "service": "codex",
696 "request_id": 11,
697 "event": "request_completed",
698 "payload": {
699 "method": "POST",
700 "path": "/v1/responses",
701 "status_code": 200,
702 "duration_ms": 321,
703 "station_name": "right",
704 "provider_id": "right",
705 "service_tier": {
706 "effective": "priority"
707 }
708 }
709 }))
710 .expect("deserialize control trace");
711
712 assert_eq!(
713 entry.resolved_detail(),
714 Some(ControlTraceDetail::RequestCompleted {
715 method: Some("POST".to_string()),
716 path: Some("/v1/responses".to_string()),
717 status_code: Some(200),
718 duration_ms: Some(321),
719 station_name: Some("right".to_string()),
720 provider_id: Some("right".to_string()),
721 upstream_base_url: None,
722 service_tier: ServiceTierLog {
723 requested: None,
724 effective: Some("priority".to_string()),
725 actual: None,
726 },
727 })
728 );
729 }
730
731 #[test]
732 fn control_trace_entry_resolved_detail_infers_provider_runtime_override() {
733 let entry: ControlTraceLogEntry = serde_json::from_value(serde_json::json!({
734 "ts_ms": 1,
735 "kind": "retry_trace",
736 "service": "codex",
737 "event": "provider_runtime_override",
738 "payload": {
739 "event": "provider_runtime_override",
740 "provider_name": "alpha",
741 "endpoint_name": "default",
742 "base_urls": ["https://alpha.example/v1"],
743 "enabled": false,
744 "clear_enabled": false,
745 "runtime_state": "breaker_open",
746 "clear_runtime_state": false
747 }
748 }))
749 .expect("deserialize provider runtime trace");
750
751 assert_eq!(
752 entry.resolved_detail(),
753 Some(ControlTraceDetail::ProviderRuntimeOverride {
754 provider_name: Some("alpha".to_string()),
755 endpoint_name: Some("default".to_string()),
756 base_urls: vec!["https://alpha.example/v1".to_string()],
757 enabled: Some(false),
758 clear_enabled: false,
759 runtime_state: Some("breaker_open".to_string()),
760 clear_runtime_state: false,
761 })
762 );
763 }
764
765 #[test]
766 fn control_trace_entry_resolved_detail_infers_route_graph_selection_explain() {
767 let entry: ControlTraceLogEntry = serde_json::from_value(serde_json::json!({
768 "ts_ms": 1,
769 "kind": "retry_trace",
770 "service": "codex",
771 "request_id": 42,
772 "event": "route_graph_selection_explain",
773 "payload": {
774 "event": "route_graph_selection_explain",
775 "service": "codex",
776 "request_id": 42,
777 "request_model": "gpt-5.4",
778 "continuity": {
779 "class": "stateless_or_session_preferred",
780 "provider_failover_allowed": true,
781 "provider_failover_blocked_reason": null,
782 "balance_signal_authoritative": false
783 },
784 "affinity": {
785 "policy": "preferred_group",
786 "provider_endpoint_key": "codex/chili/default",
787 "source": "session_route_affinity",
788 "selected_matches_affinity": false
789 },
790 "selected": {
791 "provider_id": "chili",
792 "endpoint_id": "default",
793 "provider_endpoint_key": "codex/chili/default",
794 "preference_group": 1,
795 "route_path": ["entry", "fallback"]
796 },
797 "skipped_higher_priority_groups": [0],
798 "skipped_higher_priority_candidates": [
799 {
800 "provider_id": "monthly",
801 "endpoint_id": "default",
802 "provider_endpoint_key": "codex/monthly/default",
803 "preference_group": 0,
804 "route_path": ["entry", "monthly"],
805 "reasons": ["usage_exhausted"]
806 }
807 ]
808 }
809 }))
810 .expect("deserialize route graph selection explain trace");
811
812 assert_eq!(
813 entry.resolved_detail(),
814 Some(ControlTraceDetail::RouteGraphSelectionExplain {
815 request_model: Some("gpt-5.4".to_string()),
816 continuity_class: Some("stateless_or_session_preferred".to_string()),
817 provider_failover_allowed: Some(true),
818 provider_failover_blocked_reason: None,
819 balance_signal_authoritative: Some(false),
820 affinity_policy: Some("preferred_group".to_string()),
821 affinity_source: Some("session_route_affinity".to_string()),
822 affinity_provider_endpoint_key: Some("codex/chili/default".to_string()),
823 selected_matches_affinity: Some(false),
824 selected_provider_id: Some("chili".to_string()),
825 selected_endpoint_id: Some("default".to_string()),
826 selected_provider_endpoint_key: Some("codex/chili/default".to_string()),
827 selected_preference_group: Some(1),
828 skipped_higher_priority_groups: vec![0],
829 skipped_higher_priority_candidates: vec![ControlTraceRouteGraphSkippedCandidate {
830 provider_id: Some("monthly".to_string()),
831 endpoint_id: Some("default".to_string()),
832 provider_endpoint_key: Some("codex/monthly/default".to_string()),
833 preference_group: Some(0),
834 route_path: vec!["entry".to_string(), "monthly".to_string()],
835 reasons: vec!["usage_exhausted".to_string()],
836 }],
837 })
838 );
839 }
840}