Skip to main content

codex_helper_core/logging/
control_trace.rs

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}