Skip to main content

roder_api/
catalog.rs

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