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