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