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