1use super::*;
2use agent_core::reasoning::ReasoningLevel;
3use serde::Deserialize;
4
5pub const PROVIDER_KEY: &str = "openai-codex";
7pub const PROVIDER_NAME: &str = "OpenAI Codex";
9pub const MODELS_BASE_URL: &str = "https://chatgpt.com/backend-api";
11pub const MODELS_PATH: &str = "/codex/models";
18pub const CODEX_CLIENT_VERSION: &str = "0.153.3";
28
29pub fn codex_static_capability(model_id: &str) -> Option<ReasoningSupport> {
41 use ReasoningLevel::*;
42 let (supported, default_level, multi_agent_version): (
43 &[ReasoningLevel],
44 ReasoningLevel,
45 Option<CodexMultiAgentVersion>,
46 ) = match model_id {
47 "gpt-6-astra" => (
50 &[Low, Medium, High, XHigh, Max, Ultra],
51 Medium,
52 Some(CodexMultiAgentVersion::V2),
53 ),
54 "gpt-5.6-sol" => (
55 &[Low, Medium, High, XHigh, Max, Ultra],
56 Low,
57 Some(CodexMultiAgentVersion::V2),
58 ),
59 "gpt-5.6-terra" => (
60 &[Low, Medium, High, XHigh, Max, Ultra],
61 Medium,
62 Some(CodexMultiAgentVersion::V2),
63 ),
64 "gpt-5.6-luna" => (
65 &[Low, Medium, High, XHigh, Max],
66 Medium,
67 Some(CodexMultiAgentVersion::V1),
68 ),
69 "gpt-5.5" | "gpt-5.4" | "gpt-5.4-mini" => (&[Low, Medium, High, XHigh], Medium, None),
70 "gpt-5.3-codex-spark" => (&[Low, Medium, High, XHigh], High, None),
71 _ => return None,
72 };
73 Some(ReasoningSupport::CodexNamed {
74 supported: supported.to_vec(),
75 multi_agent_reasoning_effort: (model_id == "gpt-6-astra").then_some(XHigh),
76 default_level: Some(default_level),
77 multi_agent_version,
78 })
79}
80
81pub fn codex_static_catalog_models() -> Vec<CatalogModel> {
83 [
84 ("gpt-6-astra", "GPT-6-Astra"),
85 ("gpt-5.6-sol", "GPT-5.6-Sol"),
86 ("gpt-5.6-terra", "GPT-5.6-Terra"),
87 ("gpt-5.6-luna", "GPT-5.6-Luna"),
88 ("gpt-5.5", "GPT-5.5"),
89 ("gpt-5.4", "GPT-5.4"),
90 ("gpt-5.4-mini", "GPT-5.4 Mini"),
91 ("gpt-5.3-codex-spark", "GPT-5.3-Codex-Spark"),
92 ]
93 .into_iter()
94 .filter_map(|(id, label)| {
95 let mut m = CatalogModel::new(PROVIDER_KEY, PROVIDER_NAME, id)?;
96 m.provider_kind = CatalogProviderKind::OpenAiCodex;
97 m.label = Some(label.to_string());
98 m.reasoning = codex_static_capability(id).unwrap_or(ReasoningSupport::Unknown);
99 if matches!(
103 id,
104 "gpt-6-astra"
105 | "gpt-5.6-sol"
106 | "gpt-5.6-terra"
107 | "gpt-5.6-luna"
108 | "gpt-5.5"
109 | "gpt-5.4-mini"
110 ) {
111 m.input_modalities.push(Modality::Image);
112 }
113 m.source = CatalogSource::StaticFallback;
115 Some(m)
116 })
117 .collect()
118}
119
120pub fn codex_models_path(client_version: &str) -> String {
126 let version = if client_version.trim().is_empty() {
127 CODEX_CLIENT_VERSION
128 } else {
129 client_version.trim()
130 };
131 format!("{MODELS_PATH}?client_version={version}")
132}
133
134pub fn codex_models_url(client_version: &str) -> String {
136 format!("{MODELS_BASE_URL}{}", codex_models_path(client_version))
137}
138
139#[derive(Debug, Deserialize)]
142struct CodexModelsResponse {
143 models: Vec<CodexModelItem>,
144}
145
146#[derive(Debug, Deserialize)]
147struct CodexModelItem {
148 slug: String,
149 #[serde(default)]
150 display_name: Option<String>,
151 #[serde(default)]
152 visibility: Option<String>,
153 #[serde(default)]
154 supported_in_api: Option<bool>,
155 #[serde(default)]
156 context_window: Option<u64>,
157 #[serde(default)]
159 input_modalities: Option<Vec<String>>,
160 #[serde(default)]
162 supported_reasoning_levels: Option<Vec<CodexReasoningLevelItem>>,
163 #[serde(default)]
164 default_reasoning_level: Option<String>,
165 #[serde(default)]
166 multi_agent_version: Option<String>,
167 #[serde(default)]
168 multi_agent_reasoning_effort: Option<String>,
169}
170
171#[derive(Debug, Deserialize)]
172struct CodexReasoningLevelItem {
173 effort: String,
174}
175
176pub fn codex_model_is_selectable(
184 visibility: Option<&str>,
185 _supported_in_api: Option<bool>,
186) -> bool {
187 match visibility.map(str::trim).filter(|v| !v.is_empty()) {
188 None => true,
189 Some("list") => true,
190 Some(other) => {
191 other.eq_ignore_ascii_case("list")
194 }
195 }
196}
197
198fn parse_reasoning_levels(items: &[CodexReasoningLevelItem]) -> Vec<ReasoningLevel> {
205 items
206 .iter()
207 .filter_map(|item| ReasoningLevel::parse(&item.effort))
208 .collect()
209}
210
211fn parse_default_level(s: Option<&str>) -> Option<ReasoningLevel> {
213 s.and_then(ReasoningLevel::parse)
214}
215
216fn parse_multi_agent_version(s: Option<&str>) -> Option<CodexMultiAgentVersion> {
217 s.map(|version| match version {
218 "v1" => CodexMultiAgentVersion::V1,
219 "v2" => CodexMultiAgentVersion::V2,
220 _ => CodexMultiAgentVersion::Unknown,
221 })
222}
223
224fn codex_reasoning_support(item: &CodexModelItem) -> ReasoningSupport {
227 let default_level = parse_default_level(item.default_reasoning_level.as_deref());
228 let multi_agent_version = parse_multi_agent_version(item.multi_agent_version.as_deref());
229 let multi_agent_reasoning_effort =
230 parse_default_level(item.multi_agent_reasoning_effort.as_deref());
231
232 if let Some(items) = item.supported_reasoning_levels.as_deref() {
233 return ReasoningSupport::CodexNamed {
234 supported: parse_reasoning_levels(items),
235 multi_agent_reasoning_effort,
236 default_level,
237 multi_agent_version,
238 };
239 }
240 match codex_static_capability(&item.slug) {
244 Some(ReasoningSupport::CodexNamed {
245 supported,
246 default_level: static_default,
247 ..
248 }) => ReasoningSupport::CodexNamed {
249 supported,
250 default_level: default_level.or(static_default),
251 multi_agent_reasoning_effort,
253 multi_agent_version,
254 },
255 Some(other) => other,
256 None => ReasoningSupport::Unknown,
257 }
258}
259
260pub fn parse_codex_catalog_models(body: &str) -> Result<Vec<CatalogModel>, serde_json::Error> {
269 let resp: CodexModelsResponse = serde_json::from_str(body)?;
270 let models: Vec<CatalogModel> = resp
271 .models
272 .into_iter()
273 .filter(|item| codex_model_is_selectable(item.visibility.as_deref(), item.supported_in_api))
274 .filter_map(|item| {
275 let reasoning = codex_reasoning_support(&item);
276 let mut m = CatalogModel::new(PROVIDER_KEY, PROVIDER_NAME, item.slug)?;
277 m.provider_kind = CatalogProviderKind::OpenAiCodex;
278 m.label = item.display_name.filter(|name| !name.trim().is_empty());
279 m.context_tokens = item.context_window;
280 if let Some(modalities) = item.input_modalities {
281 m.input_modalities = modalities.iter().map(|m| Modality::from_str(m)).collect();
284 }
285 m.reasoning = reasoning;
286 m.source = CatalogSource::Live;
287 Some(m)
288 })
289 .collect();
290 Ok(models)
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum CodexWireEffort {
299 Low,
300 Medium,
301 High,
302 XHigh,
303 Max,
304}
305
306impl CodexWireEffort {
307 fn from_level(level: ReasoningLevel) -> Option<Self> {
308 match level {
309 ReasoningLevel::Low => Some(Self::Low),
310 ReasoningLevel::Medium => Some(Self::Medium),
311 ReasoningLevel::High => Some(Self::High),
312 ReasoningLevel::XHigh => Some(Self::XHigh),
313 ReasoningLevel::Max => Some(Self::Max),
314 _ => None,
316 }
317 }
318
319 pub fn as_str(self) -> &'static str {
320 match self {
321 Self::Low => "low",
322 Self::Medium => "medium",
323 Self::High => "high",
324 Self::XHigh => "xhigh",
325 Self::Max => "max",
326 }
327 }
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum CodexExecutionMode {
332 Standard,
333 Max,
334 Ultra,
335}
336
337impl CodexExecutionMode {
338 pub fn as_str(self) -> &'static str {
339 match self {
340 Self::Standard => "standard",
341 Self::Max => "max",
342 Self::Ultra => "ultra",
343 }
344 }
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum CodexMultiAgentMode {
349 ExplicitRequestOnly,
350 Proactive,
351}
352
353impl CodexMultiAgentMode {
354 pub fn as_str(self) -> &'static str {
355 match self {
356 Self::ExplicitRequestOnly => "explicit_request_only",
357 Self::Proactive => "proactive",
358 }
359 }
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
363pub enum ExecutionRole {
364 #[default]
365 Foreground,
366 Worker,
367 Internal,
368}
369
370impl ExecutionRole {
371 pub fn as_str(self) -> &'static str {
372 match self {
373 Self::Foreground => "foreground",
374 Self::Worker => "worker",
375 Self::Internal => "internal",
376 }
377 }
378}
379
380pub type CodexRequestRole = ExecutionRole;
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384pub enum CodexCapabilitySource {
385 Live,
386 Static,
387}
388
389impl CodexCapabilitySource {
390 pub fn as_str(self) -> &'static str {
391 match self {
392 Self::Live => "live",
393 Self::Static => "static",
394 }
395 }
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum CodexPlanErrorCode {
400 InvalidProviderIdentity,
401 CapabilityMetadataMissing,
402 UnsupportedReasoningLevel,
403 UltraRequiresMultiAgentV2,
404}
405
406impl CodexPlanErrorCode {
407 pub fn as_str(self) -> &'static str {
408 match self {
409 Self::InvalidProviderIdentity => "invalid_provider_identity",
410 Self::CapabilityMetadataMissing => "capability_metadata_missing",
411 Self::UnsupportedReasoningLevel => "unsupported_reasoning_level",
412 Self::UltraRequiresMultiAgentV2 => "ultra_requires_multi_agent_v2",
413 }
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub struct CodexPlanError {
419 code: CodexPlanErrorCode,
420 message: String,
421}
422
423impl CodexPlanError {
424 fn new(code: CodexPlanErrorCode, message: impl Into<String>) -> Self {
425 Self {
426 code,
427 message: message.into(),
428 }
429 }
430
431 pub fn code(&self) -> CodexPlanErrorCode {
432 self.code
433 }
434}
435
436impl std::fmt::Display for CodexPlanError {
437 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 formatter.write_str(&self.message)
439 }
440}
441
442impl std::error::Error for CodexPlanError {}
443
444#[derive(Debug, Clone, PartialEq, Eq)]
445pub struct CodexExecutionPlan {
446 pub qualified_model: String,
447 pub selected_level: ReasoningLevel,
448 pub mode: CodexExecutionMode,
449 pub wire_effort: Option<CodexWireEffort>,
450 pub multi_agent_version: Option<CodexMultiAgentVersion>,
451 pub multi_agent_mode: Option<CodexMultiAgentMode>,
452 pub request_role: CodexRequestRole,
453 pub capability_source: Option<CodexCapabilitySource>,
454}
455
456impl CodexExecutionPlan {
457 pub fn automatic_delegation(&self) -> bool {
458 self.multi_agent_mode == Some(CodexMultiAgentMode::Proactive)
459 }
460
461 pub fn wire_effort_label(&self) -> &'static str {
462 self.wire_effort.map_or("omitted", CodexWireEffort::as_str)
463 }
464
465 pub fn multi_agent_version_label(&self) -> &'static str {
466 self.multi_agent_version
467 .map_or("missing", CodexMultiAgentVersion::as_str)
468 }
469
470 pub fn multi_agent_mode_label(&self) -> &'static str {
471 self.multi_agent_mode
472 .map_or("disabled", CodexMultiAgentMode::as_str)
473 }
474}
475
476#[derive(Debug, Clone)]
477struct CodexCapabilitySnapshot {
478 supported: Vec<ReasoningLevel>,
479 multi_agent_reasoning_effort: Option<ReasoningLevel>,
480 multi_agent_version: Option<CodexMultiAgentVersion>,
481 source: CodexCapabilitySource,
482}
483
484fn snapshot_from_model(
485 model: &CatalogModel,
486 source: CodexCapabilitySource,
487) -> Option<CodexCapabilitySnapshot> {
488 match &model.reasoning {
489 ReasoningSupport::CodexNamed {
490 supported,
491 multi_agent_reasoning_effort,
492 multi_agent_version,
493 ..
494 } => Some(CodexCapabilitySnapshot {
495 supported: supported.clone(),
496 multi_agent_reasoning_effort: *multi_agent_reasoning_effort,
497 multi_agent_version: *multi_agent_version,
498 source,
499 }),
500 _ => None,
501 }
502}
503
504fn authoritative_source(
505 model: &CatalogModel,
506 qualified_model: &str,
507) -> Result<CodexCapabilitySource, CodexPlanError> {
508 match model.source {
509 CatalogSource::Live => Ok(CodexCapabilitySource::Live),
510 CatalogSource::StaticFallback => Ok(CodexCapabilitySource::Static),
511 CatalogSource::StaticWithLive | CatalogSource::Inferred => Err(CodexPlanError::new(
512 CodexPlanErrorCode::CapabilityMetadataMissing,
513 format!(
514 "authoritative exact-model capability metadata is unavailable for {qualified_model}"
515 ),
516 )),
517 }
518}
519
520fn authoritative_capability(
521 qualified_model: &str,
522 model_id: &str,
523 catalog_model: Option<&CatalogModel>,
524) -> Result<Option<CodexCapabilitySnapshot>, CodexPlanError> {
525 if let Some(model) = catalog_model {
526 if model.runtime_id() != qualified_model
527 || model.provider_kind != CatalogProviderKind::OpenAiCodex
528 {
529 return Err(CodexPlanError::new(
530 CodexPlanErrorCode::InvalidProviderIdentity,
531 format!("catalog identity does not match exact Codex model {qualified_model}"),
532 ));
533 }
534 let source = authoritative_source(model, qualified_model)?;
535 return Ok(snapshot_from_model(model, source));
536 }
537
538 if let Some(model) = super::capability_cache::get(qualified_model) {
541 if model.runtime_id() != qualified_model
542 || model.provider_kind != CatalogProviderKind::OpenAiCodex
543 {
544 return Err(CodexPlanError::new(
545 CodexPlanErrorCode::InvalidProviderIdentity,
546 format!(
547 "cached catalog identity does not match exact Codex model {qualified_model}"
548 ),
549 ));
550 }
551 let source = authoritative_source(&model, qualified_model)?;
552 return Ok(snapshot_from_model(&model, source));
553 }
554
555 Ok(match codex_static_capability(model_id) {
556 Some(ReasoningSupport::CodexNamed {
557 supported,
558 multi_agent_reasoning_effort,
559 multi_agent_version,
560 ..
561 }) => Some(CodexCapabilitySnapshot {
562 supported,
563 multi_agent_reasoning_effort,
564 multi_agent_version,
565 source: CodexCapabilitySource::Static,
566 }),
567 _ => None,
568 })
569}
570
571pub fn plan_codex_execution(
574 qualified_model: &str,
575 selected_level: ReasoningLevel,
576 request_role: CodexRequestRole,
577 catalog_model: Option<&CatalogModel>,
578) -> Result<CodexExecutionPlan, CodexPlanError> {
579 let Some(model_id) = qualified_model.strip_prefix("openai-codex/") else {
580 return Err(CodexPlanError::new(
581 CodexPlanErrorCode::InvalidProviderIdentity,
582 format!("{qualified_model} is not an exact openai-codex identity"),
583 ));
584 };
585 if model_id.is_empty() || model_id.contains('/') {
586 return Err(CodexPlanError::new(
587 CodexPlanErrorCode::InvalidProviderIdentity,
588 format!("{qualified_model} is not an exact openai-codex identity"),
589 ));
590 }
591
592 let capability = authoritative_capability(qualified_model, model_id, catalog_model)?;
593 if !matches!(
594 selected_level,
595 ReasoningLevel::Off | ReasoningLevel::Adaptive
596 ) {
597 let Some(capability) = capability.as_ref() else {
598 return Err(CodexPlanError::new(
599 CodexPlanErrorCode::CapabilityMetadataMissing,
600 format!(
601 "no capability metadata for {qualified_model}; cannot authorize level '{selected_level}'"
602 ),
603 ));
604 };
605 if !capability.supported.contains(&selected_level) {
606 return Err(CodexPlanError::new(
607 CodexPlanErrorCode::UnsupportedReasoningLevel,
608 format!(
609 "reasoning level '{selected_level}' is not supported by {qualified_model}; supported: [{}]",
610 capability
611 .supported
612 .iter()
613 .map(|level| level.as_str())
614 .collect::<Vec<_>>()
615 .join(", ")
616 ),
617 ));
618 }
619 }
620
621 let multi_agent_version = capability
622 .as_ref()
623 .and_then(|capability| capability.multi_agent_version);
624 if selected_level == ReasoningLevel::Ultra
625 && multi_agent_version != Some(CodexMultiAgentVersion::V2)
626 {
627 return Err(CodexPlanError::new(
628 CodexPlanErrorCode::UltraRequiresMultiAgentV2,
629 format!(
630 "reasoning level 'ultra' requires multi-agent v2 capability for {qualified_model}"
631 ),
632 ));
633 }
634
635 let mode = match selected_level {
636 ReasoningLevel::Max => CodexExecutionMode::Max,
637 ReasoningLevel::Ultra => CodexExecutionMode::Ultra,
638 _ => CodexExecutionMode::Standard,
639 };
640 let wire_effort = if selected_level == ReasoningLevel::Ultra {
641 let cap = capability
646 .as_ref()
647 .expect("Ultra capability validated above");
648 Some(
649 cap.multi_agent_reasoning_effort
650 .filter(|level| cap.supported.contains(level))
651 .and_then(CodexWireEffort::from_level)
652 .or_else(|| {
653 cap.supported
654 .contains(&ReasoningLevel::Max)
655 .then_some(CodexWireEffort::Max)
656 })
657 .or_else(|| {
658 cap.supported
659 .iter()
660 .rev()
661 .find_map(|level| CodexWireEffort::from_level(*level))
662 })
663 .ok_or_else(|| {
664 CodexPlanError::new(
665 CodexPlanErrorCode::CapabilityMetadataMissing,
666 format!("no supported wire effort for Ultra on {qualified_model}"),
667 )
668 })?,
669 )
670 } else {
671 CodexWireEffort::from_level(selected_level)
672 };
673 let multi_agent_mode = if request_role == CodexRequestRole::Foreground
674 && multi_agent_version == Some(CodexMultiAgentVersion::V2)
675 {
676 Some(if selected_level == ReasoningLevel::Ultra {
677 CodexMultiAgentMode::Proactive
678 } else {
679 CodexMultiAgentMode::ExplicitRequestOnly
680 })
681 } else {
682 None
683 };
684
685 Ok(CodexExecutionPlan {
686 qualified_model: qualified_model.to_string(),
687 selected_level,
688 mode,
689 wire_effort,
690 multi_agent_version,
691 multi_agent_mode,
692 request_role,
693 capability_source: capability.map(|capability| capability.source),
694 })
695}
696
697pub fn validate_codex_level(
706 model_id: &str,
707 level: ReasoningLevel,
708 catalog_model: Option<&CatalogModel>,
709) -> Result<(), String> {
710 let qualified = format!("openai-codex/{model_id}");
711 plan_codex_execution(
712 &qualified,
713 level,
714 CodexRequestRole::Foreground,
715 catalog_model,
716 )
717 .map(|_| ())
718 .map_err(|error| error.to_string())
719}
720
721#[cfg(test)]
724mod tests {
725 use super::*;
726
727 const FIXTURE: &str = include_str!("fixtures/openai_codex_models.json");
728
729 #[test]
730 fn input_modalities_use_only_exact_observed_static_and_fixture_rows() {
731 for models in [
732 codex_static_catalog_models(),
733 parse_codex_catalog_models(FIXTURE).unwrap(),
734 ] {
735 for model in models {
736 let expected = match model.id.as_str() {
737 "gpt-6-astra" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-5.6-luna"
738 | "gpt-5.5" | "gpt-5.4-mini" => vec![Modality::Text, Modality::Image],
739 "gpt-5.4" | "gpt-5.3-codex-spark" => vec![Modality::Text],
741 _ => panic!("unexpected selectable fixture row"),
742 };
743 assert_eq!(model.input_modalities, expected, "{}", model.id);
744 assert!(!model.input_modalities.contains(&Modality::File));
745 }
746 }
747 }
748
749 #[test]
750 fn live_input_modalities_never_backfill_static_images() {
751 for (field, expected) in [
752 ("", vec![Modality::Text]),
753 (r#", "input_modalities": null"#, vec![Modality::Text]),
754 (r#", "input_modalities": ["text"]"#, vec![Modality::Text]),
755 (r#", "input_modalities": []"#, vec![]),
756 (r#", "input_modalities": ["image"]"#, vec![Modality::Image]),
757 (
758 r#", "input_modalities": ["text", "file"]"#,
759 vec![Modality::Text, Modality::File],
760 ),
761 (
762 r#", "input_modalities": ["Image", "image/png", "future"]"#,
763 vec![
764 Modality::Other("Image".into()),
765 Modality::Other("image/png".into()),
766 Modality::Other("future".into()),
767 ],
768 ),
769 ] {
770 let body = format!(r#"{{"models":[{{"slug":"gpt-6-astra"{field}}}]}}"#);
771 let models = parse_codex_catalog_models(&body).unwrap();
772 assert_eq!(models[0].input_modalities, expected);
773 assert_eq!(models[0].source, CatalogSource::Live);
774 }
775 }
776
777 #[test]
778 fn malformed_input_modalities_reject_catalog_instead_of_authorizing_static() {
779 for value in [
780 r#""image""#,
781 "true",
782 "{}",
783 "[null]",
784 "[1]",
785 r#"["text", {}]"#,
786 ] {
787 let body =
788 format!(r#"{{"models":[{{"slug":"gpt-6-astra","input_modalities":{value}}}]}}"#);
789 assert!(parse_codex_catalog_models(&body).is_err());
790 }
791 }
792
793 fn ultra_test_row(
794 id: &str,
795 levels: &[&str],
796 override_value: serde_json::Value,
797 ) -> CatalogModel {
798 let body = serde_json::json!({"models": [{
799 "slug": id, "visibility": "list", "multi_agent_version": "v2",
800 "supported_reasoning_levels": levels.iter().map(|level| serde_json::json!({"effort": level})).collect::<Vec<_>>(),
801 "multi_agent_reasoning_effort": override_value,
802 }]}).to_string();
803 parse_codex_catalog_models(&body).unwrap().remove(0)
804 }
805
806 #[test]
807 fn ultra_uses_supported_override_for_foreground_worker_and_internal() {
808 for (value, wire) in [
809 ("low", CodexWireEffort::Low),
810 ("medium", CodexWireEffort::Medium),
811 ("high", CodexWireEffort::High),
812 ("xhigh", CodexWireEffort::XHigh),
813 ("max", CodexWireEffort::Max),
814 ] {
815 let row = ultra_test_row(
816 "gpt-override-matrix",
817 &["low", "medium", "high", "xhigh", "max", "ultra"],
818 value.into(),
819 );
820 for role in [
821 CodexRequestRole::Foreground,
822 CodexRequestRole::Worker,
823 CodexRequestRole::Internal,
824 ] {
825 let plan = plan_codex_execution(
826 &row.runtime_id(),
827 ReasoningLevel::Ultra,
828 role,
829 Some(&row),
830 )
831 .unwrap();
832 assert_eq!(plan.selected_level, ReasoningLevel::Ultra);
833 assert_eq!(plan.wire_effort, Some(wire));
834 assert_eq!(
835 plan.automatic_delegation(),
836 role == CodexRequestRole::Foreground
837 );
838 }
839 }
840 }
841
842 #[test]
843 fn ultra_invalid_or_absent_override_uses_supported_fallback_only() {
844 for value in [
845 serde_json::Value::Null,
846 "future".into(),
847 "ultra".into(),
848 "adaptive".into(),
849 "off".into(),
850 "ultracode".into(),
851 "high".into(),
852 ] {
853 for (levels, expected) in [
854 (vec!["low", "max", "xhigh", "ultra"], CodexWireEffort::Max),
855 (vec!["low", "xhigh", "ultra"], CodexWireEffort::XHigh),
856 (vec!["low", "ultra"], CodexWireEffort::Low),
857 ] {
858 let row = ultra_test_row("gpt-fallback-matrix", &levels, value.clone());
859 let plan = plan_codex_execution(
860 &row.runtime_id(),
861 ReasoningLevel::Ultra,
862 CodexRequestRole::Foreground,
863 Some(&row),
864 )
865 .unwrap();
866 assert_eq!(
867 plan.wire_effort,
868 Some(expected),
869 "override={value} levels={levels:?}"
870 );
871 }
872 }
873 let row = ultra_test_row("gpt-no-wire-effort", &["ultra"], "xhigh".into());
874 assert_eq!(
875 plan_codex_execution(
876 &row.runtime_id(),
877 ReasoningLevel::Ultra,
878 CodexRequestRole::Foreground,
879 Some(&row)
880 )
881 .unwrap_err()
882 .code(),
883 CodexPlanErrorCode::CapabilityMetadataMissing
884 );
885 }
886
887 #[test]
888 fn astra_live_absence_never_borrows_static_xhigh_override() {
889 for levels in [
890 Some(serde_json::json!([{"effort":"max"},{"effort":"ultra"}])),
891 None,
892 ] {
893 for value in [None, Some(serde_json::Value::Null), Some("future".into())] {
894 let mut body = serde_json::json!({"models":[{"slug":"gpt-6-astra", "multi_agent_version":"v2"}]});
895 if let Some(levels) = &levels {
896 body["models"][0]["supported_reasoning_levels"] = levels.clone();
897 }
898 if let Some(value) = value {
899 body["models"][0]["multi_agent_reasoning_effort"] = value;
900 }
901 let row = parse_codex_catalog_models(&body.to_string())
902 .unwrap()
903 .remove(0);
904 let plan = plan_codex_execution(
905 &row.runtime_id(),
906 ReasoningLevel::Ultra,
907 CodexRequestRole::Foreground,
908 Some(&row),
909 )
910 .unwrap();
911 assert_eq!(plan.wire_effort, Some(CodexWireEffort::Max));
912 assert_eq!(plan.capability_source, Some(CodexCapabilitySource::Live));
913 }
914 }
915 }
916
917 #[test]
918 fn ultra_override_survives_cache_and_refresh_removal() {
919 let id = "gpt-ultra-cache-override-removal";
921 let row = ultra_test_row(id, &["high", "max", "ultra"], "high".into());
922 let qualified = row.runtime_id();
923 super::super::capability_cache::insert(row);
924 assert_eq!(
925 plan_codex_execution(
926 &qualified,
927 ReasoningLevel::Ultra,
928 CodexRequestRole::Worker,
929 None
930 )
931 .unwrap()
932 .wire_effort,
933 Some(CodexWireEffort::High)
934 );
935 let row = ultra_test_row(id, &["high", "max", "ultra"], serde_json::Value::Null);
936 super::super::capability_cache::insert(row);
937 assert_eq!(
938 plan_codex_execution(
939 &qualified,
940 ReasoningLevel::Ultra,
941 CodexRequestRole::Worker,
942 None
943 )
944 .unwrap()
945 .wire_effort,
946 Some(CodexWireEffort::Max)
947 );
948 }
949
950 #[test]
951 fn astra_override_does_not_change_explicit_levels_or_omission_modes() {
952 let row = parse_codex_catalog_models(FIXTURE).unwrap().remove(0);
953 for (level, wire) in [
954 (ReasoningLevel::Max, Some(CodexWireEffort::Max)),
955 (ReasoningLevel::XHigh, Some(CodexWireEffort::XHigh)),
956 (ReasoningLevel::Medium, Some(CodexWireEffort::Medium)),
957 (ReasoningLevel::Off, None),
958 (ReasoningLevel::Adaptive, None),
959 ] {
960 let plan = plan_codex_execution(
961 &row.runtime_id(),
962 level,
963 CodexRequestRole::Foreground,
964 Some(&row),
965 )
966 .unwrap();
967 assert_eq!(plan.wire_effort, wire);
968 assert!(!plan.automatic_delegation());
969 }
970 assert!(validate_codex_level("gpt-6-astra-wm", ReasoningLevel::Ultra, None).is_err());
971 }
972
973 #[test]
974 fn ultra_override_does_not_grant_missing_v2() {
975 for version in [None, Some("v1"), Some("future")] {
976 let mut body = serde_json::json!({"models":[{"slug":"gpt-6-astra", "multi_agent_reasoning_effort":"xhigh"}]});
977 if let Some(version) = version {
978 body["models"][0]["multi_agent_version"] = version.into();
979 }
980 let row = parse_codex_catalog_models(&body.to_string())
981 .unwrap()
982 .remove(0);
983 assert_eq!(
984 plan_codex_execution(
985 &row.runtime_id(),
986 ReasoningLevel::Ultra,
987 CodexRequestRole::Foreground,
988 Some(&row)
989 )
990 .unwrap_err()
991 .code(),
992 CodexPlanErrorCode::UltraRequiresMultiAgentV2
993 );
994 }
995 }
996
997 #[test]
998 fn parse_fixture_matches_list_visible_chatgpt_picker_models() {
999 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1000 let ids: Vec<_> = models.iter().map(|m| m.id.as_str()).collect();
1001 assert_eq!(
1002 ids,
1003 vec![
1004 "gpt-6-astra",
1005 "gpt-5.6-sol",
1006 "gpt-5.6-terra",
1007 "gpt-5.6-luna",
1008 "gpt-5.5",
1009 "gpt-5.4",
1010 "gpt-5.4-mini",
1011 "gpt-5.3-codex-spark",
1012 ]
1013 );
1014 assert!(ids.contains(&"gpt-5.3-codex-spark"));
1017 assert!(!ids.contains(&"codex-auto-review"));
1019 assert!(!ids.contains(&"codex-internal-eval"));
1020 assert!(models
1021 .iter()
1022 .all(|m| m.runtime_id().starts_with("openai-codex/")));
1023 assert!(models.iter().all(|m| m.source == CatalogSource::Live));
1024 assert!(models
1025 .iter()
1026 .all(|m| m.provider_kind == CatalogProviderKind::OpenAiCodex));
1027 }
1028
1029 #[test]
1030 fn parse_fixture_reads_display_name_and_context_window() {
1031 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1032 let gpt55 = models.iter().find(|m| m.id == "gpt-5.5").unwrap();
1033 assert_eq!(gpt55.label.as_deref(), Some("GPT-5.5"));
1034 assert_eq!(gpt55.context_tokens, Some(272_000));
1035 }
1036
1037 #[test]
1038 fn execution_plan_fixture_preserves_multi_agent_versions() {
1039 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1040 let version = |id: &str| {
1041 models
1042 .iter()
1043 .find(|model| model.id == id)
1044 .and_then(CatalogModel::codex_multi_agent_version)
1045 };
1046 assert_eq!(version("gpt-6-astra"), Some(CodexMultiAgentVersion::V2));
1047 assert_eq!(version("gpt-5.6-sol"), Some(CodexMultiAgentVersion::V2));
1048 assert_eq!(version("gpt-5.6-terra"), Some(CodexMultiAgentVersion::V2));
1049 assert_eq!(version("gpt-5.6-luna"), Some(CodexMultiAgentVersion::V1));
1050 assert_eq!(version("gpt-5.5"), None);
1051 }
1052
1053 #[test]
1054 fn execution_plan_maps_max_ultra_and_xhigh_exactly() {
1055 let max = plan_codex_execution(
1056 "openai-codex/gpt-5.6-sol",
1057 ReasoningLevel::Max,
1058 CodexRequestRole::Foreground,
1059 None,
1060 )
1061 .expect("Sol Max plan");
1062 assert_eq!(max.mode, CodexExecutionMode::Max);
1063 assert_eq!(max.wire_effort, Some(CodexWireEffort::Max));
1064 assert_eq!(
1065 max.multi_agent_mode,
1066 Some(CodexMultiAgentMode::ExplicitRequestOnly)
1067 );
1068
1069 let ultra = plan_codex_execution(
1070 "openai-codex/gpt-5.6-sol",
1071 ReasoningLevel::Ultra,
1072 CodexRequestRole::Foreground,
1073 None,
1074 )
1075 .expect("Sol Ultra plan");
1076 assert_eq!(ultra.mode, CodexExecutionMode::Ultra);
1077 assert_eq!(ultra.wire_effort, Some(CodexWireEffort::Max));
1078 assert_eq!(ultra.multi_agent_mode, Some(CodexMultiAgentMode::Proactive));
1079
1080 let xhigh = plan_codex_execution(
1081 "openai-codex/gpt-5.6-sol",
1082 ReasoningLevel::XHigh,
1083 CodexRequestRole::Foreground,
1084 None,
1085 )
1086 .expect("Sol XHigh plan");
1087 assert_eq!(xhigh.mode, CodexExecutionMode::Standard);
1088 assert_eq!(xhigh.wire_effort, Some(CodexWireEffort::XHigh));
1089 assert_eq!(
1090 xhigh.multi_agent_mode,
1091 Some(CodexMultiAgentMode::ExplicitRequestOnly)
1092 );
1093 }
1094
1095 #[test]
1096 fn execution_plan_luna_max_uses_max_without_v2_context() {
1097 let plan = plan_codex_execution(
1098 "openai-codex/gpt-5.6-luna",
1099 ReasoningLevel::Max,
1100 CodexRequestRole::Foreground,
1101 None,
1102 )
1103 .expect("Luna Max plan");
1104 assert_eq!(plan.wire_effort, Some(CodexWireEffort::Max));
1105 assert_eq!(plan.multi_agent_version, Some(CodexMultiAgentVersion::V1));
1106 assert_eq!(plan.multi_agent_mode, None);
1107 }
1108
1109 #[test]
1110 fn execution_plan_worker_ultra_never_enables_proactive_mode() {
1111 let plan = plan_codex_execution(
1112 "openai-codex/gpt-5.6-sol",
1113 ReasoningLevel::Ultra,
1114 CodexRequestRole::Worker,
1115 None,
1116 )
1117 .expect("worker Ultra plan");
1118 assert_eq!(plan.wire_effort, Some(CodexWireEffort::Max));
1119 assert_eq!(plan.multi_agent_mode, None);
1120 assert!(!plan.automatic_delegation());
1121 }
1122
1123 #[test]
1124 fn execution_plan_ultra_requires_live_v2_without_static_backfill() {
1125 for (slug, version_field) in [
1126 ("gpt-ultra-live-missing", ""),
1127 ("gpt-ultra-live-v1", r#", "multi_agent_version": "v1""#),
1128 ("gpt-ultra-live-unknown", r#", "multi_agent_version": "v3""#),
1129 ] {
1130 let body = format!(
1131 r#"{{"models":[{{
1132 "slug":"{slug}",
1133 "visibility":"list",
1134 "supported_reasoning_levels":[{{"effort":"max"}},{{"effort":"ultra"}}]
1135 {version_field}
1136 }}]}}"#
1137 );
1138 let models = parse_codex_catalog_models(&body).expect("parse live row");
1139 let error = plan_codex_execution(
1140 &format!("openai-codex/{slug}"),
1141 ReasoningLevel::Ultra,
1142 CodexRequestRole::Foreground,
1143 Some(&models[0]),
1144 )
1145 .expect_err("live missing/v1/unknown version must deny Ultra");
1146 assert_eq!(error.code(), CodexPlanErrorCode::UltraRequiresMultiAgentV2);
1147 }
1148 }
1149
1150 #[test]
1151 fn execution_plan_rejects_wrong_provider_identity() {
1152 let error = plan_codex_execution(
1153 "openrouter/gpt-5.6-sol",
1154 ReasoningLevel::Ultra,
1155 CodexRequestRole::Foreground,
1156 None,
1157 )
1158 .expect_err("wrong provider must fail closed");
1159 assert_eq!(error.code(), CodexPlanErrorCode::InvalidProviderIdentity);
1160 }
1161
1162 #[test]
1163 fn execution_plan_rejects_wrong_provider_kind_in_cache() {
1164 let id = "gpt-cached-provider-kind-spoof";
1165 let qualified = format!("openai-codex/{id}");
1166 let mut spoofed = CatalogModel::new(PROVIDER_KEY, PROVIDER_NAME, id).unwrap();
1167 spoofed.reasoning = ReasoningSupport::CodexNamed {
1168 multi_agent_reasoning_effort: None,
1169 supported: vec![ReasoningLevel::Max, ReasoningLevel::Ultra],
1170 default_level: Some(ReasoningLevel::Max),
1171 multi_agent_version: Some(CodexMultiAgentVersion::V2),
1172 };
1173 super::super::capability_cache::insert(spoofed);
1174
1175 let error = plan_codex_execution(
1176 &qualified,
1177 ReasoningLevel::Ultra,
1178 CodexRequestRole::Foreground,
1179 None,
1180 )
1181 .expect_err("cache rows must preserve the exact Codex provider kind");
1182 assert_eq!(error.code(), CodexPlanErrorCode::InvalidProviderIdentity);
1183 }
1184
1185 #[test]
1186 fn execution_plan_rejects_inferred_capability_provenance() {
1187 fn inferred_model(id: &str) -> CatalogModel {
1188 let mut model = CatalogModel::new(PROVIDER_KEY, PROVIDER_NAME, id).unwrap();
1189 model.provider_kind = CatalogProviderKind::OpenAiCodex;
1190 model.source = CatalogSource::Inferred;
1191 model.reasoning = ReasoningSupport::CodexNamed {
1192 multi_agent_reasoning_effort: None,
1193 supported: vec![ReasoningLevel::Max, ReasoningLevel::Ultra],
1194 default_level: Some(ReasoningLevel::Max),
1195 multi_agent_version: Some(CodexMultiAgentVersion::V2),
1196 };
1197 model
1198 }
1199
1200 let explicit_id = "gpt-inferred-explicit-provenance";
1201 let explicit = inferred_model(explicit_id);
1202 let explicit_error = plan_codex_execution(
1203 &format!("openai-codex/{explicit_id}"),
1204 ReasoningLevel::Ultra,
1205 CodexRequestRole::Foreground,
1206 Some(&explicit),
1207 )
1208 .expect_err("inferred explicit metadata must never authorize Ultra");
1209 assert_eq!(
1210 explicit_error.code(),
1211 CodexPlanErrorCode::CapabilityMetadataMissing
1212 );
1213
1214 let cached_id = "gpt-inferred-cached-provenance";
1215 super::super::capability_cache::insert(inferred_model(cached_id));
1216 let cached_error = plan_codex_execution(
1217 &format!("openai-codex/{cached_id}"),
1218 ReasoningLevel::Ultra,
1219 CodexRequestRole::Foreground,
1220 None,
1221 )
1222 .expect_err("inferred cached metadata must never authorize Ultra");
1223 assert_eq!(
1224 cached_error.code(),
1225 CodexPlanErrorCode::CapabilityMetadataMissing
1226 );
1227 }
1228
1229 #[test]
1232 fn parse_fixture_astra_is_first_with_full_ladder_and_v2() {
1233 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1234 let astra = &models[0];
1235 assert_eq!(
1236 astra.id, "gpt-6-astra",
1237 "astra is priority 1 in the live catalog"
1238 );
1239 assert_eq!(astra.label.as_deref(), Some("GPT-6-Astra"));
1240 assert_eq!(astra.context_tokens, Some(272_000));
1241 for level in [
1242 ReasoningLevel::Low,
1243 ReasoningLevel::Medium,
1244 ReasoningLevel::High,
1245 ReasoningLevel::XHigh,
1246 ReasoningLevel::Max,
1247 ReasoningLevel::Ultra,
1248 ] {
1249 assert!(
1250 astra.codex_supports_level(level),
1251 "astra must support {level}"
1252 );
1253 }
1254 assert_eq!(
1255 astra.codex_multi_agent_version(),
1256 Some(CodexMultiAgentVersion::V2)
1257 );
1258 assert_eq!(astra.runtime_id(), "openai-codex/gpt-6-astra");
1259 }
1260
1261 #[test]
1262 fn parse_fixture_sol_has_ultra() {
1263 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1264 let sol = models.iter().find(|m| m.id == "gpt-5.6-sol").unwrap();
1265 assert!(
1266 sol.codex_supports_level(ReasoningLevel::Ultra),
1267 "sol must support ultra"
1268 );
1269 assert!(sol.codex_supports_level(ReasoningLevel::Max));
1270 assert!(sol.codex_supports_level(ReasoningLevel::XHigh));
1271 }
1272
1273 #[test]
1274 fn parse_fixture_terra_has_ultra() {
1275 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1276 let terra = models.iter().find(|m| m.id == "gpt-5.6-terra").unwrap();
1277 assert!(terra.codex_supports_level(ReasoningLevel::Ultra));
1278 assert!(terra.codex_supports_level(ReasoningLevel::Max));
1279 }
1280
1281 #[test]
1282 fn parse_fixture_luna_has_max_not_ultra() {
1283 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1284 let luna = models.iter().find(|m| m.id == "gpt-5.6-luna").unwrap();
1285 assert!(luna.codex_supports_level(ReasoningLevel::Max));
1286 assert!(
1287 !luna.codex_supports_level(ReasoningLevel::Ultra),
1288 "luna must NOT support ultra"
1289 );
1290 }
1291
1292 #[test]
1293 fn parse_fixture_gpt55_has_xhigh_not_max_or_ultra() {
1294 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1295 let m = models.iter().find(|m| m.id == "gpt-5.5").unwrap();
1296 assert!(m.codex_supports_level(ReasoningLevel::XHigh));
1297 assert!(
1298 !m.codex_supports_level(ReasoningLevel::Max),
1299 "gpt-5.5 must NOT support max"
1300 );
1301 assert!(
1302 !m.codex_supports_level(ReasoningLevel::Ultra),
1303 "gpt-5.5 must NOT support ultra"
1304 );
1305 }
1306
1307 #[test]
1308 fn parse_fixture_spark_has_xhigh_not_max_or_ultra() {
1309 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1310 let spark = models
1311 .iter()
1312 .find(|m| m.id == "gpt-5.3-codex-spark")
1313 .unwrap();
1314 assert!(spark.codex_supports_level(ReasoningLevel::XHigh));
1315 assert!(!spark.codex_supports_level(ReasoningLevel::Max));
1316 assert!(!spark.codex_supports_level(ReasoningLevel::Ultra));
1317 }
1318
1319 #[test]
1320 fn parse_fixture_default_levels_match_observed_cache() {
1321 let models = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1322 let get = |id: &str| {
1323 models
1324 .iter()
1325 .find(|m| m.id == id)
1326 .unwrap_or_else(|| panic!("missing {id}"))
1327 };
1328 let default_of = |id: &str| match &get(id).reasoning {
1329 ReasoningSupport::CodexNamed { default_level, .. } => *default_level,
1330 other => panic!("{id}: expected CodexNamed, got {other:?}"),
1331 };
1332 assert_eq!(
1333 default_of("gpt-6-astra"),
1334 Some(ReasoningLevel::Medium),
1335 "astra"
1336 );
1337 assert_eq!(default_of("gpt-5.6-sol"), Some(ReasoningLevel::Low), "sol");
1338 assert_eq!(
1339 default_of("gpt-5.6-terra"),
1340 Some(ReasoningLevel::Medium),
1341 "terra"
1342 );
1343 assert_eq!(
1344 default_of("gpt-5.6-luna"),
1345 Some(ReasoningLevel::Medium),
1346 "luna"
1347 );
1348 assert_eq!(default_of("gpt-5.5"), Some(ReasoningLevel::Medium), "5.5");
1349 assert_eq!(default_of("gpt-5.4"), Some(ReasoningLevel::Medium), "5.4");
1350 assert_eq!(
1351 default_of("gpt-5.4-mini"),
1352 Some(ReasoningLevel::Medium),
1353 "5.4-mini"
1354 );
1355 assert_eq!(
1356 default_of("gpt-5.3-codex-spark"),
1357 Some(ReasoningLevel::High),
1358 "spark"
1359 );
1360 }
1361
1362 #[test]
1363 fn unknown_effort_strings_are_ignored_not_coerced() {
1364 let body = r#"{"models":[{
1365 "slug": "gpt-test",
1366 "visibility": "list",
1367 "supported_reasoning_levels": [
1368 {"effort": "low"},
1369 {"effort": "hyper-ultra-experimental"},
1370 {"effort": "high"}
1371 ]
1372 }]}"#;
1373 let models = parse_codex_catalog_models(body).unwrap();
1374 let m = &models[0];
1375 assert!(m.codex_supports_level(ReasoningLevel::Low));
1377 assert!(m.codex_supports_level(ReasoningLevel::High));
1378 assert!(!m.codex_supports_level(ReasoningLevel::Ultra));
1379 assert!(!m.codex_supports_level(ReasoningLevel::Max));
1380 }
1381
1382 #[test]
1383 fn live_known_model_with_only_unknown_efforts_does_not_borrow_static_ultra() {
1384 let item: CodexModelItem = serde_json::from_str(
1385 r#"{
1386 "slug": "gpt-5.6-sol",
1387 "visibility": "list",
1388 "supported_reasoning_levels": [{"effort": "future-effort"}],
1389 "multi_agent_version": "v2"
1390 }"#,
1391 )
1392 .expect("parse live Sol row");
1393 let mut model = CatalogModel::new(PROVIDER_KEY, PROVIDER_NAME, &item.slug).unwrap();
1394 model.provider_kind = CatalogProviderKind::OpenAiCodex;
1395 model.source = CatalogSource::Live;
1396 model.reasoning = codex_reasoning_support(&item);
1397
1398 let error = plan_codex_execution(
1399 "openai-codex/gpt-5.6-sol",
1400 ReasoningLevel::Ultra,
1401 CodexRequestRole::Foreground,
1402 Some(&model),
1403 )
1404 .expect_err("an explicit unknown live effort list must fail closed");
1405 assert_eq!(error.code(), CodexPlanErrorCode::UnsupportedReasoningLevel);
1406 }
1407
1408 #[test]
1411 fn static_astra_has_ultra_v2_and_default_medium() {
1412 let cap = codex_static_capability("gpt-6-astra").expect("astra");
1413 match cap {
1414 ReasoningSupport::CodexNamed {
1415 supported,
1416 default_level,
1417 multi_agent_version,
1418 ..
1419 } => {
1420 assert_eq!(
1421 supported,
1422 vec![
1423 ReasoningLevel::Low,
1424 ReasoningLevel::Medium,
1425 ReasoningLevel::High,
1426 ReasoningLevel::XHigh,
1427 ReasoningLevel::Max,
1428 ReasoningLevel::Ultra,
1429 ],
1430 "astra ladder matches the live catalog exactly"
1431 );
1432 assert_eq!(
1433 default_level,
1434 Some(ReasoningLevel::Medium),
1435 "astra default is Medium (unlike Sol's Low)"
1436 );
1437 assert_eq!(
1438 multi_agent_version,
1439 Some(CodexMultiAgentVersion::V2),
1440 "astra advertises multi-agent v2 so Ultra is authorizable offline"
1441 );
1442 }
1443 _ => panic!("expected CodexNamed"),
1444 }
1445 }
1446
1447 #[test]
1448 fn static_astra_matches_fixture_row_exactly() {
1449 let live = parse_codex_catalog_models(FIXTURE).expect("parse fixture");
1451 let live_astra = live.iter().find(|m| m.id == "gpt-6-astra").unwrap();
1452 let static_astra = codex_static_catalog_models()
1453 .into_iter()
1454 .find(|m| m.id == "gpt-6-astra")
1455 .unwrap();
1456 assert_eq!(static_astra.label, live_astra.label);
1457 assert_eq!(static_astra.reasoning, live_astra.reasoning);
1458 assert_eq!(
1459 static_astra.codex_supported_levels(),
1460 live_astra.codex_supported_levels()
1461 );
1462 assert_eq!(
1463 static_astra.codex_multi_agent_version(),
1464 live_astra.codex_multi_agent_version()
1465 );
1466 assert_eq!(static_astra.source, CatalogSource::StaticFallback);
1467 assert_eq!(live_astra.source, CatalogSource::Live);
1468 }
1469
1470 #[test]
1471 fn static_sol_has_ultra_and_default_low() {
1472 let cap = codex_static_capability("gpt-5.6-sol").expect("sol");
1473 match cap {
1474 ReasoningSupport::CodexNamed {
1475 supported,
1476 default_level,
1477 ..
1478 } => {
1479 assert!(
1480 supported.contains(&ReasoningLevel::Ultra),
1481 "sol needs ultra"
1482 );
1483 assert!(supported.contains(&ReasoningLevel::Max), "sol needs max");
1484 assert_eq!(
1485 default_level,
1486 Some(ReasoningLevel::Low),
1487 "sol default is Low"
1488 );
1489 }
1490 _ => panic!("expected CodexNamed"),
1491 }
1492 }
1493
1494 #[test]
1495 fn static_terra_has_ultra_and_default_medium() {
1496 let cap = codex_static_capability("gpt-5.6-terra").expect("terra");
1497 match cap {
1498 ReasoningSupport::CodexNamed {
1499 supported,
1500 default_level,
1501 ..
1502 } => {
1503 assert!(
1504 supported.contains(&ReasoningLevel::Ultra),
1505 "terra needs ultra"
1506 );
1507 assert!(supported.contains(&ReasoningLevel::Max), "terra needs max");
1508 assert_eq!(
1509 default_level,
1510 Some(ReasoningLevel::Medium),
1511 "terra default is Medium"
1512 );
1513 }
1514 _ => panic!("expected CodexNamed"),
1515 }
1516 }
1517
1518 #[test]
1519 fn static_luna_has_max_not_ultra_and_default_medium() {
1520 let cap = codex_static_capability("gpt-5.6-luna").unwrap();
1521 match cap {
1522 ReasoningSupport::CodexNamed {
1523 supported,
1524 default_level,
1525 ..
1526 } => {
1527 assert!(supported.contains(&ReasoningLevel::Max));
1528 assert!(!supported.contains(&ReasoningLevel::Ultra));
1529 assert_eq!(
1530 default_level,
1531 Some(ReasoningLevel::Medium),
1532 "luna default is Medium"
1533 );
1534 }
1535 _ => panic!(),
1536 }
1537 }
1538
1539 #[test]
1540 fn static_gpt55_family_has_xhigh_not_max_ultra_default_medium() {
1541 for id in ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"] {
1542 let cap = codex_static_capability(id).expect(id);
1543 match cap {
1544 ReasoningSupport::CodexNamed {
1545 supported,
1546 default_level,
1547 ..
1548 } => {
1549 assert!(
1550 supported.contains(&ReasoningLevel::XHigh),
1551 "{id} needs xhigh"
1552 );
1553 assert!(
1554 !supported.contains(&ReasoningLevel::Max),
1555 "{id} must NOT have max"
1556 );
1557 assert!(
1558 !supported.contains(&ReasoningLevel::Ultra),
1559 "{id} must NOT have ultra"
1560 );
1561 assert_eq!(
1562 default_level,
1563 Some(ReasoningLevel::Medium),
1564 "{id} default is Medium"
1565 );
1566 }
1567 _ => panic!(),
1568 }
1569 }
1570 }
1571
1572 #[test]
1573 fn static_spark_default_high() {
1574 let cap = codex_static_capability("gpt-5.3-codex-spark").unwrap();
1575 match cap {
1576 ReasoningSupport::CodexNamed {
1577 supported,
1578 default_level,
1579 ..
1580 } => {
1581 assert!(supported.contains(&ReasoningLevel::XHigh));
1582 assert!(!supported.contains(&ReasoningLevel::Max));
1583 assert!(!supported.contains(&ReasoningLevel::Ultra));
1584 assert_eq!(
1585 default_level,
1586 Some(ReasoningLevel::High),
1587 "spark default is High"
1588 );
1589 }
1590 _ => panic!(),
1591 }
1592 }
1593
1594 #[test]
1595 fn static_unknown_model_returns_none() {
1596 assert!(codex_static_capability("gpt-future-unknown").is_none());
1597 assert!(codex_static_capability("codex-auto-review").is_none());
1599 }
1600
1601 #[test]
1604 fn validate_astra_full_ladder_ok() {
1605 for level in [
1606 ReasoningLevel::Low,
1607 ReasoningLevel::Medium,
1608 ReasoningLevel::High,
1609 ReasoningLevel::XHigh,
1610 ReasoningLevel::Max,
1611 ReasoningLevel::Ultra,
1612 ] {
1613 assert!(
1614 validate_codex_level("gpt-6-astra", level, None).is_ok(),
1615 "astra must accept {level}"
1616 );
1617 }
1618 }
1619
1620 #[test]
1621 fn execution_plan_astra_ultra_is_proactive_foreground_xhigh_wire() {
1622 let plan = plan_codex_execution(
1623 "openai-codex/gpt-6-astra",
1624 ReasoningLevel::Ultra,
1625 CodexRequestRole::Foreground,
1626 None,
1627 )
1628 .expect("astra ultra plan");
1629 assert_eq!(plan.mode, CodexExecutionMode::Ultra);
1630 assert_eq!(plan.wire_effort, Some(CodexWireEffort::XHigh));
1631 assert_eq!(plan.multi_agent_version, Some(CodexMultiAgentVersion::V2));
1632 assert_eq!(plan.multi_agent_mode, Some(CodexMultiAgentMode::Proactive));
1633 assert!(plan.automatic_delegation());
1634 }
1635
1636 #[test]
1637 fn validate_sol_ultra_ok() {
1638 assert!(validate_codex_level("gpt-5.6-sol", ReasoningLevel::Ultra, None).is_ok());
1639 }
1640
1641 #[test]
1642 fn validate_sol_client_omission_modes_ok() {
1643 assert!(validate_codex_level("gpt-5.6-sol", ReasoningLevel::Off, None).is_ok());
1644 assert!(validate_codex_level("gpt-5.6-sol", ReasoningLevel::Adaptive, None).is_ok());
1645 }
1646
1647 #[test]
1648 fn validate_luna_ultra_rejected() {
1649 let err = validate_codex_level("gpt-5.6-luna", ReasoningLevel::Ultra, None).unwrap_err();
1650 assert!(err.contains("ultra"), "error must name the rejected level");
1651 assert!(err.contains("gpt-5.6-luna"));
1652 }
1653
1654 #[test]
1655 fn validate_gpt55_max_rejected() {
1656 assert!(validate_codex_level("gpt-5.5", ReasoningLevel::Max, None).is_err());
1657 assert!(validate_codex_level("gpt-5.5", ReasoningLevel::Ultra, None).is_err());
1658 }
1659
1660 #[test]
1661 fn validate_unknown_model_rejected() {
1662 let err = validate_codex_level("gpt-future-x", ReasoningLevel::Max, None).unwrap_err();
1663 assert!(err.contains("no capability metadata"));
1664 }
1665
1666 #[test]
1667 fn validate_live_catalog_overrides_static() {
1668 let mut live = CatalogModel::new(PROVIDER_KEY, PROVIDER_NAME, "gpt-5.6-sol").unwrap();
1670 live.provider_kind = CatalogProviderKind::OpenAiCodex;
1671 live.reasoning = ReasoningSupport::CodexNamed {
1672 multi_agent_reasoning_effort: None,
1673 supported: vec![ReasoningLevel::Low, ReasoningLevel::Medium],
1674 default_level: Some(ReasoningLevel::Low),
1675 multi_agent_version: None,
1676 };
1677 let err =
1679 validate_codex_level("gpt-5.6-sol", ReasoningLevel::Ultra, Some(&live)).unwrap_err();
1680 assert!(err.contains("ultra"));
1681 assert!(validate_codex_level("gpt-5.6-sol", ReasoningLevel::Low, Some(&live)).is_ok());
1683 }
1684
1685 #[test]
1686 fn validate_live_ultra_requires_v2_even_when_listed() {
1687 let mut live = CatalogModel::new(PROVIDER_KEY, PROVIDER_NAME, "gpt-ultra-no-v2").unwrap();
1688 live.provider_kind = CatalogProviderKind::OpenAiCodex;
1689 live.source = CatalogSource::Live;
1690 live.reasoning = ReasoningSupport::CodexNamed {
1691 multi_agent_reasoning_effort: None,
1692 supported: vec![ReasoningLevel::Max, ReasoningLevel::Ultra],
1693 default_level: Some(ReasoningLevel::Max),
1694 multi_agent_version: None,
1695 };
1696
1697 assert!(validate_codex_level("gpt-ultra-no-v2", ReasoningLevel::Max, Some(&live)).is_ok());
1698 let err = validate_codex_level("gpt-ultra-no-v2", ReasoningLevel::Ultra, Some(&live))
1699 .expect_err("Ultra must require exact v2 capability");
1700 assert!(err.contains("multi-agent v2"), "{err}");
1701 }
1702
1703 #[test]
1707 fn validate_cache_narrows_sol_rejects_ultra_without_catalog_arg() {
1708 let unique_id = "gpt-5.6-sol-cache-test-ultra";
1710 let qualified = format!("openai-codex/{unique_id}");
1711
1712 let mut live = CatalogModel::new(PROVIDER_KEY, PROVIDER_NAME, unique_id).unwrap();
1714 live.provider_kind = CatalogProviderKind::OpenAiCodex;
1715 live.source = CatalogSource::Live;
1716 live.reasoning = ReasoningSupport::CodexNamed {
1717 multi_agent_reasoning_effort: None,
1718 supported: vec![ReasoningLevel::Low, ReasoningLevel::Medium],
1719 default_level: Some(ReasoningLevel::Low),
1720 multi_agent_version: None,
1721 };
1722 super::super::capability_cache::insert(live);
1723
1724 let cached = super::super::capability_cache::get(&qualified)
1726 .expect("model must be in cache after insert");
1727 assert!(matches!(
1728 cached.reasoning,
1729 ReasoningSupport::CodexNamed { .. }
1730 ));
1731
1732 let err = validate_codex_level(unique_id, ReasoningLevel::Ultra, None)
1735 .expect_err("cache should narrow Ultra rejection");
1736 assert!(
1737 err.contains("ultra"),
1738 "error must name the rejected level; got: {err}"
1739 );
1740
1741 assert!(
1743 validate_codex_level(unique_id, ReasoningLevel::Low, None).is_ok(),
1744 "Low must still pass via cache"
1745 );
1746 }
1747
1748 #[test]
1751 fn models_path_includes_client_version_query() {
1752 assert_eq!(
1753 codex_models_path("0.153.3"),
1754 "/codex/models?client_version=0.153.3"
1755 );
1756 assert_eq!(
1757 codex_models_url("0.153.3"),
1758 "https://chatgpt.com/backend-api/codex/models?client_version=0.153.3"
1759 );
1760 }
1761
1762 #[test]
1763 fn models_path_empty_version_falls_back_to_pinned_protocol_version() {
1764 assert_eq!(
1767 codex_models_path(""),
1768 format!("/codex/models?client_version={CODEX_CLIENT_VERSION}")
1769 );
1770 assert_eq!(codex_models_path(" "), codex_models_path(""));
1771 }
1772
1773 #[test]
1774 fn models_path_targets_codex_catalog_not_chatgpt_web_picker() {
1775 assert_eq!(MODELS_PATH, "/codex/models");
1778 assert!(codex_models_path("0.153.3").starts_with("/codex/models?"));
1779 }
1780
1781 #[test]
1782 fn selectable_rules_match_codex_picker() {
1783 assert!(codex_model_is_selectable(Some("list"), Some(true)));
1784 assert!(codex_model_is_selectable(Some("list"), Some(false)));
1785 assert!(codex_model_is_selectable(None, Some(true)));
1786 assert!(codex_model_is_selectable(Some("list"), None));
1787 assert!(!codex_model_is_selectable(Some("hide"), Some(true)));
1788 assert!(!codex_model_is_selectable(Some("internal"), Some(false)));
1789 assert!(!codex_model_is_selectable(Some("hidden"), Some(true)));
1790 }
1791
1792 #[test]
1793 fn static_catalog_is_current_safe_chatgpt_oauth_set() {
1794 let models = codex_static_catalog_models();
1795 let ids: Vec<_> = models.iter().map(|model| model.id.as_str()).collect();
1796 assert_eq!(
1797 ids,
1798 vec![
1799 "gpt-6-astra",
1800 "gpt-5.6-sol",
1801 "gpt-5.6-terra",
1802 "gpt-5.6-luna",
1803 "gpt-5.5",
1804 "gpt-5.4",
1805 "gpt-5.4-mini",
1806 "gpt-5.3-codex-spark",
1807 ]
1808 );
1809 for api_only in [
1811 "gpt-6-astra-wm",
1812 "gpt-6-pro",
1813 "gpt-5.6-sol-wm",
1814 "gpt-5.6-pro",
1815 "gpt-5.5-pro",
1816 ] {
1817 assert!(!ids.contains(&api_only));
1818 }
1819 assert!(models
1820 .iter()
1821 .all(|m| m.source == CatalogSource::StaticFallback));
1822 assert!(models
1823 .iter()
1824 .all(|m| m.runtime_id().starts_with("openai-codex/")));
1825 }
1826
1827 #[test]
1828 fn static_catalog_has_codex_named_reasoning() {
1829 let models = codex_static_catalog_models();
1830 for m in &models {
1831 assert!(
1832 matches!(m.reasoning, ReasoningSupport::CodexNamed { .. }),
1833 "static model {} must have CodexNamed reasoning",
1834 m.id
1835 );
1836 }
1837 }
1838
1839 #[test]
1840 fn invalid_json_returns_error() {
1841 assert!(parse_codex_catalog_models("{not json}").is_err());
1842 }
1843
1844 #[test]
1845 fn missing_models_key_returns_error() {
1846 assert!(parse_codex_catalog_models(r#"{"data":[]}"#).is_err());
1847 }
1848}