Skip to main content

roder_api/
catalog.rs

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