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