Skip to main content

agent_engine/runtime/openai/catalog/
xai.rs

1//! Selectable xAI model IDs documented for public text/Responses use.
2//!
3//! This is a conservative allowlist, not an exhaustive xAI model list. Aliases
4//! are first-class selectable IDs when xAI documents them as valid model IDs.
5//! Build/code and image, video, and voice models are excluded because the
6//! available public documentation does not establish public Responses support
7//! for this text runtime.
8
9use super::{
10    CatalogModel, CatalogProviderKind, CatalogSource, Modality, PricingSummary, ReasoningSupport,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct XaiModelDescriptor {
15    pub id: &'static str,
16    pub label: &'static str,
17    pub context_tokens: Option<u64>,
18    pub reasoning: bool,
19}
20
21pub const XAI_TEXT_MODELS: &[XaiModelDescriptor] = &[
22    XaiModelDescriptor {
23        id: "grok-4.3",
24        label: "Grok 4.3",
25        context_tokens: None,
26        reasoning: true,
27    },
28    XaiModelDescriptor {
29        id: "grok-4.3-latest",
30        label: "Grok 4.3 (latest alias)",
31        context_tokens: None,
32        reasoning: true,
33    },
34    XaiModelDescriptor {
35        id: "grok-latest",
36        label: "Grok (latest alias)",
37        context_tokens: None,
38        reasoning: true,
39    },
40    XaiModelDescriptor {
41        id: "grok-4.20-0309-reasoning",
42        label: "Grok 4.20 Reasoning",
43        context_tokens: None,
44        reasoning: true,
45    },
46    XaiModelDescriptor {
47        id: "grok-4.20-0309-non-reasoning",
48        label: "Grok 4.20 Non-Reasoning",
49        context_tokens: None,
50        reasoning: false,
51    },
52    XaiModelDescriptor {
53        id: "grok-4.20-multi-agent-0309",
54        label: "Grok 4.20 Multi-Agent",
55        context_tokens: None,
56        reasoning: true,
57    },
58    XaiModelDescriptor {
59        id: "grok-4.5",
60        label: "Grok 4.5",
61        context_tokens: None,
62        reasoning: true,
63    },
64    XaiModelDescriptor {
65        id: "grok-4.5-latest",
66        label: "Grok 4.5 (latest alias)",
67        context_tokens: None,
68        reasoning: true,
69    },
70];
71
72pub fn xai_model(id: &str) -> Option<&'static XaiModelDescriptor> {
73    XAI_TEXT_MODELS.iter().find(|model| model.id == id)
74}
75
76// ─── Exact-id reasoning capability (spec: anthropic-xai-reasoning-modes) ─────
77
78/// Documented reasoning capability for an exact xAI model id.
79///
80/// Evidence: official xAI docs. `grok-4.5`/`grok-4.5-latest` support
81/// low/medium/high effort, default high, and reasoning cannot be disabled.
82/// `grok-4.20-multi-agent-0309` supports low/medium/high/xhigh where effort
83/// controls agent count. No other exact id has documented effort support.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum XaiReasoningCapability {
86    /// Documented named-effort control (`reasoning:{effort:"..."}` on the
87    /// Responses wire).
88    Effort {
89        supported: &'static [agent_core::reasoning::ReasoningLevel],
90        default_level: Option<agent_core::reasoning::ReasoningLevel>,
91        /// Whether reasoning can be disabled. `false` → `Off` must be
92        /// rejected, never silently omitted/downgraded.
93        can_disable: bool,
94    },
95    /// Reasoning is intrinsic; no documented effort control or disable switch.
96    IntrinsicReasoning,
97    /// Model does not reason; named reasoning levels must be rejected.
98    NonReasoning,
99}
100
101/// Exact-id capability lookup — never substring-based. Unknown ids return
102/// `None` and fail closed at validation time.
103pub fn xai_static_capability(model_id: &str) -> Option<XaiReasoningCapability> {
104    use agent_core::reasoning::ReasoningLevel::*;
105    match model_id {
106        "grok-4.5" | "grok-4.5-latest" => Some(XaiReasoningCapability::Effort {
107            supported: &[Low, Medium, High],
108            default_level: Some(High),
109            can_disable: false,
110        }),
111        "grok-4.20-multi-agent-0309" => Some(XaiReasoningCapability::Effort {
112            supported: &[Low, Medium, High, XHigh],
113            // Effort controls agent count; no documented default level.
114            default_level: None,
115            can_disable: false,
116        }),
117        "grok-4.3" | "grok-4.3-latest" | "grok-latest" | "grok-4.20-0309-reasoning" => {
118            Some(XaiReasoningCapability::IntrinsicReasoning)
119        }
120        "grok-4.20-0309-non-reasoning" => Some(XaiReasoningCapability::NonReasoning),
121        _ => None,
122    }
123}
124
125pub fn xai_static_catalog_models() -> Vec<CatalogModel> {
126    XAI_TEXT_MODELS
127        .iter()
128        .map(|descriptor| CatalogModel {
129            provider_key: "xai-auth".into(),
130            provider_name: "xAI (Grok)".into(),
131            provider_kind: CatalogProviderKind::Generic {
132                key: "xai-auth".into(),
133            },
134            id: descriptor.id.into(),
135            label: Some(descriptor.label.into()),
136            context_tokens: descriptor.context_tokens,
137            max_output_tokens: None,
138            input_modalities: vec![Modality::Text],
139            pricing: PricingSummary::default(),
140            reasoning: if descriptor.reasoning {
141                ReasoningSupport::GenericOpenAi
142            } else {
143                ReasoningSupport::None
144            },
145            source: CatalogSource::StaticFallback,
146        })
147        .collect()
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn documented_selectable_public_responses_text_ids_are_exposed() {
156        assert_eq!(
157            XAI_TEXT_MODELS.iter().map(|m| m.id).collect::<Vec<_>>(),
158            vec![
159                "grok-4.3",
160                "grok-4.3-latest",
161                "grok-latest",
162                "grok-4.20-0309-reasoning",
163                "grok-4.20-0309-non-reasoning",
164                "grok-4.20-multi-agent-0309",
165                "grok-4.5",
166                "grok-4.5-latest",
167            ]
168        );
169    }
170
171    #[test]
172    fn reasoning_metadata_is_limited_to_documented_reasoning_models() {
173        let reasoning_ids = xai_static_catalog_models()
174            .into_iter()
175            .filter(|model| model.reasoning != ReasoningSupport::None)
176            .map(|model| model.id)
177            .collect::<Vec<_>>();
178        assert_eq!(
179            reasoning_ids,
180            vec![
181                "grok-4.3",
182                "grok-4.3-latest",
183                "grok-latest",
184                "grok-4.20-0309-reasoning",
185                "grok-4.20-multi-agent-0309",
186                "grok-4.5",
187                "grok-4.5-latest",
188            ]
189        );
190    }
191
192    // ── Exact-id reasoning capability table (spec: anthropic-xai-reasoning-modes) ──
193
194    #[test]
195    fn grok_45_family_effort_capability_is_exact() {
196        use agent_core::reasoning::ReasoningLevel::*;
197        for id in ["grok-4.5", "grok-4.5-latest"] {
198            match xai_static_capability(id) {
199                Some(XaiReasoningCapability::Effort {
200                    supported,
201                    default_level,
202                    can_disable,
203                }) => {
204                    assert_eq!(supported, &[Low, Medium, High], "{id}");
205                    assert_eq!(default_level, Some(High), "{id}");
206                    assert!(!can_disable, "{id}: reasoning cannot be disabled");
207                }
208                other => panic!("{id}: expected Effort capability, got {other:?}"),
209            }
210        }
211    }
212
213    #[test]
214    fn multi_agent_supports_xhigh_but_45_does_not() {
215        use agent_core::reasoning::ReasoningLevel::*;
216        match xai_static_capability("grok-4.20-multi-agent-0309") {
217            Some(XaiReasoningCapability::Effort {
218                supported,
219                default_level,
220                can_disable,
221            }) => {
222                assert_eq!(supported, &[Low, Medium, High, XHigh]);
223                assert_eq!(default_level, None, "no documented default");
224                assert!(!can_disable);
225            }
226            other => panic!("expected Effort capability, got {other:?}"),
227        }
228        // 4.5 has no documented xhigh — must not appear in its set.
229        if let Some(XaiReasoningCapability::Effort { supported, .. }) =
230            xai_static_capability("grok-4.5")
231        {
232            assert!(!supported.contains(&XHigh));
233        }
234    }
235
236    #[test]
237    fn intrinsic_reasoning_models_have_no_named_effort() {
238        for id in [
239            "grok-4.3",
240            "grok-4.3-latest",
241            "grok-latest",
242            "grok-4.20-0309-reasoning",
243        ] {
244            assert_eq!(
245                xai_static_capability(id),
246                Some(XaiReasoningCapability::IntrinsicReasoning),
247                "{id}"
248            );
249        }
250    }
251
252    #[test]
253    fn non_reasoning_model_is_marked_non_reasoning() {
254        assert_eq!(
255            xai_static_capability("grok-4.20-0309-non-reasoning"),
256            Some(XaiReasoningCapability::NonReasoning)
257        );
258    }
259
260    #[test]
261    fn unknown_exact_ids_fail_closed() {
262        for id in ["grok-5", "grok-4.50", "grok-4.5x", "", "gpt-5.5"] {
263            assert_eq!(xai_static_capability(id), None, "{id}");
264        }
265    }
266
267    #[test]
268    fn catalog_is_text_only_and_excludes_unconfirmed_code_model() {
269        let models = xai_static_catalog_models();
270        assert!(models
271            .iter()
272            .all(|m| m.input_modalities == vec![Modality::Text]));
273        assert!(!models.iter().any(|m| m.id == "grok-build-0.1"));
274        assert!(!models.iter().any(|m| m.id == "grok-build-latest"));
275        assert!(!models.iter().any(|m| ["image", "video", "voice"]
276            .iter()
277            .any(|kind| m.id.contains(kind))));
278    }
279}