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