Skip to main content

roder_api/
catalog.rs

1use serde::Serialize;
2
3use crate::inference::{
4    ModelDescriptor, ModelHarnessProfile, ModelInstructionOverlay, ModelProfileReasoning,
5    ModelSchemaPolicy, ProviderFamily, ReasoningEffortDescriptor,
6};
7
8pub mod image_models;
9mod openai_codex;
10mod synthetic;
11mod xiaomi_mimo;
12
13pub use image_models::{
14    IMAGE_PROVIDER_GOOGLE, IMAGE_PROVIDER_OPENAI, ImageModelCatalogEntry,
15    ImageProviderCatalogEntry, built_in_image_providers, image_model_descriptors,
16    image_models_for_provider, lookup_image_model, lookup_image_provider,
17};
18pub use synthetic::{SYNTHETIC_DEFAULT_BASE_URL, SYNTHETIC_DEFAULT_MODEL, SYNTHETIC_ENV_ALIASES};
19pub use xiaomi_mimo::{XIAOMI_MIMO_ENV_ALIASES, XIAOMI_MIMO_TOKEN_PLAN_ENV_ALIASES};
20
21pub const PROVIDER_MOCK: &str = "mock";
22pub const PROVIDER_OPENAI: &str = "openai";
23pub const PROVIDER_CODEX: &str = "codex";
24pub const PROVIDER_ANTHROPIC: &str = "anthropic";
25pub const PROVIDER_CLAUDE_CODE: &str = "claude-code";
26pub const PROVIDER_GEMINI: &str = "gemini";
27pub const PROVIDER_VERTEX: &str = "vertex";
28pub const PROVIDER_GOOGLE: &str = "google";
29pub const PROVIDER_ZEROENTROPY: &str = "zeroentropy";
30pub const PROVIDER_XAI: &str = "xai";
31pub const PROVIDER_SUPERGROK: &str = "supergrok";
32pub const PROVIDER_OPENCODE: &str = "opencode";
33pub const PROVIDER_OPENCODE_GO: &str = "opencode-go";
34pub const PROVIDER_OPENROUTER: &str = "openrouter";
35pub const PROVIDER_FIREWORKS: &str = "fireworks";
36pub const PROVIDER_RODER_CLOUD: &str = "roder-cloud";
37pub const PROVIDER_POOLSIDE: &str = "poolside";
38pub const PROVIDER_CURSOR: &str = "cursor";
39pub const PROVIDER_XIAOMI_MIMO: &str = "xiaomi-mimo";
40pub const PROVIDER_XIAOMI_MIMO_TOKEN_PLAN: &str = "xiaomi-mimo-token-plan";
41pub const PROVIDER_KIMI_CODE: &str = "kimi-code";
42pub const PROVIDER_SYNTHETIC: &str = "synthetic";
43
44pub const PROVIDER_KIND_MOCK: &str = "mock";
45pub const PROVIDER_KIND_OPENAI: &str = "openai";
46pub const PROVIDER_KIND_CHAT_COMPLETIONS: &str = "chat_completions";
47pub const PROVIDER_KIND_ANTHROPIC: &str = "anthropic";
48pub const PROVIDER_KIND_CLAUDE_CODE: &str = "claude_code";
49pub const PROVIDER_KIND_GEMINI: &str = "gemini";
50pub const PROVIDER_KIND_VERTEX: &str = "vertex";
51pub const PROVIDER_KIND_XAI: &str = "xai";
52pub const PROVIDER_KIND_OPENCODE: &str = "opencode";
53pub const PROVIDER_KIND_OPENROUTER: &str = "openrouter";
54pub const PROVIDER_KIND_FIREWORKS: &str = "fireworks";
55pub const PROVIDER_KIND_RODER_CLOUD: &str = "roder_cloud";
56pub const PROVIDER_KIND_POOLSIDE: &str = "poolside";
57pub const PROVIDER_KIND_CURSOR: &str = "cursor";
58pub const PROVIDER_KIND_XIAOMI_MIMO: &str = PROVIDER_KIND_CHAT_COMPLETIONS;
59pub const PROVIDER_KIND_SYNTHETIC: &str = PROVIDER_KIND_CHAT_COMPLETIONS;
60
61pub const REASONING_NONE: &str = "none";
62pub const REASONING_MINIMAL: &str = "minimal";
63pub const REASONING_LOW: &str = "low";
64pub const REASONING_MEDIUM: &str = "medium";
65pub const REASONING_HIGH: &str = "high";
66pub const REASONING_XHIGH: &str = "xhigh";
67pub const REASONING_MAX: &str = "max";
68pub const REASONING_ULTRA: &str = "ultra";
69
70pub const DEFAULT_MODEL_ID: &str = "gpt-5.6-sol";
71pub const EDIT_TOOL_PATCH: &str = "patch";
72pub const EDIT_TOOL_EDIT: &str = "edit";
73
74#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
75pub struct ProviderCatalogEntry {
76    pub id: &'static str,
77    pub name: &'static str,
78    pub kind: &'static str,
79    pub default_model: &'static str,
80    pub base_url: Option<&'static str>,
81    pub env_key: Option<&'static str>,
82    pub env_aliases: &'static [&'static str],
83    pub requires_auth: bool,
84    pub supports_websockets: bool,
85}
86
87#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
88pub struct ReasoningOption {
89    pub effort: &'static str,
90    pub description: &'static str,
91}
92
93#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
94pub struct ModelCatalogEntry {
95    pub id: &'static str,
96    pub display_name: &'static str,
97    pub description: &'static str,
98    pub provider: &'static str,
99    pub default_reasoning: &'static str,
100    pub supported_reasoning: &'static [ReasoningOption],
101    pub context_window: u32,
102    pub max_context_window: u32,
103    pub auto_compact_token_limit: u32,
104    pub supports_compaction: bool,
105    pub supports_images: bool,
106    pub supports_tools: bool,
107    pub supports_structured: bool,
108    pub edit_tool: Option<&'static str>,
109    pub hidden: bool,
110}
111
112pub const STANDARD_REASONING: &[ReasoningOption] = &[
113    ReasoningOption {
114        effort: REASONING_LOW,
115        description: "Fast responses with lighter reasoning",
116    },
117    ReasoningOption {
118        effort: REASONING_MEDIUM,
119        description: "Balances speed and reasoning depth for everyday tasks",
120    },
121    ReasoningOption {
122        effort: REASONING_HIGH,
123        description: "Greater reasoning depth for complex problems",
124    },
125    ReasoningOption {
126        effort: REASONING_XHIGH,
127        description: "Extra high reasoning depth for complex problems",
128    },
129];
130
131// Claude Fable 5 and Opus 4.7/4.8 support the full effort range, including
132// `xhigh` for long-horizon agentic work and `max` for genuinely frontier
133// problems.
134pub const OPUS_REASONING: &[ReasoningOption] = &[
135    ReasoningOption {
136        effort: REASONING_LOW,
137        description: "Most efficient; best for short, scoped tasks",
138    },
139    ReasoningOption {
140        effort: REASONING_MEDIUM,
141        description: "Balanced reasoning depth for cost-sensitive workflows",
142    },
143    ReasoningOption {
144        effort: REASONING_HIGH,
145        description: "High capability for complex reasoning and agentic tasks",
146    },
147    ReasoningOption {
148        effort: REASONING_XHIGH,
149        description: "Extended capability for long-horizon coding and agentic work",
150    },
151    ReasoningOption {
152        effort: REASONING_MAX,
153        description: "Absolute maximum capability with no constraints on token spending",
154    },
155];
156
157// Claude Sonnet 4.6 supports `max` but not `xhigh`.
158pub const SONNET_REASONING: &[ReasoningOption] = &[
159    ReasoningOption {
160        effort: REASONING_LOW,
161        description: "Most efficient; lowest latency and cost",
162    },
163    ReasoningOption {
164        effort: REASONING_MEDIUM,
165        description: "Balances speed, cost, and performance for most tasks",
166    },
167    ReasoningOption {
168        effort: REASONING_HIGH,
169        description: "Greater reasoning depth for complex problems",
170    },
171    ReasoningOption {
172        effort: REASONING_MAX,
173        description: "Absolute maximum capability with no constraints on token spending",
174    },
175];
176
177pub const GPT_52_REASONING: &[ReasoningOption] = &[
178    ReasoningOption {
179        effort: REASONING_LOW,
180        description: "Balances speed with some reasoning; useful for straightforward queries and short explanations",
181    },
182    ReasoningOption {
183        effort: REASONING_MEDIUM,
184        description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks",
185    },
186    ReasoningOption {
187        effort: REASONING_HIGH,
188        description: "Maximizes reasoning depth for complex or ambiguous problems",
189    },
190    ReasoningOption {
191        effort: REASONING_XHIGH,
192        description: "Extra high reasoning for complex problems",
193    },
194];
195
196pub const HAIKU_REASONING: &[ReasoningOption] = &[
197    ReasoningOption {
198        effort: REASONING_LOW,
199        description: "Fast responses with lighter reasoning",
200    },
201    ReasoningOption {
202        effort: REASONING_MEDIUM,
203        description: "Balances speed and reasoning depth for everyday tasks",
204    },
205];
206
207pub const GEMINI_REASONING: &[ReasoningOption] = &[
208    ReasoningOption {
209        effort: REASONING_MINIMAL,
210        description: "Minimal Gemini thinking",
211    },
212    ReasoningOption {
213        effort: REASONING_LOW,
214        description: "Low Gemini thinking",
215    },
216    ReasoningOption {
217        effort: REASONING_MEDIUM,
218        description: "Medium Gemini thinking",
219    },
220    ReasoningOption {
221        effort: REASONING_HIGH,
222        description: "High Gemini thinking",
223    },
224];
225
226pub const MOCK_REASONING: &[ReasoningOption] = &[ReasoningOption {
227    effort: REASONING_NONE,
228    description: "No model-side reasoning",
229}];
230
231pub const POOLSIDE_REASONING: &[ReasoningOption] = &[
232    ReasoningOption {
233        effort: REASONING_NONE,
234        description: "Disable Poolside thinking for lower latency",
235    },
236    ReasoningOption {
237        effort: REASONING_MEDIUM,
238        description: "Enable Poolside thinking",
239    },
240];
241
242pub const GEMINI_ENV_ALIASES: &[&str] = &[
243    "GEMINI_API_KEY",
244    "GOOGLE_API_KEY",
245    "GOOGLE_GENAI_API_KEY",
246    "GOOGLE_AI_API_KEY",
247];
248
249pub const VERTEX_ENV_ALIASES: &[&str] = &["VERTEX_CREDENTIALS_JSON"];
250
251pub const XAI_ENV_ALIASES: &[&str] = &["RODER_XAI_API_KEY"];
252
253pub const XAI_CONFIGURABLE_REASONING: &[ReasoningOption] = &[
254    ReasoningOption {
255        effort: REASONING_NONE,
256        description: "No xAI reasoning effort",
257    },
258    ReasoningOption {
259        effort: REASONING_LOW,
260        description: "Low xAI reasoning effort",
261    },
262    ReasoningOption {
263        effort: REASONING_MEDIUM,
264        description: "Medium xAI reasoning effort",
265    },
266    ReasoningOption {
267        effort: REASONING_HIGH,
268        description: "High xAI reasoning effort",
269    },
270];
271
272pub const XAI_REASONING: &[ReasoningOption] = &[
273    ReasoningOption {
274        effort: REASONING_LOW,
275        description: "Low xAI reasoning effort",
276    },
277    ReasoningOption {
278        effort: REASONING_MEDIUM,
279        description: "Medium xAI reasoning effort",
280    },
281    ReasoningOption {
282        effort: REASONING_HIGH,
283        description: "High xAI reasoning effort",
284    },
285];
286
287pub const XAI_NO_REASONING: &[ReasoningOption] = &[ReasoningOption {
288    effort: REASONING_NONE,
289    description: "No xAI reasoning effort",
290}];
291
292pub const OPENROUTER_REASONING: &[ReasoningOption] = &[
293    ReasoningOption {
294        effort: REASONING_NONE,
295        description: "Disable OpenRouter reasoning controls",
296    },
297    ReasoningOption {
298        effort: REASONING_LOW,
299        description: "Low OpenRouter reasoning effort",
300    },
301    ReasoningOption {
302        effort: REASONING_MEDIUM,
303        description: "Medium OpenRouter reasoning effort",
304    },
305    ReasoningOption {
306        effort: REASONING_HIGH,
307        description: "High OpenRouter reasoning effort",
308    },
309];
310
311/**
312 * The roder.cloud Responses-subset edge is synchronous text-only today: it
313 * does not stream SSE and drops function-call payloads from upstream output,
314 * so hosted models advertise no tool/image/structured support until the edge
315 * grows those surfaces.
316 */
317pub const RODER_CLOUD_REASONING: &[ReasoningOption] = &[ReasoningOption {
318    effort: REASONING_NONE,
319    description: "roder.cloud forwards no reasoning controls",
320}];
321
322pub const BUILT_IN_PROVIDERS: &[ProviderCatalogEntry] = &[
323    ProviderCatalogEntry {
324        id: PROVIDER_MOCK,
325        name: "Mock",
326        kind: PROVIDER_KIND_MOCK,
327        default_model: "mock",
328        base_url: None,
329        env_key: None,
330        env_aliases: &[],
331        requires_auth: false,
332        supports_websockets: false,
333    },
334    ProviderCatalogEntry {
335        id: PROVIDER_OPENAI,
336        name: "OpenAI",
337        kind: PROVIDER_KIND_OPENAI,
338        default_model: DEFAULT_MODEL_ID,
339        base_url: Some("https://api.openai.com/v1"),
340        env_key: Some("OPENAI_API_KEY"),
341        env_aliases: &[],
342        requires_auth: true,
343        supports_websockets: true,
344    },
345    ProviderCatalogEntry {
346        id: PROVIDER_CODEX,
347        name: "Codex",
348        kind: PROVIDER_KIND_OPENAI,
349        default_model: DEFAULT_MODEL_ID,
350        base_url: Some("https://api.openai.com/v1"),
351        env_key: Some("OPENAI_API_KEY"),
352        env_aliases: &[],
353        requires_auth: true,
354        supports_websockets: true,
355    },
356    ProviderCatalogEntry {
357        id: PROVIDER_ANTHROPIC,
358        name: "Anthropic",
359        kind: PROVIDER_KIND_ANTHROPIC,
360        default_model: "claude-sonnet-4-6",
361        base_url: Some("https://api.anthropic.com"),
362        env_key: Some("ANTHROPIC_API_KEY"),
363        env_aliases: &[],
364        requires_auth: true,
365        supports_websockets: false,
366    },
367    ProviderCatalogEntry {
368        id: PROVIDER_CLAUDE_CODE,
369        name: "Claude Code",
370        kind: PROVIDER_KIND_CLAUDE_CODE,
371        default_model: "sonnet",
372        base_url: None,
373        env_key: None,
374        env_aliases: &["CLAUDE_CODE_CLI_PATH", "RODER_CLAUDE_CODE_CLI_PATH"],
375        requires_auth: false,
376        supports_websockets: false,
377    },
378    ProviderCatalogEntry {
379        id: PROVIDER_GEMINI,
380        name: "Gemini",
381        kind: PROVIDER_KIND_GEMINI,
382        default_model: "gemini-3.5-flash",
383        base_url: None,
384        env_key: Some("GEMINI_API_TOKEN"),
385        env_aliases: GEMINI_ENV_ALIASES,
386        requires_auth: true,
387        supports_websockets: false,
388    },
389    ProviderCatalogEntry {
390        id: PROVIDER_VERTEX,
391        name: "Vertex AI",
392        kind: PROVIDER_KIND_VERTEX,
393        default_model: "gemini-3.5-flash",
394        base_url: None,
395        env_key: Some("GOOGLE_APPLICATION_CREDENTIALS"),
396        env_aliases: VERTEX_ENV_ALIASES,
397        requires_auth: true,
398        supports_websockets: false,
399    },
400    ProviderCatalogEntry {
401        id: PROVIDER_XAI,
402        name: "xAI",
403        kind: PROVIDER_KIND_XAI,
404        default_model: "grok-4.5",
405        base_url: Some("https://api.x.ai/v1"),
406        env_key: Some("XAI_API_KEY"),
407        env_aliases: XAI_ENV_ALIASES,
408        requires_auth: true,
409        supports_websockets: false,
410    },
411    ProviderCatalogEntry {
412        id: PROVIDER_SUPERGROK,
413        name: "SuperGrok",
414        kind: PROVIDER_KIND_XAI,
415        default_model: "grok-4.5",
416        base_url: Some("https://api.x.ai/v1"),
417        env_key: None,
418        env_aliases: &[],
419        requires_auth: true,
420        supports_websockets: false,
421    },
422    ProviderCatalogEntry {
423        id: PROVIDER_OPENCODE,
424        name: "OpenCode Zen",
425        kind: PROVIDER_KIND_OPENCODE,
426        default_model: "gpt-5.5",
427        base_url: Some("https://opencode.ai/zen/v1"),
428        env_key: Some("OPENCODE_API_KEY"),
429        env_aliases: &["OPENCODE_ZEN_API_KEY", "RODER_OPENCODE_API_KEY"],
430        requires_auth: true,
431        supports_websockets: false,
432    },
433    ProviderCatalogEntry {
434        id: PROVIDER_OPENCODE_GO,
435        name: "OpenCode Go",
436        kind: PROVIDER_KIND_OPENCODE,
437        default_model: "kimi-k2.6",
438        base_url: Some("https://opencode.ai/zen/go/v1"),
439        env_key: Some("OPENCODE_GO_API_KEY"),
440        env_aliases: &["RODER_OPENCODE_GO_API_KEY", "OPENCODE_API_KEY"],
441        requires_auth: true,
442        supports_websockets: false,
443    },
444    ProviderCatalogEntry {
445        id: PROVIDER_OPENROUTER,
446        name: "OpenRouter",
447        kind: PROVIDER_KIND_OPENROUTER,
448        default_model: "x-ai/grok-build-0.1",
449        base_url: Some("https://openrouter.ai/api/v1"),
450        env_key: Some("OPENROUTER_API_KEY"),
451        env_aliases: &["RODER_OPENROUTER_API_KEY"],
452        requires_auth: true,
453        supports_websockets: false,
454    },
455    ProviderCatalogEntry {
456        id: PROVIDER_FIREWORKS,
457        name: "Fireworks AI",
458        kind: PROVIDER_KIND_FIREWORKS,
459        default_model: "accounts/fireworks/models/qwen3-235b-a22b",
460        base_url: Some("https://api.fireworks.ai/inference/v1"),
461        env_key: Some("FIREWORKS_API_KEY"),
462        env_aliases: &["RODER_FIREWORKS_API_KEY"],
463        requires_auth: true,
464        supports_websockets: false,
465    },
466    ProviderCatalogEntry {
467        id: PROVIDER_RODER_CLOUD,
468        name: "Roder Cloud",
469        kind: PROVIDER_KIND_RODER_CLOUD,
470        default_model: "roder.cloud/free",
471        // The production inference edge hostname is deploy-specific; clients
472        // must configure base_url (or RODER_CLOUD_BASE_URL) until it is
473        // stable. Local dev: http://127.0.0.1:8080/v1.
474        base_url: None,
475        env_key: Some("RODER_CLOUD_API_KEY"),
476        env_aliases: &["RODER_CLOUD_TOKEN"],
477        requires_auth: true,
478        supports_websockets: false,
479    },
480    ProviderCatalogEntry {
481        id: PROVIDER_POOLSIDE,
482        name: "Poolside",
483        kind: PROVIDER_KIND_POOLSIDE,
484        default_model: "poolside/laguna-m.1",
485        base_url: Some("https://inference.poolside.ai/v1"),
486        env_key: Some("POOLSIDE_API_KEY"),
487        env_aliases: &["RODER_POOLSIDE_API_KEY"],
488        requires_auth: true,
489        supports_websockets: false,
490    },
491    ProviderCatalogEntry {
492        id: PROVIDER_CURSOR,
493        name: "Cursor",
494        kind: PROVIDER_KIND_CURSOR,
495        default_model: "composer-2.5",
496        base_url: Some("https://agentn.global.api5.cursor.sh"),
497        env_key: Some("CURSOR_API_KEY"),
498        env_aliases: &["RODER_CURSOR_API_KEY"],
499        requires_auth: true,
500        supports_websockets: false,
501    },
502    xiaomi_mimo::PAY_AS_YOU_GO_PROVIDER,
503    xiaomi_mimo::TOKEN_PLAN_PROVIDER,
504    synthetic::SYNTHETIC_PROVIDER,
505    ProviderCatalogEntry {
506        id: PROVIDER_KIMI_CODE,
507        name: "Kimi Code",
508        kind: PROVIDER_KIND_CHAT_COMPLETIONS,
509        default_model: "kimi-for-coding",
510        base_url: Some("https://api.kimi.com/coding/v1"),
511        env_key: Some("KIMI_CODE_API_KEY"),
512        env_aliases: &["RODER_KIMI_CODE_API_KEY"],
513        requires_auth: true,
514        supports_websockets: false,
515    },
516];
517
518pub const BUILT_IN_MODELS: &[ModelCatalogEntry] = &[
519    openai_codex::GPT_56_SOL,
520    openai_codex::GPT_56_TERRA,
521    openai_codex::GPT_56_LUNA,
522    openai_model(
523        "gpt-5.5",
524        "GPT-5.5",
525        "Frontier model for complex coding, research, and real-world work.",
526        1_050_000,
527        945_000,
528        true,
529        STANDARD_REASONING,
530    ),
531    openai_codex::GPT_54,
532    openai_model(
533        "gpt-5.4-mini",
534        "GPT-5.4-Mini",
535        "Small, fast, and cost-efficient model for simpler coding tasks.",
536        400_000,
537        360_000,
538        true,
539        STANDARD_REASONING,
540    ),
541    ModelCatalogEntry {
542        id: "gpt-5.3-codex-spark",
543        display_name: "GPT-5.3-Codex-Spark",
544        description: "Ultra-fast coding model optimized for low-latency Codex workflows.",
545        provider: PROVIDER_CODEX,
546        default_reasoning: REASONING_HIGH,
547        supported_reasoning: STANDARD_REASONING,
548        context_window: 128_000,
549        max_context_window: 128_000,
550        auto_compact_token_limit: 115_200,
551        supports_compaction: true,
552        supports_images: false,
553        supports_tools: true,
554        supports_structured: false,
555        edit_tool: Some("patch"),
556        hidden: false,
557    },
558    ModelCatalogEntry {
559        id: "codex-auto-review",
560        display_name: "Codex Auto Review",
561        description: "Automatic approval review model for Codex.",
562        provider: PROVIDER_OPENAI,
563        default_reasoning: REASONING_MEDIUM,
564        supported_reasoning: STANDARD_REASONING,
565        context_window: 272_000,
566        max_context_window: 272_000,
567        auto_compact_token_limit: 244_800,
568        supports_compaction: false,
569        supports_images: false,
570        supports_tools: true,
571        supports_structured: false,
572        edit_tool: Some("patch"),
573        hidden: true,
574    },
575    anthropic_model(
576        "claude-fable-5",
577        "Claude Fable 5",
578        "Anthropic's most powerful, most intelligent model; a new tier above Opus for frontier reasoning and agentic work.",
579        1_000_000,
580        900_000,
581        REASONING_HIGH,
582        OPUS_REASONING,
583        true,
584    ),
585    anthropic_model(
586        "claude-opus-4-8",
587        "Claude Opus 4.8",
588        "Anthropic's most capable Opus-tier model for complex reasoning, long-horizon agentic coding, and high-autonomy work.",
589        1_000_000,
590        900_000,
591        REASONING_HIGH,
592        OPUS_REASONING,
593        true,
594    ),
595    anthropic_model(
596        "claude-opus-4-7",
597        "Claude Opus 4.7",
598        "Most capable Claude model for complex reasoning and agentic coding.",
599        1_000_000,
600        900_000,
601        REASONING_HIGH,
602        OPUS_REASONING,
603        true,
604    ),
605    anthropic_model(
606        "claude-sonnet-4-6",
607        "Claude Sonnet 4.6",
608        "Balanced Claude model for coding, tool use, and everyday agent workflows.",
609        1_000_000,
610        900_000,
611        REASONING_MEDIUM,
612        SONNET_REASONING,
613        true,
614    ),
615    anthropic_model(
616        "claude-haiku-4-5-20251001",
617        "Claude Haiku 4.5",
618        "Fast Claude model for lower-latency tool workflows.",
619        200_000,
620        180_000,
621        REASONING_NONE,
622        &[],
623        // Live API rejects the compaction edit for Haiku 4.5 with 400.
624        false,
625    ),
626    claude_code_model(
627        "fable",
628        "Claude Code Fable",
629        "Claude Code harness Fable alias for the most powerful frontier model.",
630        1_000_000,
631        900_000,
632        REASONING_HIGH,
633        OPUS_REASONING,
634    ),
635    claude_code_model(
636        "sonnet",
637        "Claude Code Sonnet",
638        "Claude Code harness Sonnet alias for coding and tool workflows.",
639        1_000_000,
640        900_000,
641        REASONING_MEDIUM,
642        SONNET_REASONING,
643    ),
644    claude_code_model(
645        "opus",
646        "Claude Code Opus",
647        "Claude Code harness Opus alias for complex long-horizon agentic work.",
648        1_000_000,
649        900_000,
650        REASONING_HIGH,
651        OPUS_REASONING,
652    ),
653    claude_code_model(
654        "haiku",
655        "Claude Code Haiku",
656        "Claude Code harness Haiku alias for fast lower-latency coding turns.",
657        200_000,
658        180_000,
659        REASONING_NONE,
660        &[],
661    ),
662    claude_code_model(
663        "claude-sonnet-4-6",
664        "Claude Code Sonnet 4.6",
665        "Claude Sonnet 4.6 through the local Claude Code harness.",
666        1_000_000,
667        900_000,
668        REASONING_MEDIUM,
669        SONNET_REASONING,
670    ),
671    claude_code_model(
672        "claude-opus-4-8",
673        "Claude Code Opus 4.8",
674        "Claude Opus 4.8 through the local Claude Code harness.",
675        1_000_000,
676        900_000,
677        REASONING_HIGH,
678        OPUS_REASONING,
679    ),
680    claude_code_model(
681        "claude-fable-5",
682        "Claude Code Fable 5",
683        "Claude Fable 5 through the local Claude Code harness.",
684        1_000_000,
685        900_000,
686        REASONING_HIGH,
687        OPUS_REASONING,
688    ),
689    gemini_model(
690        PROVIDER_GEMINI,
691        "gemini-3.5-flash",
692        "Gemini 3.5 Flash",
693        "Stable Gemini Flash model for agentic coding, tool use, and long-horizon workflows.",
694        REASONING_MEDIUM,
695    ),
696    gemini_model(
697        PROVIDER_GEMINI,
698        "gemini-3.1-pro-preview",
699        "Gemini 3.1 Pro Preview",
700        "Gemini model for complex coding, long context, and tool-heavy agent workflows.",
701        REASONING_HIGH,
702    ),
703    gemini_model(
704        PROVIDER_GEMINI,
705        "gemini-3.1-pro-preview-customtools",
706        "Gemini 3.1 Pro Preview Custom Tools",
707        "Gemini preview variant exposed for custom tool validation and tool-heavy coding workflows.",
708        REASONING_HIGH,
709    ),
710    gemini_model(
711        PROVIDER_GEMINI,
712        "gemini-3-flash-preview",
713        "Gemini 3 Flash Preview",
714        "Fast Gemini model for everyday coding, tool use, and multimodal prompts.",
715        REASONING_MEDIUM,
716    ),
717    gemini_model(
718        PROVIDER_GEMINI,
719        "gemini-3.1-flash-lite-preview",
720        "Gemini 3.1 Flash-Lite Preview",
721        "Lightweight Gemini model for low-latency coding and agent interactions.",
722        REASONING_LOW,
723    ),
724    gemini_model(
725        PROVIDER_VERTEX,
726        "gemini-3.5-flash",
727        "Gemini 3.5 Flash",
728        "Stable Gemini Flash model on Vertex AI for agentic coding, tool use, and long-horizon workflows.",
729        REASONING_MEDIUM,
730    ),
731    gemini_model(
732        PROVIDER_VERTEX,
733        "gemini-3.1-pro-preview",
734        "Gemini 3.1 Pro Preview",
735        "Gemini model on Vertex AI for complex coding, long context, and tool-heavy agent workflows.",
736        REASONING_HIGH,
737    ),
738    gemini_model(
739        PROVIDER_VERTEX,
740        "gemini-3-flash-preview",
741        "Gemini 3 Flash Preview",
742        "Fast Gemini model on Vertex AI for everyday coding, tool use, and multimodal prompts.",
743        REASONING_MEDIUM,
744    ),
745    gemini_model(
746        PROVIDER_VERTEX,
747        "gemini-3.1-flash-lite-preview",
748        "Gemini 3.1 Flash-Lite Preview",
749        "Lightweight Gemini model on Vertex AI for low-latency coding and agent interactions.",
750        REASONING_LOW,
751    ),
752    xai_model(
753        PROVIDER_XAI,
754        "grok-4.5",
755        "Grok 4.5",
756        "xAI flagship model for coding, agentic tasks, knowledge work, and configurable reasoning.",
757        500_000,
758        REASONING_HIGH,
759        XAI_REASONING,
760        true,
761        false,
762    ),
763    xai_model(
764        PROVIDER_XAI,
765        "grok-4.3",
766        "Grok 4.3",
767        "xAI flagship model for chat, coding, tool use, and configurable reasoning.",
768        1_000_000,
769        REASONING_LOW,
770        XAI_CONFIGURABLE_REASONING,
771        true,
772        false,
773    ),
774    xai_model(
775        PROVIDER_XAI,
776        "grok-4.20-multi-agent-0309",
777        "Grok 4.20 Multi-Agent",
778        "xAI long-context model with agentic tool-calling and reasoning.",
779        2_000_000,
780        REASONING_LOW,
781        XAI_REASONING,
782        true,
783        false,
784    ),
785    xai_model(
786        PROVIDER_XAI,
787        "grok-4.20-0309-reasoning",
788        "Grok 4.20 Reasoning",
789        "xAI long-context reasoning model for complex tool-heavy workflows.",
790        2_000_000,
791        REASONING_LOW,
792        XAI_REASONING,
793        true,
794        false,
795    ),
796    xai_model(
797        PROVIDER_XAI,
798        "grok-4.20-0309-non-reasoning",
799        "Grok 4.20 Non-Reasoning",
800        "xAI long-context model for lower-latency non-reasoning workflows.",
801        2_000_000,
802        REASONING_NONE,
803        XAI_NO_REASONING,
804        true,
805        false,
806    ),
807    xai_model(
808        PROVIDER_SUPERGROK,
809        "grok-4.5",
810        "Grok 4.5",
811        "SuperGrok OAuth access to xAI Grok 4.5 for coding, agentic tasks, and knowledge work.",
812        500_000,
813        REASONING_HIGH,
814        XAI_REASONING,
815        true,
816        false,
817    ),
818    xai_model(
819        PROVIDER_SUPERGROK,
820        "grok-build-0.1",
821        "Grok Build 0.1",
822        "SuperGrok OAuth access to xAI Grok Build, optimized for agentic coding and software engineering workflows.",
823        500_000,
824        REASONING_LOW,
825        XAI_CONFIGURABLE_REASONING,
826        true,
827        false,
828    ),
829    xai_model(
830        PROVIDER_SUPERGROK,
831        "grok-composer-2.5-fast",
832        "Grok Composer 2.5 Fast",
833        "SuperGrok OAuth access to xAI Composer 2.5 Fast for lower-latency agentic coding.",
834        200_000,
835        REASONING_NONE,
836        &[],
837        false,
838        false,
839    ),
840    xai_model(
841        PROVIDER_SUPERGROK,
842        "grok-4.3",
843        "Grok 4.3",
844        "SuperGrok OAuth access to xAI Grok 4.3.",
845        1_000_000,
846        REASONING_LOW,
847        XAI_CONFIGURABLE_REASONING,
848        true,
849        true,
850    ),
851    xai_model(
852        PROVIDER_SUPERGROK,
853        "grok-4.20-multi-agent-0309",
854        "Grok 4.20 Multi-Agent",
855        "SuperGrok OAuth access to xAI's long-context multi-agent model.",
856        2_000_000,
857        REASONING_LOW,
858        XAI_REASONING,
859        true,
860        true,
861    ),
862    xai_model(
863        PROVIDER_SUPERGROK,
864        "grok-4.20-0309-reasoning",
865        "Grok 4.20 Reasoning",
866        "SuperGrok OAuth access to xAI's long-context reasoning model.",
867        2_000_000,
868        REASONING_LOW,
869        XAI_REASONING,
870        true,
871        true,
872    ),
873    xai_model(
874        PROVIDER_SUPERGROK,
875        "grok-4.20-0309-non-reasoning",
876        "Grok 4.20 Non-Reasoning",
877        "SuperGrok OAuth access to xAI's long-context non-reasoning model.",
878        2_000_000,
879        REASONING_NONE,
880        XAI_NO_REASONING,
881        true,
882        true,
883    ),
884    opencode_model(
885        PROVIDER_OPENCODE,
886        "gpt-5.5",
887        "GPT 5.5",
888        "OpenCode Zen GPT 5.5 gateway model.",
889        1_050_000,
890        REASONING_MEDIUM,
891        STANDARD_REASONING,
892    ),
893    opencode_model(
894        PROVIDER_OPENCODE,
895        "gpt-5.3-codex-spark",
896        "GPT 5.3 Codex Spark",
897        "OpenCode Zen low-latency Codex model.",
898        128_000,
899        REASONING_HIGH,
900        STANDARD_REASONING,
901    ),
902    opencode_model(
903        PROVIDER_OPENCODE,
904        "big-pickle",
905        "Big Pickle",
906        "OpenCode Zen free coding model.",
907        256_000,
908        REASONING_NONE,
909        &[],
910    ),
911    opencode_model(
912        PROVIDER_OPENCODE,
913        "deepseek-v4-flash-free",
914        "DeepSeek V4 Flash Free",
915        "OpenCode Zen free DeepSeek coding model.",
916        128_000,
917        REASONING_NONE,
918        &[],
919    ),
920    opencode_model(
921        PROVIDER_OPENCODE,
922        "minimax-m2.5-free",
923        "MiniMax M2.5 Free",
924        "OpenCode Zen free MiniMax coding model.",
925        256_000,
926        REASONING_NONE,
927        &[],
928    ),
929    opencode_model(
930        PROVIDER_OPENCODE,
931        "nemotron-3-super-free",
932        "Nemotron 3 Super Free",
933        "OpenCode Zen free Nemotron coding model.",
934        128_000,
935        REASONING_NONE,
936        &[],
937    ),
938    opencode_model(
939        PROVIDER_OPENCODE_GO,
940        "kimi-k2.6",
941        "Kimi K2.6",
942        "OpenCode Go Kimi coding model.",
943        256_000,
944        REASONING_NONE,
945        &[],
946    ),
947    opencode_model(
948        PROVIDER_OPENCODE_GO,
949        "qwen3.6-plus",
950        "Qwen3.6 Plus",
951        "OpenCode Go Qwen coding model.",
952        256_000,
953        REASONING_NONE,
954        &[],
955    ),
956    opencode_model(
957        PROVIDER_OPENCODE_GO,
958        "glm-5.1",
959        "GLM-5.1",
960        "OpenCode Go GLM coding model.",
961        256_000,
962        REASONING_NONE,
963        &[],
964    ),
965    opencode_model(
966        PROVIDER_OPENCODE_GO,
967        "deepseek-v4-flash",
968        "DeepSeek V4 Flash",
969        "OpenCode Go DeepSeek coding model.",
970        128_000,
971        REASONING_NONE,
972        &[],
973    ),
974    opencode_model(
975        PROVIDER_KIMI_CODE,
976        "kimi-for-coding",
977        "K2.7 Code",
978        "Kimi Code subscription coding model (OAuth via api.kimi.com/coding/v1).",
979        262_144,
980        REASONING_NONE,
981        &[],
982    ),
983    ModelCatalogEntry {
984        id: "x-ai/grok-build-0.1",
985        display_name: "Grok Build 0.1",
986        description: "OpenRouter route for xAI's fast coding model for agentic software engineering workflows.",
987        provider: PROVIDER_OPENROUTER,
988        default_reasoning: REASONING_LOW,
989        supported_reasoning: OPENROUTER_REASONING,
990        context_window: 256_000,
991        max_context_window: 256_000,
992        auto_compact_token_limit: 230_400,
993        supports_compaction: true,
994        supports_images: true,
995        supports_tools: true,
996        supports_structured: true,
997        edit_tool: Some(EDIT_TOOL_PATCH),
998        hidden: false,
999    },
1000    ModelCatalogEntry {
1001        id: "accounts/fireworks/models/qwen3-235b-a22b",
1002        display_name: "Qwen3 235B A22B",
1003        description: "Fireworks Responses-capable serverless model with client-executed function tool support.",
1004        provider: PROVIDER_FIREWORKS,
1005        default_reasoning: REASONING_NONE,
1006        supported_reasoning: &[],
1007        context_window: 131_072,
1008        max_context_window: 131_072,
1009        auto_compact_token_limit: 0,
1010        supports_compaction: false,
1011        supports_images: false,
1012        supports_tools: true,
1013        supports_structured: true,
1014        edit_tool: Some(EDIT_TOOL_PATCH),
1015        hidden: false,
1016    },
1017    roder_cloud_model(
1018        "roder.cloud/free",
1019        "Roder Free",
1020        "Free hosted model on roder.cloud.",
1021        32_768,
1022    ),
1023    roder_cloud_model(
1024        "roder.cloud/openai/gpt-5.5",
1025        "GPT-5.5 (Roder Cloud)",
1026        "roder.cloud hosted route for OpenAI GPT-5.5.",
1027        400_000,
1028    ),
1029    roder_cloud_model(
1030        "roder.cloud/anthropic/claude-opus-4-7",
1031        "Claude Opus 4.7 (Roder Cloud)",
1032        "roder.cloud hosted route for Anthropic Claude Opus 4.7.",
1033        200_000,
1034    ),
1035    roder_cloud_model(
1036        "roder.cloud/google/gemini-3.1-pro-preview",
1037        "Gemini 3.1 Pro (Roder Cloud)",
1038        "roder.cloud hosted route for Google Gemini 3.1 Pro Preview.",
1039        200_000,
1040    ),
1041    poolside_model(
1042        "poolside/laguna-m.1",
1043        "Laguna M.1",
1044        "Poolside flagship agentic coding model.",
1045        REASONING_MEDIUM,
1046    ),
1047    poolside_model(
1048        "poolside/laguna-xs.2",
1049        "Laguna XS.2",
1050        "Poolside lightweight agentic coding model.",
1051        REASONING_MEDIUM,
1052    ),
1053    xiaomi_mimo::PAYG_V25_PRO,
1054    xiaomi_mimo::PAYG_V2_PRO,
1055    xiaomi_mimo::PAYG_V25,
1056    xiaomi_mimo::PAYG_V2_OMNI,
1057    xiaomi_mimo::PAYG_V2_FLASH,
1058    xiaomi_mimo::TOKEN_PLAN_V25_PRO,
1059    xiaomi_mimo::TOKEN_PLAN_V2_PRO,
1060    xiaomi_mimo::TOKEN_PLAN_V25,
1061    xiaomi_mimo::TOKEN_PLAN_V2_OMNI,
1062    xiaomi_mimo::TOKEN_PLAN_V2_FLASH,
1063    synthetic::SYN_LARGE_TEXT,
1064    synthetic::SYN_SMALL_TEXT,
1065    synthetic::SYN_LARGE_VISION,
1066    synthetic::SYN_SMALL_VISION,
1067    synthetic::HF_MINIMAX_M3,
1068    synthetic::HF_QWEN3_6_27B,
1069    synthetic::HF_KIMI_K2_6,
1070    synthetic::HF_NEMOTRON_3_SUPER,
1071    synthetic::HF_GLM_4_7,
1072    synthetic::HF_GLM_4_7_FLASH,
1073    synthetic::HF_GLM_5_1,
1074    synthetic::HF_GLM_5_2,
1075    synthetic::HF_GPT_OSS_120B,
1076    synthetic::HF_QWEN3_5_397B_A17B,
1077    ModelCatalogEntry {
1078        id: "composer-2.5",
1079        display_name: "Composer 2.5",
1080        description: "Cursor Composer model exposed through direct AgentService inference.",
1081        provider: PROVIDER_CURSOR,
1082        default_reasoning: REASONING_NONE,
1083        supported_reasoning: &[],
1084        context_window: 200_000,
1085        max_context_window: 200_000,
1086        auto_compact_token_limit: 180_000,
1087        supports_compaction: true,
1088        supports_images: false,
1089        supports_tools: false,
1090        supports_structured: false,
1091        edit_tool: None,
1092        hidden: false,
1093    },
1094    cursor_model(
1095        "composer-2.5-fast",
1096        "Composer 2.5 Fast",
1097        "Cursor Composer 2.5 fast variant for lower-latency agent turns.",
1098        200_000,
1099        180_000,
1100        REASONING_NONE,
1101        &[],
1102    ),
1103    cursor_model(
1104        "claude-fable-5",
1105        "Claude Fable 5",
1106        "Anthropic Claude Fable 5, Anthropic's most powerful frontier model, routed through Cursor's AgentService.",
1107        1_000_000,
1108        900_000,
1109        REASONING_HIGH,
1110        OPUS_REASONING,
1111    ),
1112    cursor_model(
1113        "claude-opus-4-8",
1114        "Claude Opus 4.8",
1115        "Anthropic Claude Opus 4.8 routed through Cursor's AgentService.",
1116        1_000_000,
1117        900_000,
1118        REASONING_HIGH,
1119        OPUS_REASONING,
1120    ),
1121    cursor_model(
1122        "claude-sonnet-4-6",
1123        "Claude Sonnet 4.6",
1124        "Anthropic Claude Sonnet 4.6 routed through Cursor's AgentService.",
1125        1_000_000,
1126        900_000,
1127        REASONING_NONE,
1128        &[],
1129    ),
1130    cursor_model(
1131        "gpt-5.5",
1132        "GPT-5.5",
1133        "OpenAI GPT-5.5 routed through Cursor's AgentService.",
1134        1_050_000,
1135        945_000,
1136        REASONING_MEDIUM,
1137        STANDARD_REASONING,
1138    ),
1139    cursor_model(
1140        "gpt-5.5-fast",
1141        "GPT-5.5 Fast",
1142        "OpenAI GPT-5.5 fast variant routed through Cursor's AgentService.",
1143        1_050_000,
1144        945_000,
1145        REASONING_MEDIUM,
1146        STANDARD_REASONING,
1147    ),
1148    cursor_model(
1149        "gemini-3.1-pro-preview",
1150        "Gemini 3.1 Pro",
1151        "Google Gemini 3.1 Pro routed through Cursor's AgentService.",
1152        1_048_576,
1153        943_718,
1154        REASONING_MEDIUM,
1155        GEMINI_REASONING,
1156    ),
1157    cursor_model(
1158        "grok-4.3",
1159        "Grok 4.3",
1160        "xAI Grok 4.3 routed through Cursor's AgentService.",
1161        1_000_000,
1162        900_000,
1163        REASONING_MEDIUM,
1164        STANDARD_REASONING,
1165    ),
1166    ModelCatalogEntry {
1167        id: "text-embedding-3-large",
1168        display_name: "Text Embedding 3 Large",
1169        description: "OpenAI embedding model for local semantic memories.",
1170        provider: PROVIDER_OPENAI,
1171        default_reasoning: REASONING_NONE,
1172        supported_reasoning: &[],
1173        context_window: 0,
1174        max_context_window: 0,
1175        auto_compact_token_limit: 0,
1176        supports_compaction: false,
1177        supports_images: false,
1178        supports_tools: true,
1179        supports_structured: false,
1180        edit_tool: None,
1181        hidden: true,
1182    },
1183    ModelCatalogEntry {
1184        id: "gemini-embedding-2",
1185        display_name: "Gemini Embedding 2",
1186        description: "Google Gemini embedding model for local semantic memories.",
1187        provider: PROVIDER_GOOGLE,
1188        default_reasoning: REASONING_NONE,
1189        supported_reasoning: &[],
1190        context_window: 0,
1191        max_context_window: 0,
1192        auto_compact_token_limit: 0,
1193        supports_compaction: false,
1194        supports_images: false,
1195        supports_tools: false,
1196        supports_structured: false,
1197        edit_tool: None,
1198        hidden: true,
1199    },
1200    ModelCatalogEntry {
1201        id: "zembed-1",
1202        display_name: "ZeroEntropy zembed-1",
1203        description: "ZeroEntropy embedding model for local semantic memories.",
1204        provider: PROVIDER_ZEROENTROPY,
1205        default_reasoning: REASONING_NONE,
1206        supported_reasoning: &[],
1207        context_window: 0,
1208        max_context_window: 0,
1209        auto_compact_token_limit: 0,
1210        supports_compaction: false,
1211        supports_images: false,
1212        supports_tools: false,
1213        supports_structured: false,
1214        edit_tool: None,
1215        hidden: true,
1216    },
1217    ModelCatalogEntry {
1218        id: "mock",
1219        display_name: "Mock",
1220        description: "Local deterministic mock provider for tests and offline development.",
1221        provider: PROVIDER_MOCK,
1222        default_reasoning: REASONING_NONE,
1223        supported_reasoning: MOCK_REASONING,
1224        context_window: 128_000,
1225        max_context_window: 128_000,
1226        auto_compact_token_limit: 115_200,
1227        supports_compaction: false,
1228        supports_images: false,
1229        supports_tools: true,
1230        supports_structured: false,
1231        edit_tool: None,
1232        hidden: true,
1233    },
1234];
1235
1236const fn openai_model(
1237    id: &'static str,
1238    display_name: &'static str,
1239    description: &'static str,
1240    context_window: u32,
1241    auto_compact_token_limit: u32,
1242    supports_compaction: bool,
1243    supported_reasoning: &'static [ReasoningOption],
1244) -> ModelCatalogEntry {
1245    ModelCatalogEntry {
1246        id,
1247        display_name,
1248        description,
1249        provider: PROVIDER_OPENAI,
1250        default_reasoning: REASONING_MEDIUM,
1251        supported_reasoning,
1252        context_window,
1253        max_context_window: context_window,
1254        auto_compact_token_limit,
1255        supports_compaction,
1256        supports_images: false,
1257        supports_tools: true,
1258        supports_structured: false,
1259        edit_tool: Some("patch"),
1260        hidden: false,
1261    }
1262}
1263
1264#[allow(clippy::too_many_arguments)]
1265const fn anthropic_model(
1266    id: &'static str,
1267    display_name: &'static str,
1268    description: &'static str,
1269    context_window: u32,
1270    auto_compact_token_limit: u32,
1271    default_reasoning: &'static str,
1272    supported_reasoning: &'static [ReasoningOption],
1273    // The direct Anthropic API supports native server-side compaction
1274    // (`context_management` with a `compact_20260112` edit) on the 1M-context
1275    // models. Pass `true` there so Roder forwards `auto_compact_token_limit`
1276    // as the input-token trigger and defers to the server instead of
1277    // compacting the transcript client-side, which is what prevents 1M
1278    // sessions ending in "Prompt is too long". Not every model accepts the
1279    // edit: the API rejects every request carrying it for Haiku 4.5 ("does
1280    // not support the 'compact_20260112' context management strategy"), so
1281    // such models must pass `false` and rely on Roder's client-side
1282    // compaction at `auto_compact_token_limit`.
1283    supports_compaction: bool,
1284) -> ModelCatalogEntry {
1285    ModelCatalogEntry {
1286        id,
1287        display_name,
1288        description,
1289        provider: PROVIDER_ANTHROPIC,
1290        default_reasoning,
1291        supported_reasoning,
1292        context_window,
1293        max_context_window: context_window,
1294        auto_compact_token_limit,
1295        supports_compaction,
1296        supports_images: false,
1297        supports_tools: true,
1298        supports_structured: false,
1299        edit_tool: Some("edit"),
1300        hidden: false,
1301    }
1302}
1303
1304const fn claude_code_model(
1305    id: &'static str,
1306    display_name: &'static str,
1307    description: &'static str,
1308    context_window: u32,
1309    auto_compact_token_limit: u32,
1310    default_reasoning: &'static str,
1311    supported_reasoning: &'static [ReasoningOption],
1312) -> ModelCatalogEntry {
1313    ModelCatalogEntry {
1314        id,
1315        display_name,
1316        description,
1317        provider: PROVIDER_CLAUDE_CODE,
1318        default_reasoning,
1319        supported_reasoning,
1320        context_window,
1321        max_context_window: context_window,
1322        auto_compact_token_limit,
1323        // The Claude Code provider re-sends the full Roder transcript every turn
1324        // and does not reuse CLI sessions, so there is no server-side compaction
1325        // to rely on. Keep this `false` so Roder proactively compacts the
1326        // transcript on the fly at `auto_compact_token_limit` instead of waiting
1327        // for the full context window (which overflows into "Prompt too long").
1328        supports_compaction: false,
1329        supports_images: true,
1330        supports_tools: true,
1331        supports_structured: false,
1332        edit_tool: Some(EDIT_TOOL_EDIT),
1333        hidden: false,
1334    }
1335}
1336
1337const fn gemini_model(
1338    provider: &'static str,
1339    id: &'static str,
1340    display_name: &'static str,
1341    description: &'static str,
1342    default_reasoning: &'static str,
1343) -> ModelCatalogEntry {
1344    ModelCatalogEntry {
1345        id,
1346        display_name,
1347        description,
1348        provider,
1349        default_reasoning,
1350        supported_reasoning: GEMINI_REASONING,
1351        context_window: 1_048_576,
1352        max_context_window: 1_048_576,
1353        auto_compact_token_limit: 943_718,
1354        supports_compaction: false,
1355        supports_images: true,
1356        supports_tools: true,
1357        supports_structured: true,
1358        edit_tool: Some("edit"),
1359        hidden: false,
1360    }
1361}
1362
1363const fn xai_model(
1364    provider: &'static str,
1365    id: &'static str,
1366    display_name: &'static str,
1367    description: &'static str,
1368    context_window: u32,
1369    default_reasoning: &'static str,
1370    supported_reasoning: &'static [ReasoningOption],
1371    supports_images: bool,
1372    hidden: bool,
1373) -> ModelCatalogEntry {
1374    ModelCatalogEntry {
1375        id,
1376        display_name,
1377        description,
1378        provider,
1379        default_reasoning,
1380        supported_reasoning,
1381        context_window,
1382        max_context_window: context_window,
1383        auto_compact_token_limit: context_window.saturating_mul(9) / 10,
1384        supports_compaction: false,
1385        supports_images,
1386        supports_tools: true,
1387        supports_structured: true,
1388        edit_tool: Some("edit"),
1389        hidden,
1390    }
1391}
1392
1393const fn opencode_model(
1394    provider: &'static str,
1395    id: &'static str,
1396    display_name: &'static str,
1397    description: &'static str,
1398    context_window: u32,
1399    default_reasoning: &'static str,
1400    supported_reasoning: &'static [ReasoningOption],
1401) -> ModelCatalogEntry {
1402    ModelCatalogEntry {
1403        id,
1404        display_name,
1405        description,
1406        provider,
1407        default_reasoning,
1408        supported_reasoning,
1409        context_window,
1410        max_context_window: context_window,
1411        auto_compact_token_limit: context_window.saturating_mul(9) / 10,
1412        supports_compaction: false,
1413        supports_images: false,
1414        supports_tools: true,
1415        supports_structured: true,
1416        edit_tool: Some("edit"),
1417        hidden: false,
1418    }
1419}
1420
1421const fn roder_cloud_model(
1422    id: &'static str,
1423    display_name: &'static str,
1424    description: &'static str,
1425    context_window: u32,
1426) -> ModelCatalogEntry {
1427    ModelCatalogEntry {
1428        id,
1429        display_name,
1430        description,
1431        provider: PROVIDER_RODER_CLOUD,
1432        default_reasoning: REASONING_NONE,
1433        supported_reasoning: RODER_CLOUD_REASONING,
1434        context_window,
1435        max_context_window: context_window,
1436        auto_compact_token_limit: 0,
1437        supports_compaction: false,
1438        supports_images: false,
1439        supports_tools: false,
1440        supports_structured: false,
1441        edit_tool: None,
1442        hidden: false,
1443    }
1444}
1445
1446const fn poolside_model(
1447    id: &'static str,
1448    display_name: &'static str,
1449    description: &'static str,
1450    default_reasoning: &'static str,
1451) -> ModelCatalogEntry {
1452    ModelCatalogEntry {
1453        id,
1454        display_name,
1455        description,
1456        provider: PROVIDER_POOLSIDE,
1457        default_reasoning,
1458        supported_reasoning: POOLSIDE_REASONING,
1459        context_window: 131_072,
1460        max_context_window: 131_072,
1461        auto_compact_token_limit: 117_964,
1462        supports_compaction: false,
1463        supports_images: false,
1464        supports_tools: true,
1465        supports_structured: true,
1466        edit_tool: Some("edit"),
1467        hidden: false,
1468    }
1469}
1470
1471const fn cursor_model(
1472    id: &'static str,
1473    display_name: &'static str,
1474    description: &'static str,
1475    context_window: u32,
1476    auto_compact_token_limit: u32,
1477    default_reasoning: &'static str,
1478    supported_reasoning: &'static [ReasoningOption],
1479) -> ModelCatalogEntry {
1480    ModelCatalogEntry {
1481        id,
1482        display_name,
1483        description,
1484        provider: PROVIDER_CURSOR,
1485        default_reasoning,
1486        supported_reasoning,
1487        context_window,
1488        max_context_window: context_window,
1489        auto_compact_token_limit,
1490        supports_compaction: true,
1491        // Cursor's AgentService proxies vision-capable frontier models and
1492        // accepts inline images via `agent.v1.SelectedImage`, which the Cursor
1493        // provider now encodes.
1494        supports_images: true,
1495        supports_tools: false,
1496        supports_structured: false,
1497        edit_tool: None,
1498        hidden: false,
1499    }
1500}
1501
1502pub fn built_in_providers() -> &'static [ProviderCatalogEntry] {
1503    BUILT_IN_PROVIDERS
1504}
1505
1506pub fn built_in_models(include_hidden: bool) -> Vec<&'static ModelCatalogEntry> {
1507    BUILT_IN_MODELS
1508        .iter()
1509        .filter(|model| include_hidden || !model.hidden)
1510        .collect()
1511}
1512
1513pub fn models_for_provider(provider: &str, include_hidden: bool) -> Vec<ModelDescriptor> {
1514    built_in_models(include_hidden)
1515        .into_iter()
1516        .filter(|model| model.provider == provider)
1517        .map(ModelDescriptor::from)
1518        .collect()
1519}
1520
1521pub fn models_for_codex(include_hidden: bool) -> Vec<ModelDescriptor> {
1522    built_in_models(include_hidden)
1523        .into_iter()
1524        .filter(|model| model.provider == PROVIDER_OPENAI || model.provider == PROVIDER_CODEX)
1525        .map(ModelDescriptor::from)
1526        .collect()
1527}
1528
1529pub fn lookup_model(id: &str) -> Option<&'static ModelCatalogEntry> {
1530    BUILT_IN_MODELS.iter().find(|model| model.id == id)
1531}
1532
1533/// Resolve a catalog entry preferring an exact `(provider, id)` match.
1534///
1535/// Several model ids are shared across providers (for example `gpt-5.5` is
1536/// offered by both OpenAI and Cursor). [`lookup_model`] returns the first entry
1537/// by id, which silently resolves cross-provider ids to the wrong provider's
1538/// metadata. When the active provider is known, prefer this function so that,
1539/// e.g., `cursor/claude-opus-4-8` resolves to the Cursor catalog entry rather
1540/// than the Anthropic one. Falls back to id-only lookup so provider aliases and
1541/// user-defined models keep working.
1542pub fn lookup_model_for_provider(provider: &str, id: &str) -> Option<&'static ModelCatalogEntry> {
1543    BUILT_IN_MODELS
1544        .iter()
1545        .find(|model| model.provider == provider && model.id == id)
1546        .or_else(|| lookup_model(id))
1547}
1548
1549pub fn built_in_model_profile(id: &str) -> Option<ModelHarnessProfile> {
1550    lookup_model(id).map(model_harness_profile_from_catalog)
1551}
1552
1553/// Provider-aware variant of [`built_in_model_profile`].
1554///
1555/// Resolves the harness profile (provider family, instruction overlay, schema
1556/// policy, edit tool) using the active provider so cross-provider model ids
1557/// pick up the correct family instead of the first id match.
1558pub fn built_in_model_profile_for_provider(
1559    provider: &str,
1560    id: &str,
1561) -> Option<ModelHarnessProfile> {
1562    lookup_model_for_provider(provider, id).map(model_harness_profile_from_catalog)
1563}
1564
1565pub fn built_in_model_profiles() -> Vec<ModelHarnessProfile> {
1566    built_in_models(true)
1567        .into_iter()
1568        .map(model_harness_profile_from_catalog)
1569        .collect()
1570}
1571
1572fn model_harness_profile_from_catalog(model: &ModelCatalogEntry) -> ModelHarnessProfile {
1573    let provider_family = provider_family_for_provider(model.provider);
1574    ModelHarnessProfile {
1575        model: model.id.to_string(),
1576        provider: model.provider.to_string(),
1577        provider_family,
1578        edit_tool: model.edit_tool.map(str::to_string),
1579        schema_policy: schema_policy_for_family(provider_family),
1580        instruction_overlay: instruction_overlay_for_family(provider_family),
1581        reasoning: ModelProfileReasoning {
1582            orientation: Some(model.default_reasoning.to_string()),
1583            execution: Some(default_execution_reasoning(model)),
1584            verification: Some(model.default_reasoning.to_string()),
1585            recovery: Some(model.default_reasoning.to_string()),
1586        },
1587        parallel_tool_calls: Some(
1588            model.supports_tools
1589                && matches!(
1590                    provider_family,
1591                    ProviderFamily::OpenAi | ProviderFamily::Xai | ProviderFamily::Opencode
1592                ),
1593        ),
1594        auto_compact_token_limit: (model.auto_compact_token_limit > 0)
1595            .then_some(model.auto_compact_token_limit),
1596    }
1597}
1598
1599pub fn provider_family_for_provider(provider: &str) -> ProviderFamily {
1600    match provider {
1601        PROVIDER_OPENAI | PROVIDER_CODEX => ProviderFamily::OpenAi,
1602        PROVIDER_ANTHROPIC | PROVIDER_CLAUDE_CODE => ProviderFamily::Anthropic,
1603        PROVIDER_GEMINI | PROVIDER_VERTEX => ProviderFamily::Gemini,
1604        PROVIDER_XAI | PROVIDER_SUPERGROK => ProviderFamily::Xai,
1605        PROVIDER_OPENCODE | PROVIDER_OPENCODE_GO => ProviderFamily::Opencode,
1606        PROVIDER_OPENROUTER | PROVIDER_FIREWORKS | PROVIDER_RODER_CLOUD => ProviderFamily::OpenAi,
1607        PROVIDER_POOLSIDE => ProviderFamily::Poolside,
1608        PROVIDER_CURSOR => ProviderFamily::Cursor,
1609        PROVIDER_XIAOMI_MIMO | PROVIDER_XIAOMI_MIMO_TOKEN_PLAN => ProviderFamily::OpenAi,
1610        PROVIDER_KIMI_CODE => ProviderFamily::OpenAi,
1611        PROVIDER_SYNTHETIC => ProviderFamily::OpenAi,
1612        _ => ProviderFamily::Mock,
1613    }
1614}
1615
1616fn schema_policy_for_family(family: ProviderFamily) -> ModelSchemaPolicy {
1617    match family {
1618        ProviderFamily::OpenAi => ModelSchemaPolicy::RequiredFirstFlat,
1619        _ => ModelSchemaPolicy::StandardRequiredFirst,
1620    }
1621}
1622
1623fn instruction_overlay_for_family(family: ProviderFamily) -> ModelInstructionOverlay {
1624    match family {
1625        ProviderFamily::OpenAi => ModelInstructionOverlay::LiteralToolOutputs,
1626        ProviderFamily::Anthropic | ProviderFamily::Gemini => {
1627            ModelInstructionOverlay::IntuitiveContext
1628        }
1629        _ => ModelInstructionOverlay::Standard,
1630    }
1631}
1632
1633fn default_execution_reasoning(model: &ModelCatalogEntry) -> String {
1634    if model
1635        .supported_reasoning
1636        .iter()
1637        .any(|option| option.effort == REASONING_LOW)
1638    {
1639        REASONING_LOW.to_string()
1640    } else {
1641        model.default_reasoning.to_string()
1642    }
1643}
1644
1645pub fn model_supports_reasoning_effort(model: &str, effort: &str) -> bool {
1646    lookup_model(model)
1647        .map(|entry| {
1648            entry
1649                .supported_reasoning
1650                .iter()
1651                .any(|option| option.effort == effort)
1652        })
1653        .unwrap_or(false)
1654}
1655
1656pub fn normalize_provider_id(provider: &str) -> String {
1657    match provider.trim().to_ascii_lowercase().as_str() {
1658        "grok" | "x-ai" | "x.ai" => PROVIDER_XAI.to_string(),
1659        "grok-oauth" | "xai-oauth" | "x-ai-oauth" | "xai-grok-oauth" => {
1660            PROVIDER_SUPERGROK.to_string()
1661        }
1662        "opencode" => PROVIDER_OPENCODE.to_string(),
1663        "go" | "opencode_go" | "opencode-go" => PROVIDER_OPENCODE_GO.to_string(),
1664        "openrouter" => PROVIDER_OPENROUTER.to_string(),
1665        "fireworks" | "fireworks-ai" | "fireworks_ai" => PROVIDER_FIREWORKS.to_string(),
1666        "roder-cloud" | "roder_cloud" | "rodercloud" | "roder.cloud" => {
1667            PROVIDER_RODER_CLOUD.to_string()
1668        }
1669        "laguna" | "poolside" => PROVIDER_POOLSIDE.to_string(),
1670        "composer" | "cursor-composer" => PROVIDER_CURSOR.to_string(),
1671        "claude_code" | "claudecode" => PROVIDER_CLAUDE_CODE.to_string(),
1672        "kimi" | "kimi-code" | "kimi_code" | "moonshot" => PROVIDER_KIMI_CODE.to_string(),
1673        "synthetic" | "synthetic-ai" | "synthetic_ai" | "synthetic.new" => {
1674            PROVIDER_SYNTHETIC.to_string()
1675        }
1676        provider => provider.to_string(),
1677    }
1678}
1679
1680impl From<&ModelCatalogEntry> for ModelDescriptor {
1681    fn from(model: &ModelCatalogEntry) -> Self {
1682        let supported_reasoning = model
1683            .supported_reasoning
1684            .iter()
1685            .map(|option| ReasoningEffortDescriptor {
1686                effort: option.effort.to_string(),
1687                description: option.description.to_string(),
1688            })
1689            .collect::<Vec<_>>();
1690        Self {
1691            id: model.id.to_string(),
1692            name: model.display_name.to_string(),
1693            context_window: (model.context_window > 0).then_some(model.context_window),
1694            default_reasoning: (!supported_reasoning.is_empty())
1695                .then(|| model.default_reasoning.to_string()),
1696            supported_reasoning,
1697        }
1698    }
1699}
1700
1701#[cfg(test)]
1702mod tests {
1703    use super::*;
1704
1705    #[test]
1706    fn catalog_contains_gode_providers() {
1707        let ids = BUILT_IN_PROVIDERS
1708            .iter()
1709            .map(|provider| provider.id)
1710            .collect::<Vec<_>>();
1711        assert_eq!(
1712            ids,
1713            vec![
1714                "mock",
1715                "openai",
1716                "codex",
1717                "anthropic",
1718                "claude-code",
1719                "gemini",
1720                "vertex",
1721                "xai",
1722                "supergrok",
1723                "opencode",
1724                "opencode-go",
1725                "openrouter",
1726                "fireworks",
1727                "roder-cloud",
1728                "poolside",
1729                "cursor",
1730                "xiaomi-mimo",
1731                "xiaomi-mimo-token-plan",
1732                "synthetic",
1733                "kimi-code"
1734            ]
1735        );
1736    }
1737
1738    #[test]
1739    fn gemini_provider_defaults_to_stable_35_flash() {
1740        let provider = BUILT_IN_PROVIDERS
1741            .iter()
1742            .find(|provider| provider.id == PROVIDER_GEMINI)
1743            .unwrap();
1744
1745        assert_eq!(provider.default_model, "gemini-3.5-flash");
1746
1747        let model = lookup_model("gemini-3.5-flash").unwrap();
1748        assert_eq!(model.display_name, "Gemini 3.5 Flash");
1749        assert_eq!(model.provider, PROVIDER_GEMINI);
1750        assert_eq!(model.context_window, 1_048_576);
1751        assert_eq!(model.default_reasoning, REASONING_MEDIUM);
1752        assert!(model.supports_tools);
1753        assert!(model.supports_structured);
1754        assert_eq!(
1755            model
1756                .supported_reasoning
1757                .iter()
1758                .map(|option| option.effort)
1759                .collect::<Vec<_>>(),
1760            vec![
1761                REASONING_MINIMAL,
1762                REASONING_LOW,
1763                REASONING_MEDIUM,
1764                REASONING_HIGH
1765            ]
1766        );
1767    }
1768
1769    #[test]
1770    fn vertex_provider_mirrors_gemini_models_under_vertex_id() {
1771        let provider = BUILT_IN_PROVIDERS
1772            .iter()
1773            .find(|provider| provider.id == PROVIDER_VERTEX)
1774            .unwrap();
1775
1776        assert_eq!(provider.default_model, "gemini-3.5-flash");
1777        assert_eq!(provider.env_key, Some("GOOGLE_APPLICATION_CREDENTIALS"));
1778        assert_eq!(provider.env_aliases, &["VERTEX_CREDENTIALS_JSON"]);
1779
1780        let model = lookup_model_for_provider(PROVIDER_VERTEX, "gemini-3.5-flash").unwrap();
1781        assert_eq!(model.provider, PROVIDER_VERTEX);
1782        assert_eq!(model.context_window, 1_048_576);
1783        assert!(model.supports_tools);
1784        assert_eq!(
1785            provider_family_for_provider(PROVIDER_VERTEX),
1786            ProviderFamily::Gemini
1787        );
1788    }
1789
1790    #[test]
1791    fn catalog_contains_gode_visible_models() {
1792        let ids = built_in_models(false)
1793            .into_iter()
1794            .map(|model| model.id)
1795            .collect::<Vec<_>>();
1796        assert_eq!(
1797            ids,
1798            vec![
1799                "gpt-5.6-sol",
1800                "gpt-5.6-terra",
1801                "gpt-5.6-luna",
1802                "gpt-5.5",
1803                "gpt-5.4",
1804                "gpt-5.4-mini",
1805                "gpt-5.3-codex-spark",
1806                "claude-fable-5",
1807                "claude-opus-4-8",
1808                "claude-opus-4-7",
1809                "claude-sonnet-4-6",
1810                "claude-haiku-4-5-20251001",
1811                "fable",
1812                "sonnet",
1813                "opus",
1814                "haiku",
1815                "claude-sonnet-4-6",
1816                "claude-opus-4-8",
1817                "claude-fable-5",
1818                "gemini-3.5-flash",
1819                "gemini-3.1-pro-preview",
1820                "gemini-3.1-pro-preview-customtools",
1821                "gemini-3-flash-preview",
1822                "gemini-3.1-flash-lite-preview",
1823                "gemini-3.5-flash",
1824                "gemini-3.1-pro-preview",
1825                "gemini-3-flash-preview",
1826                "gemini-3.1-flash-lite-preview",
1827                "grok-4.5",
1828                "grok-4.3",
1829                "grok-4.20-multi-agent-0309",
1830                "grok-4.20-0309-reasoning",
1831                "grok-4.20-0309-non-reasoning",
1832                "grok-4.5",
1833                "grok-build-0.1",
1834                "grok-composer-2.5-fast",
1835                "gpt-5.5",
1836                "gpt-5.3-codex-spark",
1837                "big-pickle",
1838                "deepseek-v4-flash-free",
1839                "minimax-m2.5-free",
1840                "nemotron-3-super-free",
1841                "kimi-k2.6",
1842                "qwen3.6-plus",
1843                "glm-5.1",
1844                "deepseek-v4-flash",
1845                "kimi-for-coding",
1846                "x-ai/grok-build-0.1",
1847                "accounts/fireworks/models/qwen3-235b-a22b",
1848                "roder.cloud/free",
1849                "roder.cloud/openai/gpt-5.5",
1850                "roder.cloud/anthropic/claude-opus-4-7",
1851                "roder.cloud/google/gemini-3.1-pro-preview",
1852                "poolside/laguna-m.1",
1853                "poolside/laguna-xs.2",
1854                "mimo-v2.5-pro",
1855                "mimo-v2-pro",
1856                "mimo-v2.5",
1857                "mimo-v2-omni",
1858                "mimo-v2-flash",
1859                "mimo-v2.5-pro",
1860                "mimo-v2-pro",
1861                "mimo-v2.5",
1862                "mimo-v2-omni",
1863                "mimo-v2-flash",
1864                "syn:large:text",
1865                "syn:small:text",
1866                "syn:large:vision",
1867                "syn:small:vision",
1868                "hf:MiniMaxAI/MiniMax-M3",
1869                "hf:Qwen/Qwen3.6-27B",
1870                "hf:moonshotai/Kimi-K2.6",
1871                "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
1872                "hf:zai-org/GLM-4.7",
1873                "hf:zai-org/GLM-4.7-Flash",
1874                "hf:zai-org/GLM-5.1",
1875                "hf:zai-org/GLM-5.2",
1876                "hf:openai/gpt-oss-120b",
1877                "hf:Qwen/Qwen3.5-397B-A17B",
1878                "composer-2.5",
1879                "composer-2.5-fast",
1880                "claude-fable-5",
1881                "claude-opus-4-8",
1882                "claude-sonnet-4-6",
1883                "gpt-5.5",
1884                "gpt-5.5-fast",
1885                "gemini-3.1-pro-preview",
1886                "grok-4.3",
1887            ]
1888        );
1889    }
1890
1891    #[test]
1892    fn provider_model_lists_match_gode_catalog() {
1893        assert_eq!(models_for_provider(PROVIDER_OPENAI, false).len(), 6);
1894        assert_eq!(models_for_codex(false).len(), 7);
1895        assert_eq!(models_for_provider(PROVIDER_ANTHROPIC, false).len(), 5);
1896        assert_eq!(models_for_provider(PROVIDER_CLAUDE_CODE, false).len(), 7);
1897        assert_eq!(models_for_provider(PROVIDER_GEMINI, false).len(), 5);
1898        assert_eq!(models_for_provider(PROVIDER_VERTEX, false).len(), 4);
1899        assert_eq!(models_for_provider(PROVIDER_XAI, false).len(), 5);
1900        assert_eq!(models_for_provider(PROVIDER_SUPERGROK, false).len(), 3);
1901        assert_eq!(models_for_provider(PROVIDER_OPENCODE, false).len(), 6);
1902        assert_eq!(models_for_provider(PROVIDER_OPENCODE_GO, false).len(), 4);
1903        assert_eq!(models_for_provider(PROVIDER_OPENROUTER, false).len(), 1);
1904        assert_eq!(models_for_provider(PROVIDER_FIREWORKS, false).len(), 1);
1905        assert_eq!(models_for_provider(PROVIDER_RODER_CLOUD, false).len(), 4);
1906        assert_eq!(models_for_provider(PROVIDER_POOLSIDE, false).len(), 2);
1907        assert_eq!(models_for_provider(PROVIDER_CURSOR, false).len(), 9);
1908        assert_eq!(models_for_provider(PROVIDER_XIAOMI_MIMO, false).len(), 5);
1909        assert_eq!(
1910            models_for_provider(PROVIDER_XIAOMI_MIMO_TOKEN_PLAN, false).len(),
1911            5
1912        );
1913        assert_eq!(models_for_provider(PROVIDER_KIMI_CODE, false).len(), 1);
1914        assert_eq!(models_for_provider(PROVIDER_SYNTHETIC, false).len(), 14);
1915        assert_eq!(models_for_provider(PROVIDER_MOCK, true).len(), 1);
1916    }
1917
1918    #[test]
1919    fn codex_model_list_matches_current_subscription_roster() {
1920        let codex_provider = built_in_providers()
1921            .iter()
1922            .find(|provider| provider.id == PROVIDER_CODEX)
1923            .expect("codex provider");
1924        assert_eq!(codex_provider.default_model, "gpt-5.6-sol");
1925
1926        let ids = models_for_codex(false)
1927            .into_iter()
1928            .map(|model| model.id)
1929            .collect::<Vec<_>>();
1930
1931        assert_eq!(
1932            ids,
1933            vec![
1934                "gpt-5.6-sol",
1935                "gpt-5.6-terra",
1936                "gpt-5.6-luna",
1937                "gpt-5.5",
1938                "gpt-5.4",
1939                "gpt-5.4-mini",
1940                "gpt-5.3-codex-spark",
1941            ]
1942        );
1943    }
1944
1945    #[test]
1946    fn new_codex_models_match_current_subscription_metadata() {
1947        let assert_model = |id: &str,
1948                            name: &str,
1949                            description: &str,
1950                            default_reasoning: &str,
1951                            efforts: &[&str],
1952                            context_window: u32,
1953                            max_context_window: u32| {
1954            let model = lookup_model_for_provider(PROVIDER_OPENAI, id).unwrap();
1955
1956            assert_eq!(model.display_name, name, "{id} display name");
1957            assert_eq!(model.description, description, "{id} description");
1958            assert_eq!(model.provider, PROVIDER_OPENAI, "{id} provider");
1959            assert_eq!(
1960                model.default_reasoning, default_reasoning,
1961                "{id} default reasoning"
1962            );
1963            assert_eq!(
1964                model
1965                    .supported_reasoning
1966                    .iter()
1967                    .map(|option| option.effort)
1968                    .collect::<Vec<_>>(),
1969                efforts,
1970                "{id} efforts"
1971            );
1972            assert_eq!(model.context_window, context_window, "{id} context window");
1973            assert_eq!(
1974                model.max_context_window, max_context_window,
1975                "{id} max context window"
1976            );
1977            assert_eq!(
1978                model.auto_compact_token_limit,
1979                context_window.saturating_mul(9) / 10,
1980                "{id} auto compact limit"
1981            );
1982            assert!(model.supports_compaction, "{id} compaction support");
1983            assert!(model.supports_images, "{id} image support");
1984            assert!(model.supports_tools, "{id} tool support");
1985            assert!(!model.hidden, "{id} visibility");
1986        };
1987
1988        assert_model(
1989            "gpt-5.6-sol",
1990            "GPT-5.6-Sol",
1991            "Latest frontier agentic coding model.",
1992            REASONING_LOW,
1993            &[
1994                REASONING_LOW,
1995                REASONING_MEDIUM,
1996                REASONING_HIGH,
1997                REASONING_XHIGH,
1998                REASONING_MAX,
1999                REASONING_ULTRA,
2000            ],
2001            372_000,
2002            372_000,
2003        );
2004        assert_model(
2005            "gpt-5.6-terra",
2006            "GPT-5.6-Terra",
2007            "Balanced agentic coding model for everyday work.",
2008            REASONING_MEDIUM,
2009            &[
2010                REASONING_LOW,
2011                REASONING_MEDIUM,
2012                REASONING_HIGH,
2013                REASONING_XHIGH,
2014                REASONING_MAX,
2015                REASONING_ULTRA,
2016            ],
2017            372_000,
2018            372_000,
2019        );
2020        assert_model(
2021            "gpt-5.6-luna",
2022            "GPT-5.6-Luna",
2023            "Fast and affordable agentic coding model.",
2024            REASONING_MEDIUM,
2025            &[
2026                REASONING_LOW,
2027                REASONING_MEDIUM,
2028                REASONING_HIGH,
2029                REASONING_XHIGH,
2030                REASONING_MAX,
2031            ],
2032            372_000,
2033            372_000,
2034        );
2035        assert_model(
2036            "gpt-5.4",
2037            "GPT-5.4",
2038            "Strong model for everyday coding.",
2039            REASONING_MEDIUM,
2040            &[
2041                REASONING_LOW,
2042                REASONING_MEDIUM,
2043                REASONING_HIGH,
2044                REASONING_XHIGH,
2045            ],
2046            272_000,
2047            1_000_000,
2048        );
2049    }
2050
2051    #[test]
2052    fn synthetic_catalog_defaults_to_large_text_alias() {
2053        let provider = built_in_providers()
2054            .iter()
2055            .find(|provider| provider.id == PROVIDER_SYNTHETIC)
2056            .expect("synthetic provider registered");
2057        assert_eq!(provider.name, "Synthetic");
2058        assert_eq!(provider.default_model, "syn:large:text");
2059        assert_eq!(
2060            provider.base_url,
2061            Some("https://api.synthetic.new/openai/v1")
2062        );
2063        assert_eq!(provider.env_key, Some("SYNTHETIC_API_KEY"));
2064        assert!(provider.env_aliases.contains(&"RODER_SYNTHETIC_API_KEY"));
2065        assert!(!provider.supports_websockets);
2066
2067        let models = models_for_provider(PROVIDER_SYNTHETIC, false);
2068        let default = models
2069            .iter()
2070            .find(|model| model.id == provider.default_model)
2071            .expect("default synthetic model present");
2072        assert_eq!(default.name, "Synthetic Large (Text)");
2073        assert!(models.iter().any(|model| model.id == "syn:small:text"));
2074        let vision = lookup_model_for_provider(PROVIDER_SYNTHETIC, "syn:large:vision")
2075            .expect("vision alias present");
2076        assert!(vision.supports_images);
2077        assert_eq!(
2078            provider_family_for_provider(PROVIDER_SYNTHETIC),
2079            ProviderFamily::OpenAi
2080        );
2081    }
2082
2083    #[test]
2084    fn synthetic_model_ids_preserve_alias_and_hf_segments() {
2085        assert_eq!(normalize_provider_id("synthetic"), PROVIDER_SYNTHETIC);
2086        assert_eq!(normalize_provider_id("synthetic.new"), PROVIDER_SYNTHETIC);
2087        // syn: aliases keep their colon-delimited segments verbatim.
2088        let alias = lookup_model_for_provider(PROVIDER_SYNTHETIC, "syn:large:text")
2089            .expect("syn alias resolves");
2090        assert_eq!(alias.id, "syn:large:text");
2091        assert_eq!(alias.provider, PROVIDER_SYNTHETIC);
2092        // hf: concrete ids are pinned in the catalog for the always-on models
2093        // but must still keep their owner/model segments when prefixed and
2094        // parsed by the provider.
2095        let label = "synthetic/hf:zai-org/GLM-5.2";
2096        let (provider, model) = label.split_once('/').unwrap();
2097        assert_eq!(provider, PROVIDER_SYNTHETIC);
2098        assert_eq!(model, "hf:zai-org/GLM-5.2");
2099    }
2100
2101    #[test]
2102    fn synthetic_always_on_models_are_pinned_with_documented_context_windows() {
2103        let glm_5_2 = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:zai-org/GLM-5.2")
2104            .expect("GLM-5.2 pinned");
2105        assert_eq!(glm_5_2.provider, PROVIDER_SYNTHETIC);
2106        assert_eq!(glm_5_2.context_window, 524_288);
2107        assert!(!glm_5_2.supports_images);
2108
2109        let minimax = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:MiniMaxAI/MiniMax-M3")
2110            .expect("MiniMax-M3 pinned");
2111        assert_eq!(minimax.context_window, 524_288);
2112
2113        let glm_4_7 = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:zai-org/GLM-4.7")
2114            .expect("GLM-4.7 pinned");
2115        assert_eq!(glm_4_7.context_window, 202_752);
2116
2117        let gpt_oss = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:openai/gpt-oss-120b")
2118            .expect("gpt-oss-120b pinned");
2119        assert_eq!(gpt_oss.context_window, 131_072);
2120
2121        let qwen_3_5 = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:Qwen/Qwen3.5-397B-A17B")
2122            .expect("Qwen3.5 397B pinned");
2123        assert_eq!(qwen_3_5.context_window, 262_144);
2124
2125        // Every documented always-on id resolves to a catalog entry.
2126        let always_on = [
2127            "hf:MiniMaxAI/MiniMax-M3",
2128            "hf:Qwen/Qwen3.6-27B",
2129            "hf:moonshotai/Kimi-K2.6",
2130            "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
2131            "hf:zai-org/GLM-4.7",
2132            "hf:zai-org/GLM-4.7-Flash",
2133            "hf:zai-org/GLM-5.1",
2134            "hf:zai-org/GLM-5.2",
2135            "hf:openai/gpt-oss-120b",
2136            "hf:Qwen/Qwen3.5-397B-A17B",
2137        ];
2138        for id in always_on {
2139            let entry = lookup_model_for_provider(PROVIDER_SYNTHETIC, id)
2140                .unwrap_or_else(|| panic!("{id} should be pinned in the synthetic catalog"));
2141            assert_eq!(entry.provider, PROVIDER_SYNTHETIC);
2142            assert!(entry.supports_tools);
2143            assert!(entry.supports_structured);
2144        }
2145    }
2146
2147    #[test]
2148    fn claude_code_catalog_uses_long_context_windows() {
2149        let direct = lookup_model_for_provider(PROVIDER_ANTHROPIC, "claude-sonnet-4-6").unwrap();
2150        let claude_code =
2151            lookup_model_for_provider(PROVIDER_CLAUDE_CODE, "claude-sonnet-4-6").unwrap();
2152
2153        assert_eq!(direct.context_window, 1_000_000);
2154        assert_eq!(claude_code.context_window, 1_000_000);
2155        assert_eq!(claude_code.auto_compact_token_limit, 900_000);
2156        // The Claude Code provider has no server-side compaction, so Roder must
2157        // compact the transcript locally before the prompt overflows the window.
2158        assert!(!claude_code.supports_compaction);
2159        // The direct Anthropic API does support native server-side compaction,
2160        // so the threshold is forwarded to the server instead of compacting the
2161        // transcript locally.
2162        assert!(direct.supports_compaction);
2163        assert_eq!(direct.auto_compact_token_limit, 900_000);
2164    }
2165
2166    #[test]
2167    fn claude_haiku_does_not_advertise_server_side_compaction() {
2168        let haiku = lookup_model("claude-haiku-4-5-20251001").unwrap();
2169
2170        // The live API rejects every request carrying the `compact_20260112`
2171        // edit for Haiku 4.5 ("does not support the 'compact_20260112'
2172        // context management strategy"), so the entry must keep Roder on
2173        // client-side compaction at the auto-compact threshold.
2174        assert!(!haiku.supports_compaction);
2175        assert_eq!(haiku.auto_compact_token_limit, 180_000);
2176    }
2177
2178    #[test]
2179    fn google_embedding_model_is_hidden_from_chat_lists() {
2180        assert!(lookup_model("gemini-embedding-2").is_some());
2181        assert!(
2182            models_for_provider(PROVIDER_GOOGLE, false)
2183                .iter()
2184                .all(|model| model.id != "gemini-embedding-2")
2185        );
2186        let model = lookup_model("gemini-embedding-2").unwrap();
2187        assert!(model.hidden);
2188        assert!(!model.supports_tools);
2189    }
2190
2191    #[test]
2192    fn zeroentropy_embedding_model_is_hidden_from_chat_lists() {
2193        assert!(lookup_model("zembed-1").is_some());
2194        assert!(
2195            models_for_provider(PROVIDER_ZEROENTROPY, false)
2196                .iter()
2197                .all(|model| model.id != "zembed-1")
2198        );
2199        let model = lookup_model("zembed-1").unwrap();
2200        assert!(model.hidden);
2201        assert!(!model.supports_tools);
2202    }
2203
2204    #[test]
2205    fn catalog_model_profile_derives_openai_defaults() {
2206        let profile = built_in_model_profile("gpt-5.5").unwrap();
2207
2208        assert_eq!(profile.provider_family, ProviderFamily::OpenAi);
2209        assert_eq!(profile.edit_tool.as_deref(), Some(EDIT_TOOL_PATCH));
2210        assert_eq!(profile.schema_policy, ModelSchemaPolicy::RequiredFirstFlat);
2211        assert_eq!(
2212            profile.instruction_overlay,
2213            ModelInstructionOverlay::LiteralToolOutputs
2214        );
2215        assert_eq!(profile.reasoning.execution.as_deref(), Some(REASONING_LOW));
2216        assert_eq!(profile.parallel_tool_calls, Some(true));
2217    }
2218
2219    #[test]
2220    fn poolside_catalog_defaults_to_thinking_enabled() {
2221        let laguna = lookup_model("poolside/laguna-m.1").unwrap();
2222        assert_eq!(laguna.default_reasoning, REASONING_MEDIUM);
2223        assert_eq!(
2224            laguna
2225                .supported_reasoning
2226                .iter()
2227                .map(|option| option.effort)
2228                .collect::<Vec<_>>(),
2229            vec![REASONING_NONE, REASONING_MEDIUM]
2230        );
2231    }
2232
2233    #[test]
2234    fn xiaomi_mimo_catalog_uses_chat_completions_kind_and_exact_model_ids() {
2235        let provider = BUILT_IN_PROVIDERS
2236            .iter()
2237            .find(|provider| provider.id == PROVIDER_XIAOMI_MIMO)
2238            .unwrap();
2239        let token_plan = BUILT_IN_PROVIDERS
2240            .iter()
2241            .find(|provider| provider.id == PROVIDER_XIAOMI_MIMO_TOKEN_PLAN)
2242            .unwrap();
2243
2244        assert_eq!(provider.kind, PROVIDER_KIND_CHAT_COMPLETIONS);
2245        assert_eq!(token_plan.kind, PROVIDER_KIND_CHAT_COMPLETIONS);
2246        assert_eq!(provider.env_key, Some("MIMO_API_KEY"));
2247        assert_eq!(token_plan.env_key, Some("MIMO_TOKEN_PLAN_API_KEY"));
2248
2249        let ids = models_for_provider(PROVIDER_XIAOMI_MIMO, false)
2250            .into_iter()
2251            .map(|model| model.id)
2252            .collect::<Vec<_>>();
2253        assert_eq!(
2254            ids,
2255            vec![
2256                "mimo-v2.5-pro",
2257                "mimo-v2-pro",
2258                "mimo-v2.5",
2259                "mimo-v2-omni",
2260                "mimo-v2-flash"
2261            ]
2262        );
2263        assert!(lookup_model("out-of-v2-flash").is_none());
2264    }
2265
2266    #[test]
2267    fn supergrok_catalog_exposes_build_and_composer_with_expected_context_windows() {
2268        let grok45 = lookup_model_for_provider(PROVIDER_SUPERGROK, "grok-4.5").unwrap();
2269        assert_eq!(grok45.display_name, "Grok 4.5");
2270        assert_eq!(grok45.context_window, 500_000);
2271        assert_eq!(grok45.auto_compact_token_limit, 450_000);
2272        assert_eq!(grok45.default_reasoning, REASONING_HIGH);
2273
2274        let build = lookup_model_for_provider(PROVIDER_SUPERGROK, "grok-build-0.1").unwrap();
2275        assert_eq!(build.display_name, "Grok Build 0.1");
2276        assert_eq!(build.context_window, 500_000);
2277        assert_eq!(build.auto_compact_token_limit, 450_000);
2278
2279        let composer =
2280            lookup_model_for_provider(PROVIDER_SUPERGROK, "grok-composer-2.5-fast").unwrap();
2281        assert_eq!(composer.display_name, "Grok Composer 2.5 Fast");
2282        assert_eq!(composer.context_window, 200_000);
2283        assert_eq!(composer.auto_compact_token_limit, 180_000);
2284        assert!(composer.supported_reasoning.is_empty());
2285        assert!(!composer.supports_images);
2286
2287        let visible = models_for_provider(PROVIDER_SUPERGROK, false)
2288            .into_iter()
2289            .map(|model| model.id)
2290            .collect::<Vec<_>>();
2291        assert_eq!(
2292            visible,
2293            vec![
2294                "grok-4.5".to_string(),
2295                "grok-build-0.1".to_string(),
2296                "grok-composer-2.5-fast".to_string()
2297            ]
2298        );
2299    }
2300
2301    #[test]
2302    fn xai_catalog_entries_match_current_grok_contract() {
2303        let grok45 = models_for_provider(PROVIDER_XAI, false)
2304            .into_iter()
2305            .find(|model| model.id == "grok-4.5")
2306            .unwrap();
2307        assert_eq!(grok45.context_window, Some(500_000));
2308        assert_eq!(grok45.default_reasoning.as_deref(), Some(REASONING_HIGH));
2309        assert_eq!(
2310            grok45
2311                .supported_reasoning
2312                .iter()
2313                .map(|option| option.effort.as_str())
2314                .collect::<Vec<_>>(),
2315            vec![REASONING_LOW, REASONING_MEDIUM, REASONING_HIGH]
2316        );
2317
2318        let grok43 = models_for_provider(PROVIDER_XAI, false)
2319            .into_iter()
2320            .find(|model| model.id == "grok-4.3")
2321            .unwrap();
2322        assert_eq!(grok43.context_window, Some(1_000_000));
2323        assert_eq!(grok43.default_reasoning.as_deref(), Some(REASONING_LOW));
2324        assert_eq!(
2325            grok43
2326                .supported_reasoning
2327                .iter()
2328                .map(|option| option.effort.as_str())
2329                .collect::<Vec<_>>(),
2330            vec![
2331                REASONING_NONE,
2332                REASONING_LOW,
2333                REASONING_MEDIUM,
2334                REASONING_HIGH
2335            ]
2336        );
2337
2338        let grok420 = lookup_model("grok-4.20-multi-agent-0309").unwrap();
2339        assert_eq!(grok420.context_window, 2_000_000);
2340        assert_eq!(grok420.auto_compact_token_limit, 1_800_000);
2341        assert_eq!(grok420.provider, PROVIDER_XAI);
2342    }
2343
2344    #[test]
2345    fn provider_aliases_normalize_xai_and_supergrok() {
2346        assert_eq!(normalize_provider_id("grok"), PROVIDER_XAI);
2347        assert_eq!(normalize_provider_id("x.ai"), PROVIDER_XAI);
2348        assert_eq!(normalize_provider_id("x-ai"), PROVIDER_XAI);
2349        assert_eq!(normalize_provider_id("xai-oauth"), PROVIDER_SUPERGROK);
2350        assert_eq!(normalize_provider_id("grok-oauth"), PROVIDER_SUPERGROK);
2351        assert_eq!(normalize_provider_id("supergrok"), PROVIDER_SUPERGROK);
2352        assert_eq!(normalize_provider_id("laguna"), PROVIDER_POOLSIDE);
2353        assert_eq!(normalize_provider_id("composer"), PROVIDER_CURSOR);
2354    }
2355
2356    #[test]
2357    fn fireworks_catalog_preserves_account_scoped_default_model() {
2358        let provider = BUILT_IN_PROVIDERS
2359            .iter()
2360            .find(|provider| provider.id == PROVIDER_FIREWORKS)
2361            .unwrap();
2362
2363        assert_eq!(
2364            provider.default_model,
2365            "accounts/fireworks/models/qwen3-235b-a22b"
2366        );
2367        assert_eq!(provider.env_key, Some("FIREWORKS_API_KEY"));
2368        assert_eq!(provider.env_aliases, &["RODER_FIREWORKS_API_KEY"]);
2369
2370        let model = lookup_model_for_provider(PROVIDER_FIREWORKS, provider.default_model).unwrap();
2371        assert_eq!(model.provider, PROVIDER_FIREWORKS);
2372        assert!(model.supports_tools);
2373        assert!(model.supports_structured);
2374        assert_eq!(
2375            provider_family_for_provider(PROVIDER_FIREWORKS),
2376            ProviderFamily::OpenAi
2377        );
2378    }
2379
2380    #[test]
2381    fn cursor_catalog_profile_is_text_only_agentservice() {
2382        let composer = lookup_model("composer-2.5").unwrap();
2383        assert_eq!(composer.provider, PROVIDER_CURSOR);
2384        assert!(!composer.supports_tools);
2385        assert!(!composer.supports_structured);
2386
2387        let profile = built_in_model_profile("composer-2.5").unwrap();
2388        assert_eq!(profile.provider_family, ProviderFamily::Cursor);
2389        assert_eq!(profile.parallel_tool_calls, Some(false));
2390    }
2391
2392    #[test]
2393    fn provider_aware_lookup_resolves_cursor_proxied_models_to_cursor_family() {
2394        // Id-only lookup resolves shared ids to the first (native) entry.
2395        let id_only = built_in_model_profile("claude-opus-4-8").unwrap();
2396        assert_eq!(id_only.provider_family, ProviderFamily::Anthropic);
2397
2398        // Provider-aware lookup resolves to the Cursor catalog entry/family.
2399        let cursor =
2400            built_in_model_profile_for_provider(PROVIDER_CURSOR, "claude-opus-4-8").unwrap();
2401        assert_eq!(cursor.provider_family, ProviderFamily::Cursor);
2402        assert_eq!(cursor.provider, PROVIDER_CURSOR);
2403        assert_eq!(cursor.parallel_tool_calls, Some(false));
2404
2405        let anthropic =
2406            built_in_model_profile_for_provider(PROVIDER_ANTHROPIC, "claude-opus-4-8").unwrap();
2407        assert_eq!(anthropic.provider_family, ProviderFamily::Anthropic);
2408
2409        // Unknown provider falls back to id-only resolution.
2410        let fallback =
2411            built_in_model_profile_for_provider("does-not-exist", "claude-opus-4-8").unwrap();
2412        assert_eq!(fallback.provider_family, ProviderFamily::Anthropic);
2413    }
2414
2415    #[test]
2416    fn cursor_gpt55_advertises_standard_reasoning_effort() {
2417        let gpt55 = models_for_provider(PROVIDER_CURSOR, false)
2418            .into_iter()
2419            .find(|model| model.id == "gpt-5.5")
2420            .expect("cursor catalog should expose gpt-5.5");
2421
2422        assert_eq!(gpt55.default_reasoning.as_deref(), Some(REASONING_MEDIUM));
2423        assert_eq!(
2424            gpt55
2425                .supported_reasoning
2426                .iter()
2427                .map(|option| option.effort.as_str())
2428                .collect::<Vec<_>>(),
2429            vec![
2430                REASONING_LOW,
2431                REASONING_MEDIUM,
2432                REASONING_HIGH,
2433                REASONING_XHIGH
2434            ]
2435        );
2436
2437        let gpt55_fast = models_for_provider(PROVIDER_CURSOR, false)
2438            .into_iter()
2439            .find(|model| model.id == "gpt-5.5-fast")
2440            .expect("cursor catalog should expose gpt-5.5-fast");
2441        assert_eq!(
2442            gpt55_fast.default_reasoning.as_deref(),
2443            Some(REASONING_MEDIUM)
2444        );
2445        assert_eq!(gpt55_fast.supported_reasoning.len(), 4);
2446    }
2447
2448    #[test]
2449    fn cursor_opus_advertises_configurable_reasoning_effort() {
2450        let opus = models_for_provider(PROVIDER_CURSOR, false)
2451            .into_iter()
2452            .find(|model| model.id == "claude-opus-4-8")
2453            .expect("cursor catalog should expose claude-opus-4-8");
2454
2455        assert_eq!(opus.default_reasoning.as_deref(), Some(REASONING_HIGH));
2456        assert_eq!(
2457            opus.supported_reasoning
2458                .iter()
2459                .map(|option| option.effort.as_str())
2460                .collect::<Vec<_>>(),
2461            vec![
2462                REASONING_LOW,
2463                REASONING_MEDIUM,
2464                REASONING_HIGH,
2465                REASONING_XHIGH,
2466                REASONING_MAX
2467            ]
2468        );
2469
2470        // Non-Opus Cursor models remain effort-free for now.
2471        let sonnet = models_for_provider(PROVIDER_CURSOR, false)
2472            .into_iter()
2473            .find(|model| model.id == "claude-sonnet-4-6")
2474            .expect("cursor catalog should expose claude-sonnet-4-6");
2475        assert_eq!(sonnet.default_reasoning, None);
2476        assert!(sonnet.supported_reasoning.is_empty());
2477    }
2478
2479    #[test]
2480    fn claude_opus_and_sonnet_advertise_max_effort() {
2481        let efforts = |id: &str| {
2482            lookup_model(id)
2483                .unwrap()
2484                .supported_reasoning
2485                .iter()
2486                .map(|option| option.effort)
2487                .collect::<Vec<_>>()
2488        };
2489
2490        // Opus 4.7/4.8 support both xhigh and max.
2491        for id in ["claude-opus-4-8", "claude-opus-4-7"] {
2492            assert_eq!(
2493                efforts(id),
2494                vec![
2495                    REASONING_LOW,
2496                    REASONING_MEDIUM,
2497                    REASONING_HIGH,
2498                    REASONING_XHIGH,
2499                    REASONING_MAX
2500                ],
2501                "{id} effort levels"
2502            );
2503        }
2504
2505        // Sonnet 4.6 supports max but not xhigh.
2506        assert_eq!(
2507            efforts("claude-sonnet-4-6"),
2508            vec![
2509                REASONING_LOW,
2510                REASONING_MEDIUM,
2511                REASONING_HIGH,
2512                REASONING_MAX
2513            ]
2514        );
2515
2516        // max stays Anthropic-specific; shared STANDARD_REASONING models do not gain it.
2517        assert!(!efforts("gpt-5.5").contains(&REASONING_MAX));
2518    }
2519
2520    #[test]
2521    fn claude_haiku_does_not_advertise_reasoning_effort() {
2522        let haiku = lookup_model("claude-haiku-4-5-20251001").unwrap();
2523
2524        assert_eq!(haiku.default_reasoning, REASONING_NONE);
2525        assert!(haiku.supported_reasoning.is_empty());
2526
2527        let descriptor = ModelDescriptor::from(haiku);
2528        assert_eq!(descriptor.default_reasoning, None);
2529        assert!(descriptor.supported_reasoning.is_empty());
2530    }
2531
2532    #[test]
2533    fn openai_context_windows_match_current_catalog_values() {
2534        let gpt55 = lookup_model("gpt-5.5").unwrap();
2535        assert_eq!(gpt55.context_window, 1_050_000);
2536        assert_eq!(gpt55.max_context_window, 1_050_000);
2537        assert_eq!(gpt55.auto_compact_token_limit, 945_000);
2538
2539        let mini = lookup_model("gpt-5.4-mini").unwrap();
2540        assert_eq!(mini.context_window, 400_000);
2541        assert_eq!(mini.max_context_window, 400_000);
2542        assert_eq!(mini.auto_compact_token_limit, 360_000);
2543
2544        let spark = lookup_model("gpt-5.3-codex-spark").unwrap();
2545        assert_eq!(spark.provider, PROVIDER_CODEX);
2546        assert_eq!(spark.context_window, 128_000);
2547        assert_eq!(spark.max_context_window, 128_000);
2548        assert_eq!(spark.auto_compact_token_limit, 115_200);
2549    }
2550
2551    #[test]
2552    fn auto_compact_defaults_to_ninety_percent_of_context_window() {
2553        for model in BUILT_IN_MODELS {
2554            if model.context_window == 0 || model.auto_compact_token_limit == 0 {
2555                continue;
2556            }
2557            assert_eq!(
2558                model.auto_compact_token_limit,
2559                model.context_window.saturating_mul(9) / 10,
2560                "{} should compact at 90% of its context window",
2561                model.id
2562            );
2563        }
2564    }
2565}