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