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