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::QWEN_QWEN3_8_MAX_0902,
1141    openrouter::QWEN_QWEN3_8_FLASH,
1142    openrouter::Z_AI_GLM_5_3,
1143    openrouter::Z_AI_GLM_5_3_FLASH,
1144    openrouter::XIAOMI_MIMO_V2_6_PRO,
1145    openrouter::MISTRALAI_MISTRAL_LARGE_4_0,
1146    openrouter::META_MUSE_SPARK_1_3,
1147    ModelCatalogEntry {
1148        id: "accounts/fireworks/models/qwen3-235b-a22b",
1149        display_name: "Qwen3 235B A22B",
1150        description: "Fireworks Responses-capable serverless model with client-executed function tool support.",
1151        provider: PROVIDER_FIREWORKS,
1152        default_reasoning: REASONING_NONE,
1153        supported_reasoning: &[],
1154        context_window: 131_072,
1155        max_context_window: 131_072,
1156        auto_compact_token_limit: 0,
1157        supports_compaction: false,
1158        supports_images: false,
1159        supports_tools: true,
1160        supports_structured: true,
1161        edit_tool: Some(EDIT_TOOL_PATCH),
1162        hidden: false,
1163    },
1164    roder_cloud_model(
1165        "roder.cloud/free",
1166        "Roder Free",
1167        "Free hosted model on roder.cloud.",
1168        32_768,
1169    ),
1170    retired(roder_cloud_model(
1171        "roder.cloud/openai/gpt-5.5",
1172        "GPT-5.5 (Roder Cloud)",
1173        "roder.cloud hosted route for OpenAI GPT-5.5.",
1174        400_000,
1175    )),
1176    roder_cloud_model(
1177        "roder.cloud/anthropic/claude-opus-4-7",
1178        "Claude Opus 4.7 (Roder Cloud)",
1179        "roder.cloud hosted route for Anthropic Claude Opus 4.7.",
1180        200_000,
1181    ),
1182    roder_cloud_model(
1183        "roder.cloud/google/gemini-3.1-pro-preview",
1184        "Gemini 3.1 Pro (Roder Cloud)",
1185        "roder.cloud hosted route for Google Gemini 3.1 Pro Preview.",
1186        200_000,
1187    ),
1188    poolside_model(
1189        "poolside/laguna-m.1",
1190        "Laguna M.1",
1191        "Poolside flagship agentic coding model.",
1192        REASONING_MEDIUM,
1193    ),
1194    poolside_model(
1195        "poolside/laguna-xs.2",
1196        "Laguna XS.2",
1197        "Poolside lightweight agentic coding model.",
1198        REASONING_MEDIUM,
1199    ),
1200    xiaomi_mimo::PAYG_V25_PRO,
1201    xiaomi_mimo::PAYG_V2_PRO,
1202    xiaomi_mimo::PAYG_V25,
1203    xiaomi_mimo::PAYG_V2_OMNI,
1204    xiaomi_mimo::PAYG_V2_FLASH,
1205    xiaomi_mimo::TOKEN_PLAN_V25_PRO,
1206    xiaomi_mimo::TOKEN_PLAN_V2_PRO,
1207    xiaomi_mimo::TOKEN_PLAN_V25,
1208    xiaomi_mimo::TOKEN_PLAN_V2_OMNI,
1209    xiaomi_mimo::TOKEN_PLAN_V2_FLASH,
1210    synthetic::SYN_LARGE_TEXT,
1211    synthetic::SYN_SMALL_TEXT,
1212    synthetic::SYN_LARGE_VISION,
1213    synthetic::SYN_SMALL_VISION,
1214    synthetic::HF_MINIMAX_M3,
1215    synthetic::HF_QWEN3_6_27B,
1216    synthetic::HF_KIMI_K2_6,
1217    synthetic::HF_NEMOTRON_3_SUPER,
1218    synthetic::HF_GLM_4_7,
1219    synthetic::HF_GLM_4_7_FLASH,
1220    synthetic::HF_GLM_5_1,
1221    synthetic::HF_GLM_5_2,
1222    synthetic::HF_GPT_OSS_120B,
1223    synthetic::HF_QWEN3_5_397B_A17B,
1224    deepseek::DEEPSEEK_CHAT,
1225    deepseek::DEEPSEEK_REASONER,
1226    deepseek::DEEPSEEK_V4_FLASH,
1227    deepseek::DEEPSEEK_V4_PRO,
1228    ModelCatalogEntry {
1229        id: "composer-2.5",
1230        display_name: "Composer 2.5",
1231        description: "Cursor Composer model exposed through direct AgentService inference.",
1232        provider: PROVIDER_CURSOR,
1233        default_reasoning: REASONING_NONE,
1234        supported_reasoning: &[],
1235        context_window: 200_000,
1236        max_context_window: 200_000,
1237        auto_compact_token_limit: 180_000,
1238        supports_compaction: true,
1239        supports_images: false,
1240        supports_tools: false,
1241        supports_structured: false,
1242        edit_tool: None,
1243        hidden: false,
1244    },
1245    cursor_model(
1246        "composer-2.5-fast",
1247        "Composer 2.5 Fast",
1248        "Cursor Composer 2.5 fast variant for lower-latency agent turns.",
1249        200_000,
1250        180_000,
1251        REASONING_NONE,
1252        &[],
1253    ),
1254    cursor_model(
1255        "claude-fable-5",
1256        "Claude Fable 5",
1257        "Anthropic Claude Fable 5, Anthropic's most powerful frontier model, routed through Cursor's AgentService.",
1258        1_000_000,
1259        900_000,
1260        REASONING_HIGH,
1261        OPUS_REASONING,
1262    ),
1263    cursor_model(
1264        "claude-opus-4-8",
1265        "Claude Opus 4.8",
1266        "Anthropic Claude Opus 4.8 routed through Cursor's AgentService.",
1267        1_000_000,
1268        900_000,
1269        REASONING_HIGH,
1270        OPUS_REASONING,
1271    ),
1272    cursor_model(
1273        "claude-sonnet-4-6",
1274        "Claude Sonnet 4.6",
1275        "Anthropic Claude Sonnet 4.6 routed through Cursor's AgentService.",
1276        1_000_000,
1277        900_000,
1278        REASONING_MEDIUM,
1279        SONNET_REASONING,
1280    ),
1281    retired(cursor_model(
1282        "gpt-5.5",
1283        "GPT-5.5",
1284        "OpenAI GPT-5.5 routed through Cursor's AgentService.",
1285        1_050_000,
1286        945_000,
1287        REASONING_MEDIUM,
1288        STANDARD_REASONING,
1289    )),
1290    retired(cursor_model(
1291        "gpt-5.5-fast",
1292        "GPT-5.5 Fast",
1293        "OpenAI GPT-5.5 fast variant routed through Cursor's AgentService.",
1294        1_050_000,
1295        945_000,
1296        REASONING_MEDIUM,
1297        STANDARD_REASONING,
1298    )),
1299    cursor_model(
1300        "gemini-3.1-pro",
1301        "Gemini 3.1 Pro",
1302        "Google Gemini 3.1 Pro routed through Cursor's AgentService.",
1303        1_000_000,
1304        900_000,
1305        REASONING_MEDIUM,
1306        GEMINI_REASONING,
1307    ),
1308    cursor_model(
1309        "gemini-4-argon",
1310        "Gemini 4 Argon",
1311        "Google Gemini 4 Argon routed through Cursor's AgentService for complex multi-step coding.",
1312        1_000_000,
1313        900_000,
1314        REASONING_HIGH,
1315        &[
1316            ReasoningOption {
1317                effort: REASONING_LOW,
1318                description: "Low Gemini thinking",
1319            },
1320            ReasoningOption {
1321                effort: REASONING_MEDIUM,
1322                description: "Medium Gemini thinking",
1323            },
1324            ReasoningOption {
1325                effort: REASONING_HIGH,
1326                description: "High Gemini thinking",
1327            },
1328        ],
1329    ),
1330    cursor_model(
1331        "grok-4.6",
1332        "Grok 4.6",
1333        "xAI Grok 4.6 routed through Cursor's AgentService for long-running coding and knowledge-work agents.",
1334        256_000,
1335        230_400,
1336        REASONING_HIGH,
1337        STANDARD_REASONING,
1338    ),
1339    cursor_model(
1340        "gemini-3.7-flash",
1341        "Gemini 3.7 Flash",
1342        "Google Gemini 3.7 Flash routed through Cursor's AgentService for high-throughput agentic coding.",
1343        1_000_000,
1344        900_000,
1345        REASONING_HIGH,
1346        GEMINI_REASONING,
1347    ),
1348    ModelCatalogEntry {
1349        id: "text-embedding-3-large",
1350        display_name: "Text Embedding 3 Large",
1351        description: "OpenAI embedding model for local semantic memories.",
1352        provider: PROVIDER_OPENAI,
1353        default_reasoning: REASONING_NONE,
1354        supported_reasoning: &[],
1355        context_window: 0,
1356        max_context_window: 0,
1357        auto_compact_token_limit: 0,
1358        supports_compaction: false,
1359        supports_images: false,
1360        supports_tools: true,
1361        supports_structured: false,
1362        edit_tool: None,
1363        hidden: true,
1364    },
1365    ModelCatalogEntry {
1366        id: "gemini-embedding-2",
1367        display_name: "Gemini Embedding 2",
1368        description: "Google Gemini embedding model for local semantic memories.",
1369        provider: PROVIDER_GOOGLE,
1370        default_reasoning: REASONING_NONE,
1371        supported_reasoning: &[],
1372        context_window: 0,
1373        max_context_window: 0,
1374        auto_compact_token_limit: 0,
1375        supports_compaction: false,
1376        supports_images: false,
1377        supports_tools: false,
1378        supports_structured: false,
1379        edit_tool: None,
1380        hidden: true,
1381    },
1382    ModelCatalogEntry {
1383        id: "zembed-1",
1384        display_name: "ZeroEntropy zembed-1",
1385        description: "ZeroEntropy embedding model for local semantic memories.",
1386        provider: PROVIDER_ZEROENTROPY,
1387        default_reasoning: REASONING_NONE,
1388        supported_reasoning: &[],
1389        context_window: 0,
1390        max_context_window: 0,
1391        auto_compact_token_limit: 0,
1392        supports_compaction: false,
1393        supports_images: false,
1394        supports_tools: false,
1395        supports_structured: false,
1396        edit_tool: None,
1397        hidden: true,
1398    },
1399    ModelCatalogEntry {
1400        id: "mock",
1401        display_name: "Mock",
1402        description: "Local deterministic mock provider for tests and offline development.",
1403        provider: PROVIDER_MOCK,
1404        default_reasoning: REASONING_NONE,
1405        supported_reasoning: MOCK_REASONING,
1406        context_window: 128_000,
1407        max_context_window: 128_000,
1408        auto_compact_token_limit: 115_200,
1409        supports_compaction: false,
1410        supports_images: false,
1411        supports_tools: true,
1412        supports_structured: false,
1413        edit_tool: None,
1414        hidden: true,
1415    },
1416];
1417
1418const fn openai_model(
1419    id: &'static str,
1420    display_name: &'static str,
1421    description: &'static str,
1422    context_window: u32,
1423    auto_compact_token_limit: u32,
1424    supports_compaction: bool,
1425    supported_reasoning: &'static [ReasoningOption],
1426) -> ModelCatalogEntry {
1427    ModelCatalogEntry {
1428        id,
1429        display_name,
1430        description,
1431        provider: PROVIDER_OPENAI,
1432        default_reasoning: REASONING_MEDIUM,
1433        supported_reasoning,
1434        context_window,
1435        max_context_window: context_window,
1436        auto_compact_token_limit,
1437        supports_compaction,
1438        supports_images: false,
1439        supports_tools: true,
1440        supports_structured: false,
1441        edit_tool: Some("patch"),
1442        hidden: false,
1443    }
1444}
1445
1446#[allow(clippy::too_many_arguments)]
1447const fn anthropic_model(
1448    id: &'static str,
1449    display_name: &'static str,
1450    description: &'static str,
1451    context_window: u32,
1452    auto_compact_token_limit: u32,
1453    default_reasoning: &'static str,
1454    supported_reasoning: &'static [ReasoningOption],
1455    // The direct Anthropic API supports native server-side compaction
1456    // (`context_management` with a `compact_20260112` edit) on the 1M-context
1457    // models. Pass `true` there so Roder forwards `auto_compact_token_limit`
1458    // as the input-token trigger and defers to the server instead of
1459    // compacting the transcript client-side, which is what prevents 1M
1460    // sessions ending in "Prompt is too long". Not every model accepts the
1461    // edit: the API rejects every request carrying it for Haiku 4.5 ("does
1462    // not support the 'compact_20260112' context management strategy"), so
1463    // such models must pass `false` and rely on Roder's client-side
1464    // compaction at `auto_compact_token_limit`.
1465    supports_compaction: bool,
1466) -> ModelCatalogEntry {
1467    ModelCatalogEntry {
1468        id,
1469        display_name,
1470        description,
1471        provider: PROVIDER_ANTHROPIC,
1472        default_reasoning,
1473        supported_reasoning,
1474        context_window,
1475        max_context_window: context_window,
1476        auto_compact_token_limit,
1477        supports_compaction,
1478        supports_images: false,
1479        supports_tools: true,
1480        supports_structured: false,
1481        edit_tool: Some("edit"),
1482        hidden: false,
1483    }
1484}
1485
1486const fn claude_code_model(
1487    id: &'static str,
1488    display_name: &'static str,
1489    description: &'static str,
1490    context_window: u32,
1491    auto_compact_token_limit: u32,
1492    default_reasoning: &'static str,
1493    supported_reasoning: &'static [ReasoningOption],
1494) -> ModelCatalogEntry {
1495    ModelCatalogEntry {
1496        id,
1497        display_name,
1498        description,
1499        provider: PROVIDER_CLAUDE_CODE,
1500        default_reasoning,
1501        supported_reasoning,
1502        context_window,
1503        max_context_window: context_window,
1504        auto_compact_token_limit,
1505        // The Claude Code provider re-sends the full Roder transcript every turn
1506        // and does not reuse CLI sessions, so there is no server-side compaction
1507        // to rely on. Keep this `false` so Roder proactively compacts the
1508        // transcript on the fly at `auto_compact_token_limit` instead of waiting
1509        // for the full context window (which overflows into "Prompt too long").
1510        supports_compaction: false,
1511        supports_images: true,
1512        supports_tools: true,
1513        supports_structured: false,
1514        edit_tool: Some(EDIT_TOOL_EDIT),
1515        hidden: false,
1516    }
1517}
1518
1519const fn gemini_model(
1520    provider: &'static str,
1521    id: &'static str,
1522    display_name: &'static str,
1523    description: &'static str,
1524    default_reasoning: &'static str,
1525) -> ModelCatalogEntry {
1526    ModelCatalogEntry {
1527        id,
1528        display_name,
1529        description,
1530        provider,
1531        default_reasoning,
1532        supported_reasoning: GEMINI_REASONING,
1533        context_window: 1_048_576,
1534        max_context_window: 1_048_576,
1535        auto_compact_token_limit: 943_718,
1536        supports_compaction: false,
1537        supports_images: true,
1538        supports_tools: true,
1539        supports_structured: true,
1540        edit_tool: Some("edit"),
1541        hidden: false,
1542    }
1543}
1544
1545const fn xai_model(
1546    provider: &'static str,
1547    id: &'static str,
1548    display_name: &'static str,
1549    description: &'static str,
1550    context_window: u32,
1551    default_reasoning: &'static str,
1552    supported_reasoning: &'static [ReasoningOption],
1553    supports_images: bool,
1554    hidden: bool,
1555) -> ModelCatalogEntry {
1556    ModelCatalogEntry {
1557        id,
1558        display_name,
1559        description,
1560        provider,
1561        default_reasoning,
1562        supported_reasoning,
1563        context_window,
1564        max_context_window: context_window,
1565        auto_compact_token_limit: context_window.saturating_mul(9) / 10,
1566        supports_compaction: false,
1567        supports_images,
1568        supports_tools: true,
1569        supports_structured: true,
1570        edit_tool: Some("edit"),
1571        hidden,
1572    }
1573}
1574
1575const fn opencode_model(
1576    provider: &'static str,
1577    id: &'static str,
1578    display_name: &'static str,
1579    description: &'static str,
1580    context_window: u32,
1581    default_reasoning: &'static str,
1582    supported_reasoning: &'static [ReasoningOption],
1583) -> ModelCatalogEntry {
1584    ModelCatalogEntry {
1585        id,
1586        display_name,
1587        description,
1588        provider,
1589        default_reasoning,
1590        supported_reasoning,
1591        context_window,
1592        max_context_window: context_window,
1593        auto_compact_token_limit: context_window.saturating_mul(9) / 10,
1594        supports_compaction: false,
1595        supports_images: false,
1596        supports_tools: true,
1597        supports_structured: true,
1598        edit_tool: Some("edit"),
1599        hidden: false,
1600    }
1601}
1602
1603const fn roder_cloud_model(
1604    id: &'static str,
1605    display_name: &'static str,
1606    description: &'static str,
1607    context_window: u32,
1608) -> ModelCatalogEntry {
1609    ModelCatalogEntry {
1610        id,
1611        display_name,
1612        description,
1613        provider: PROVIDER_RODER_CLOUD,
1614        default_reasoning: REASONING_NONE,
1615        supported_reasoning: RODER_CLOUD_REASONING,
1616        context_window,
1617        max_context_window: context_window,
1618        auto_compact_token_limit: 0,
1619        supports_compaction: false,
1620        supports_images: false,
1621        supports_tools: false,
1622        supports_structured: false,
1623        edit_tool: None,
1624        hidden: false,
1625    }
1626}
1627
1628const fn poolside_model(
1629    id: &'static str,
1630    display_name: &'static str,
1631    description: &'static str,
1632    default_reasoning: &'static str,
1633) -> ModelCatalogEntry {
1634    ModelCatalogEntry {
1635        id,
1636        display_name,
1637        description,
1638        provider: PROVIDER_POOLSIDE,
1639        default_reasoning,
1640        supported_reasoning: POOLSIDE_REASONING,
1641        context_window: 131_072,
1642        max_context_window: 131_072,
1643        auto_compact_token_limit: 117_964,
1644        supports_compaction: false,
1645        supports_images: false,
1646        supports_tools: true,
1647        supports_structured: true,
1648        edit_tool: Some("edit"),
1649        hidden: false,
1650    }
1651}
1652
1653const fn cursor_model(
1654    id: &'static str,
1655    display_name: &'static str,
1656    description: &'static str,
1657    context_window: u32,
1658    auto_compact_token_limit: u32,
1659    default_reasoning: &'static str,
1660    supported_reasoning: &'static [ReasoningOption],
1661) -> ModelCatalogEntry {
1662    ModelCatalogEntry {
1663        id,
1664        display_name,
1665        description,
1666        provider: PROVIDER_CURSOR,
1667        default_reasoning,
1668        supported_reasoning,
1669        context_window,
1670        max_context_window: context_window,
1671        auto_compact_token_limit,
1672        supports_compaction: true,
1673        // Cursor's AgentService proxies vision-capable frontier models and
1674        // accepts inline images via `agent.v1.SelectedImage`, which the Cursor
1675        // provider now encodes.
1676        supports_images: true,
1677        supports_tools: false,
1678        supports_structured: false,
1679        edit_tool: None,
1680        hidden: false,
1681    }
1682}
1683
1684pub fn built_in_providers() -> &'static [ProviderCatalogEntry] {
1685    BUILT_IN_PROVIDERS
1686}
1687
1688pub fn built_in_models(include_hidden: bool) -> Vec<&'static ModelCatalogEntry> {
1689    BUILT_IN_MODELS
1690        .iter()
1691        .filter(|model| include_hidden || !model.hidden)
1692        .collect()
1693}
1694
1695pub fn models_for_provider(provider: &str, include_hidden: bool) -> Vec<ModelDescriptor> {
1696    built_in_models(include_hidden)
1697        .into_iter()
1698        .filter(|model| model.provider == provider)
1699        .map(ModelDescriptor::from)
1700        .collect()
1701}
1702
1703pub fn models_for_codex(include_hidden: bool) -> Vec<ModelDescriptor> {
1704    built_in_models(include_hidden)
1705        .into_iter()
1706        .filter(|model| model.provider == PROVIDER_OPENAI || model.provider == PROVIDER_CODEX)
1707        .map(ModelDescriptor::from)
1708        .collect()
1709}
1710
1711pub fn lookup_model(id: &str) -> Option<&'static ModelCatalogEntry> {
1712    BUILT_IN_MODELS.iter().find(|model| model.id == id)
1713}
1714
1715/// Resolve a catalog entry preferring an exact `(provider, id)` match.
1716///
1717/// Several model ids are shared across providers (for example `gpt-5.5` is
1718/// offered by both OpenAI and Cursor). [`lookup_model`] returns the first entry
1719/// by id, which silently resolves cross-provider ids to the wrong provider's
1720/// metadata. When the active provider is known, prefer this function so that,
1721/// e.g., `cursor/claude-opus-4-8` resolves to the Cursor catalog entry rather
1722/// than the Anthropic one. Falls back to id-only lookup so provider aliases and
1723/// user-defined models keep working.
1724pub fn lookup_model_for_provider(provider: &str, id: &str) -> Option<&'static ModelCatalogEntry> {
1725    BUILT_IN_MODELS
1726        .iter()
1727        .find(|model| model.provider == provider && model.id == id)
1728        .or_else(|| lookup_model(id))
1729}
1730
1731pub fn built_in_model_profile(id: &str) -> Option<ModelHarnessProfile> {
1732    lookup_model(id).map(model_harness_profile_from_catalog)
1733}
1734
1735/// Provider-aware variant of [`built_in_model_profile`].
1736///
1737/// Resolves the harness profile (provider family, instruction overlay, schema
1738/// policy, edit tool) using the active provider so cross-provider model ids
1739/// pick up the correct family instead of the first id match.
1740pub fn built_in_model_profile_for_provider(
1741    provider: &str,
1742    id: &str,
1743) -> Option<ModelHarnessProfile> {
1744    lookup_model_for_provider(provider, id).map(model_harness_profile_from_catalog)
1745}
1746
1747pub fn built_in_model_profiles() -> Vec<ModelHarnessProfile> {
1748    built_in_models(true)
1749        .into_iter()
1750        .map(model_harness_profile_from_catalog)
1751        .collect()
1752}
1753
1754fn model_harness_profile_from_catalog(model: &ModelCatalogEntry) -> ModelHarnessProfile {
1755    let provider_family = provider_family_for_provider(model.provider);
1756    ModelHarnessProfile {
1757        model: model.id.to_string(),
1758        provider: model.provider.to_string(),
1759        provider_family,
1760        edit_tool: model.edit_tool.map(str::to_string),
1761        schema_policy: schema_policy_for_family(provider_family),
1762        instruction_overlay: instruction_overlay_for_family(provider_family),
1763        reasoning: ModelProfileReasoning {
1764            orientation: Some(model.default_reasoning.to_string()),
1765            execution: Some(default_execution_reasoning(model)),
1766            verification: Some(model.default_reasoning.to_string()),
1767            recovery: Some(model.default_reasoning.to_string()),
1768        },
1769        parallel_tool_calls: Some(
1770            model.supports_tools
1771                && matches!(
1772                    provider_family,
1773                    ProviderFamily::OpenAi | ProviderFamily::Xai | ProviderFamily::Opencode
1774                ),
1775        ),
1776        auto_compact_token_limit: (model.auto_compact_token_limit > 0)
1777            .then_some(model.auto_compact_token_limit),
1778    }
1779}
1780
1781pub fn provider_family_for_provider(provider: &str) -> ProviderFamily {
1782    match provider {
1783        PROVIDER_OPENAI | PROVIDER_CODEX => ProviderFamily::OpenAi,
1784        PROVIDER_ANTHROPIC | PROVIDER_CLAUDE_CODE => ProviderFamily::Anthropic,
1785        PROVIDER_GEMINI | PROVIDER_VERTEX => ProviderFamily::Gemini,
1786        PROVIDER_XAI | PROVIDER_SUPERGROK => ProviderFamily::Xai,
1787        PROVIDER_OPENCODE | PROVIDER_OPENCODE_GO => ProviderFamily::Opencode,
1788        PROVIDER_OPENROUTER | PROVIDER_FIREWORKS | PROVIDER_RODER_CLOUD => ProviderFamily::OpenAi,
1789        PROVIDER_POOLSIDE => ProviderFamily::Poolside,
1790        PROVIDER_CURSOR => ProviderFamily::Cursor,
1791        PROVIDER_XIAOMI_MIMO | PROVIDER_XIAOMI_MIMO_TOKEN_PLAN => ProviderFamily::OpenAi,
1792        PROVIDER_KIMI_CODE => ProviderFamily::OpenAi,
1793        PROVIDER_SYNTHETIC => ProviderFamily::OpenAi,
1794        PROVIDER_DEEPSEEK => ProviderFamily::OpenAi,
1795        _ => ProviderFamily::Mock,
1796    }
1797}
1798
1799fn schema_policy_for_family(family: ProviderFamily) -> ModelSchemaPolicy {
1800    match family {
1801        ProviderFamily::OpenAi => ModelSchemaPolicy::RequiredFirstFlat,
1802        _ => ModelSchemaPolicy::StandardRequiredFirst,
1803    }
1804}
1805
1806fn instruction_overlay_for_family(family: ProviderFamily) -> ModelInstructionOverlay {
1807    match family {
1808        ProviderFamily::OpenAi => ModelInstructionOverlay::LiteralToolOutputs,
1809        ProviderFamily::Anthropic | ProviderFamily::Gemini => {
1810            ModelInstructionOverlay::IntuitiveContext
1811        }
1812        _ => ModelInstructionOverlay::Standard,
1813    }
1814}
1815
1816fn default_execution_reasoning(model: &ModelCatalogEntry) -> String {
1817    if model
1818        .supported_reasoning
1819        .iter()
1820        .any(|option| option.effort == REASONING_LOW)
1821    {
1822        REASONING_LOW.to_string()
1823    } else {
1824        model.default_reasoning.to_string()
1825    }
1826}
1827
1828pub fn model_supports_reasoning_effort(model: &str, effort: &str) -> bool {
1829    lookup_model(model)
1830        .map(|entry| {
1831            entry
1832                .supported_reasoning
1833                .iter()
1834                .any(|option| option.effort == effort)
1835        })
1836        .unwrap_or(false)
1837}
1838
1839pub fn normalize_provider_id(provider: &str) -> String {
1840    match provider.trim().to_ascii_lowercase().as_str() {
1841        "grok" | "x-ai" | "x.ai" => PROVIDER_XAI.to_string(),
1842        "grok-oauth" | "xai-oauth" | "x-ai-oauth" | "xai-grok-oauth" => {
1843            PROVIDER_SUPERGROK.to_string()
1844        }
1845        "opencode" => PROVIDER_OPENCODE.to_string(),
1846        "go" | "opencode_go" | "opencode-go" => PROVIDER_OPENCODE_GO.to_string(),
1847        "openrouter" => PROVIDER_OPENROUTER.to_string(),
1848        "fireworks" | "fireworks-ai" | "fireworks_ai" => PROVIDER_FIREWORKS.to_string(),
1849        "roder-cloud" | "roder_cloud" | "rodercloud" | "roder.cloud" => {
1850            PROVIDER_RODER_CLOUD.to_string()
1851        }
1852        "laguna" | "poolside" => PROVIDER_POOLSIDE.to_string(),
1853        "composer" | "cursor-composer" => PROVIDER_CURSOR.to_string(),
1854        "claude_code" | "claudecode" => PROVIDER_CLAUDE_CODE.to_string(),
1855        "kimi" | "kimi-code" | "kimi_code" | "moonshot" => PROVIDER_KIMI_CODE.to_string(),
1856        "synthetic" | "synthetic-ai" | "synthetic_ai" | "synthetic.new" => {
1857            PROVIDER_SYNTHETIC.to_string()
1858        }
1859        "deepseek" | "deepseek-platform" | "deepseek_platform" => PROVIDER_DEEPSEEK.to_string(),
1860        provider => provider.to_string(),
1861    }
1862}
1863
1864impl From<&ModelCatalogEntry> for ModelDescriptor {
1865    fn from(model: &ModelCatalogEntry) -> Self {
1866        let supported_reasoning = model
1867            .supported_reasoning
1868            .iter()
1869            .map(|option| ReasoningEffortDescriptor {
1870                effort: option.effort.to_string(),
1871                description: option.description.to_string(),
1872            })
1873            .collect::<Vec<_>>();
1874        Self {
1875            id: model.id.to_string(),
1876            name: model.display_name.to_string(),
1877            context_window: (model.context_window > 0).then_some(model.context_window),
1878            default_reasoning: (!supported_reasoning.is_empty())
1879                .then(|| model.default_reasoning.to_string()),
1880            supported_reasoning,
1881        }
1882    }
1883}
1884
1885#[cfg(test)]
1886mod tests {
1887    use super::*;
1888
1889    #[test]
1890    fn openrouter_catalog_entries_are_well_formed() {
1891        let entries = BUILT_IN_MODELS
1892            .iter()
1893            .filter(|model| model.provider == PROVIDER_OPENROUTER)
1894            .collect::<Vec<_>>();
1895        assert!(!entries.is_empty());
1896        for model in entries {
1897            assert!(
1898                model.id.contains('/'),
1899                "{} is not an OpenRouter slug",
1900                model.id
1901            );
1902            assert!(
1903                model.context_window > 0,
1904                "{} has no context window",
1905                model.id
1906            );
1907            assert_eq!(
1908                model.max_context_window, model.context_window,
1909                "{}",
1910                model.id
1911            );
1912            assert!(
1913                model.auto_compact_token_limit > 0
1914                    && model.auto_compact_token_limit < model.context_window,
1915                "{} compaction threshold must sit inside its window",
1916                model.id
1917            );
1918            // OpenRouter accepts `context_management` but never compacts, so
1919            // roder has to do it client-side at the threshold.
1920            assert!(
1921                !model.supports_compaction,
1922                "{} must compact client-side",
1923                model.id
1924            );
1925            assert!(model.supports_tools, "{} must support tools", model.id);
1926        }
1927    }
1928
1929    #[test]
1930    fn openrouter_catalog_resolves_kimi_k3_with_live_limits() {
1931        let k3 = lookup_model_for_provider(PROVIDER_OPENROUTER, "moonshotai/kimi-k3")
1932            .expect("kimi k3 is catalogued");
1933        assert_eq!(k3.context_window, 1_048_576);
1934        assert!(k3.supports_images);
1935        assert!(!k3.supported_reasoning.is_empty());
1936        assert_eq!(k3.auto_compact_token_limit, 943_718);
1937    }
1938
1939    #[test]
1940    fn catalog_contains_gode_providers() {
1941        let ids = BUILT_IN_PROVIDERS
1942            .iter()
1943            .map(|provider| provider.id)
1944            .collect::<Vec<_>>();
1945        assert_eq!(
1946            ids,
1947            vec![
1948                "mock",
1949                "openai",
1950                "codex",
1951                "anthropic",
1952                "claude-code",
1953                "gemini",
1954                "vertex",
1955                "xai",
1956                "supergrok",
1957                "opencode",
1958                "opencode-go",
1959                "openrouter",
1960                "fireworks",
1961                "roder-cloud",
1962                "poolside",
1963                "cursor",
1964                "xiaomi-mimo",
1965                "xiaomi-mimo-token-plan",
1966                "synthetic",
1967                "deepseek",
1968                "kimi-code"
1969            ]
1970        );
1971    }
1972
1973    #[test]
1974    fn gemini_provider_defaults_to_stable_35_flash() {
1975        let provider = BUILT_IN_PROVIDERS
1976            .iter()
1977            .find(|provider| provider.id == PROVIDER_GEMINI)
1978            .unwrap();
1979
1980        assert_eq!(provider.default_model, "gemini-3.5-flash");
1981
1982        let model = lookup_model("gemini-3.5-flash").unwrap();
1983        assert_eq!(model.display_name, "Gemini 3.5 Flash");
1984        assert_eq!(model.provider, PROVIDER_GEMINI);
1985        assert_eq!(model.context_window, 1_048_576);
1986        assert_eq!(model.default_reasoning, REASONING_MEDIUM);
1987        assert!(model.supports_tools);
1988        assert!(model.supports_structured);
1989        assert_eq!(
1990            model
1991                .supported_reasoning
1992                .iter()
1993                .map(|option| option.effort)
1994                .collect::<Vec<_>>(),
1995            vec![
1996                REASONING_MINIMAL,
1997                REASONING_LOW,
1998                REASONING_MEDIUM,
1999                REASONING_HIGH
2000            ]
2001        );
2002    }
2003
2004    #[test]
2005    fn vertex_provider_mirrors_gemini_models_under_vertex_id() {
2006        let provider = BUILT_IN_PROVIDERS
2007            .iter()
2008            .find(|provider| provider.id == PROVIDER_VERTEX)
2009            .unwrap();
2010
2011        assert_eq!(provider.default_model, "gemini-3.5-flash");
2012        assert_eq!(provider.env_key, Some("GOOGLE_APPLICATION_CREDENTIALS"));
2013        assert_eq!(provider.env_aliases, &["VERTEX_CREDENTIALS_JSON"]);
2014
2015        let model = lookup_model_for_provider(PROVIDER_VERTEX, "gemini-3.5-flash").unwrap();
2016        assert_eq!(model.provider, PROVIDER_VERTEX);
2017        assert_eq!(model.context_window, 1_048_576);
2018        assert!(model.supports_tools);
2019        assert_eq!(
2020            provider_family_for_provider(PROVIDER_VERTEX),
2021            ProviderFamily::Gemini
2022        );
2023    }
2024
2025    #[test]
2026    fn gemini_38_flash_is_offered_on_both_google_providers() {
2027        for provider in [PROVIDER_GEMINI, PROVIDER_VERTEX] {
2028            let model = lookup_model_for_provider(provider, "gemini-3.8-flash").unwrap();
2029            assert_eq!(model.provider, provider);
2030            assert_eq!(model.display_name, "Gemini 3.8 Flash");
2031            assert_eq!(model.context_window, 1_048_576);
2032            assert_eq!(model.auto_compact_token_limit, 943_718);
2033            assert_eq!(model.default_reasoning, REASONING_MEDIUM);
2034            assert!(model.supports_tools);
2035            assert!(model.supports_structured);
2036            assert!(model.supports_images);
2037            assert!(!model.hidden);
2038        }
2039    }
2040
2041    #[test]
2042    fn catalog_contains_gode_visible_models() {
2043        let ids = built_in_models(false)
2044            .into_iter()
2045            .map(|model| model.id)
2046            .collect::<Vec<_>>();
2047        assert_eq!(
2048            ids,
2049            vec![
2050                "gpt-6.1-terra",
2051                "gpt-6-astra",
2052                "gpt-6.1-sol",
2053                "gpt-6-luna",
2054                "claude-opus-5-5",
2055                "claude-sonnet-5",
2056                "claude-sonnet-5-5",
2057                "claude-fable-5-1",
2058                "claude-fable-5",
2059                "claude-opus-4-8",
2060                "claude-opus-4-7",
2061                "claude-sonnet-4-6",
2062                "claude-haiku-4-5-20251001",
2063                "claude-haiku-5-5",
2064                "claude-opus-5-5",
2065                "claude-sonnet-5",
2066                "claude-sonnet-5-5",
2067                "fable",
2068                "sonnet",
2069                "opus",
2070                "haiku",
2071                "claude-haiku-5-5",
2072                "claude-sonnet-4-6",
2073                "claude-opus-4-8",
2074                "claude-fable-5",
2075                "claude-fable-5-1",
2076                "gemini-3.8-flash",
2077                "gemini-3.5-flash",
2078                "gemini-3.7-flash",
2079                "gemini-3.1-pro-preview",
2080                "gemini-3.1-pro-preview-customtools",
2081                "gemini-3-flash-preview",
2082                "gemini-3.1-flash-lite-preview",
2083                "gemini-3.8-flash",
2084                "gemini-3.5-flash",
2085                "gemini-3.7-flash",
2086                "gemini-3.1-pro-preview",
2087                "gemini-3-flash-preview",
2088                "gemini-3.1-flash-lite-preview",
2089                "grok-4.7",
2090                "grok-4.6",
2091                "grok-4.3",
2092                "grok-4.20-multi-agent-0309",
2093                "grok-4.20-0309-reasoning",
2094                "grok-4.20-0309-non-reasoning",
2095                "grok-4.7",
2096                "grok-4.6",
2097                "grok-composer-2.5-fast",
2098                "gpt-6.1-sol",
2099                "big-pickle",
2100                "mimo-v2.5-free",
2101                "nemotron-3-ultra-free",
2102                "north-mini-code-free",
2103                "deepseek-v4-flash",
2104                "deepseek-v4-pro",
2105                "kimi-k2.6",
2106                "qwen3.6-plus",
2107                "glm-5.1",
2108                "deepseek-v4-flash",
2109                "deepseek-v4-pro",
2110                "kimi-for-coding",
2111                "x-ai/grok-4.6",
2112                "x-ai/grok-4.7",
2113                "moonshotai/kimi-k3",
2114                "moonshotai/kimi-k2.7-code",
2115                "moonshotai/kimi-k2.6",
2116                "anthropic/claude-opus-5.5",
2117                "anthropic/claude-sonnet-5.5",
2118                "anthropic/claude-haiku-5.5",
2119                "anthropic/claude-fable-5.1",
2120                "anthropic/claude-opus-5",
2121                "openai/gpt-6.1-terra",
2122                "openai/gpt-6.1-sol",
2123                "openai/gpt-6-luna",
2124                "openai/gpt-6-astra",
2125                "google/gemini-3.8-flash",
2126                "google/gemini-3.7-flash",
2127                "deepseek/deepseek-v4-pro-0813",
2128                "deepseek/deepseek-v4.1-flash",
2129                "qwen/qwen3.8-max-0902",
2130                "qwen/qwen3.8-flash",
2131                "z-ai/glm-5.3",
2132                "z-ai/glm-5.3-flash",
2133                "xiaomi/mimo-v2.6-pro",
2134                "mistralai/mistral-large-4-0",
2135                "meta/muse-spark-1.3",
2136                "accounts/fireworks/models/qwen3-235b-a22b",
2137                "roder.cloud/free",
2138                "roder.cloud/anthropic/claude-opus-4-7",
2139                "roder.cloud/google/gemini-3.1-pro-preview",
2140                "poolside/laguna-m.1",
2141                "poolside/laguna-xs.2",
2142                "mimo-v2.5-pro",
2143                "mimo-v2-pro",
2144                "mimo-v2.5",
2145                "mimo-v2-omni",
2146                "mimo-v2-flash",
2147                "mimo-v2.5-pro",
2148                "mimo-v2-pro",
2149                "mimo-v2.5",
2150                "mimo-v2-omni",
2151                "mimo-v2-flash",
2152                "syn:large:text",
2153                "syn:small:text",
2154                "syn:large:vision",
2155                "syn:small:vision",
2156                "hf:MiniMaxAI/MiniMax-M3",
2157                "hf:Qwen/Qwen3.6-27B",
2158                "hf:moonshotai/Kimi-K2.6",
2159                "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
2160                "hf:zai-org/GLM-4.7",
2161                "hf:zai-org/GLM-4.7-Flash",
2162                "hf:zai-org/GLM-5.1",
2163                "hf:zai-org/GLM-5.2",
2164                "hf:openai/gpt-oss-120b",
2165                "hf:Qwen/Qwen3.5-397B-A17B",
2166                "deepseek-chat",
2167                "deepseek-reasoner",
2168                "deepseek-v4-flash",
2169                "deepseek-v4-pro",
2170                "composer-2.5",
2171                "composer-2.5-fast",
2172                "claude-fable-5",
2173                "claude-opus-4-8",
2174                "claude-sonnet-4-6",
2175                "gemini-3.1-pro",
2176                "gemini-4-argon",
2177                "grok-4.6",
2178                "gemini-3.7-flash",
2179            ]
2180        );
2181    }
2182
2183    #[test]
2184    fn provider_model_lists_match_gode_catalog() {
2185        assert_eq!(models_for_provider(PROVIDER_OPENAI, false).len(), 4);
2186        assert_eq!(models_for_codex(false).len(), 4);
2187        assert_eq!(models_for_provider(PROVIDER_ANTHROPIC, false).len(), 10);
2188        assert_eq!(models_for_provider(PROVIDER_CLAUDE_CODE, false).len(), 12);
2189        assert_eq!(models_for_provider(PROVIDER_GEMINI, false).len(), 7);
2190        assert_eq!(models_for_provider(PROVIDER_VERTEX, false).len(), 6);
2191        assert_eq!(models_for_provider(PROVIDER_XAI, false).len(), 6);
2192        assert_eq!(models_for_provider(PROVIDER_SUPERGROK, false).len(), 3);
2193        assert_eq!(models_for_provider(PROVIDER_OPENCODE, false).len(), 7);
2194        assert_eq!(models_for_provider(PROVIDER_OPENCODE_GO, false).len(), 5);
2195        assert_eq!(models_for_provider(PROVIDER_OPENROUTER, false).len(), 25);
2196        assert_eq!(models_for_provider(PROVIDER_FIREWORKS, false).len(), 1);
2197        assert_eq!(models_for_provider(PROVIDER_RODER_CLOUD, false).len(), 3);
2198        assert_eq!(models_for_provider(PROVIDER_POOLSIDE, false).len(), 2);
2199        assert_eq!(models_for_provider(PROVIDER_CURSOR, false).len(), 9);
2200        assert_eq!(models_for_provider(PROVIDER_XIAOMI_MIMO, false).len(), 5);
2201        assert_eq!(
2202            models_for_provider(PROVIDER_XIAOMI_MIMO_TOKEN_PLAN, false).len(),
2203            5
2204        );
2205        assert_eq!(models_for_provider(PROVIDER_KIMI_CODE, false).len(), 1);
2206        assert_eq!(models_for_provider(PROVIDER_SYNTHETIC, false).len(), 14);
2207        assert_eq!(models_for_provider(PROVIDER_DEEPSEEK, false).len(), 4);
2208        assert_eq!(models_for_provider(PROVIDER_MOCK, true).len(), 1);
2209    }
2210
2211    #[test]
2212    fn codex_model_list_matches_current_subscription_roster() {
2213        let codex_provider = built_in_providers()
2214            .iter()
2215            .find(|provider| provider.id == PROVIDER_CODEX)
2216            .expect("codex provider");
2217        assert_eq!(codex_provider.default_model, "gpt-6.1-sol");
2218
2219        let ids = models_for_codex(false)
2220            .into_iter()
2221            .map(|model| model.id)
2222            .collect::<Vec<_>>();
2223
2224        assert_eq!(
2225            ids,
2226            vec!["gpt-6.1-terra", "gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna"]
2227        );
2228    }
2229
2230    #[test]
2231    fn new_codex_models_match_current_subscription_metadata() {
2232        let assert_model = |id: &str,
2233                            name: &str,
2234                            description: &str,
2235                            default_reasoning: &str,
2236                            efforts: &[&str],
2237                            context_window: u32,
2238                            max_context_window: u32| {
2239            let model = lookup_model_for_provider(PROVIDER_OPENAI, id).unwrap();
2240
2241            assert_eq!(model.display_name, name, "{id} display name");
2242            assert_eq!(model.description, description, "{id} description");
2243            assert_eq!(model.provider, PROVIDER_OPENAI, "{id} provider");
2244            assert_eq!(
2245                model.default_reasoning, default_reasoning,
2246                "{id} default reasoning"
2247            );
2248            assert_eq!(
2249                model
2250                    .supported_reasoning
2251                    .iter()
2252                    .map(|option| option.effort)
2253                    .collect::<Vec<_>>(),
2254                efforts,
2255                "{id} efforts"
2256            );
2257            assert_eq!(model.context_window, context_window, "{id} context window");
2258            assert_eq!(
2259                model.max_context_window, max_context_window,
2260                "{id} max context window"
2261            );
2262            assert_eq!(
2263                model.auto_compact_token_limit,
2264                context_window.saturating_mul(9) / 10,
2265                "{id} auto compact limit"
2266            );
2267            assert!(model.supports_compaction, "{id} compaction support");
2268            assert!(model.supports_images, "{id} image support");
2269            assert!(model.supports_tools, "{id} tool support");
2270            // GPT-6 family is offered; GPT-5.x entries stay resolvable but hidden.
2271            let offered = matches!(
2272                id,
2273                "gpt-6-astra" | "gpt-6-luna" | "gpt-6.1-sol" | "gpt-6.1-terra"
2274            );
2275            assert_eq!(model.hidden, !offered, "{id} visibility");
2276        };
2277
2278        assert_model(
2279            "gpt-6-astra",
2280            "GPT-6-Astra",
2281            "OpenAI's most capable model, built for the hardest end-to-end work.",
2282            REASONING_HIGH,
2283            &[
2284                REASONING_LOW,
2285                REASONING_MEDIUM,
2286                REASONING_HIGH,
2287                REASONING_XHIGH,
2288                REASONING_MAX,
2289            ],
2290            1_050_000,
2291            1_050_000,
2292        );
2293        assert_model(
2294            "gpt-6-sol",
2295            "GPT-6 Sol",
2296            "Agentic coding model balancing intelligence and cost.",
2297            REASONING_MEDIUM,
2298            &[
2299                REASONING_NONE,
2300                REASONING_LOW,
2301                REASONING_MEDIUM,
2302                REASONING_HIGH,
2303                REASONING_XHIGH,
2304                REASONING_MAX,
2305            ],
2306            1_050_000,
2307            1_050_000,
2308        );
2309        assert_model(
2310            "gpt-6.1-terra",
2311            "GPT-6.1 Terra",
2312            "Fast computer-use and coding model with a 1M context window. $1 / $5 per 1M input/output tokens.",
2313            REASONING_MEDIUM,
2314            &[
2315                REASONING_NONE,
2316                REASONING_LOW,
2317                REASONING_MEDIUM,
2318                REASONING_HIGH,
2319                REASONING_XHIGH,
2320                REASONING_MAX,
2321            ],
2322            1_000_000,
2323            1_000_000,
2324        );
2325        assert_model(
2326            "gpt-6.1-sol",
2327            "GPT-6.1-Sol",
2328            "Agentic coding model balancing intelligence and cost.",
2329            REASONING_MEDIUM,
2330            &[
2331                REASONING_NONE,
2332                REASONING_LOW,
2333                REASONING_MEDIUM,
2334                REASONING_HIGH,
2335                REASONING_XHIGH,
2336                REASONING_MAX,
2337            ],
2338            1_050_000,
2339            1_050_000,
2340        );
2341        assert_model(
2342            "gpt-6-luna",
2343            "GPT-6 Luna",
2344            "Efficient model for focused, high-volume tasks.",
2345            REASONING_MEDIUM,
2346            &[
2347                REASONING_NONE,
2348                REASONING_LOW,
2349                REASONING_MEDIUM,
2350                REASONING_HIGH,
2351                REASONING_XHIGH,
2352                REASONING_MAX,
2353            ],
2354            1_050_000,
2355            1_050_000,
2356        );
2357        assert_model(
2358            "gpt-5.6-sol",
2359            "GPT-5.6-Sol",
2360            "Latest frontier agentic coding model.",
2361            REASONING_LOW,
2362            &[
2363                REASONING_LOW,
2364                REASONING_MEDIUM,
2365                REASONING_HIGH,
2366                REASONING_XHIGH,
2367                REASONING_MAX,
2368                REASONING_ULTRA,
2369            ],
2370            372_000,
2371            372_000,
2372        );
2373        assert_model(
2374            "gpt-5.6-terra",
2375            "GPT-5.6-Terra",
2376            "Balanced agentic coding model for everyday work.",
2377            REASONING_MEDIUM,
2378            &[
2379                REASONING_LOW,
2380                REASONING_MEDIUM,
2381                REASONING_HIGH,
2382                REASONING_XHIGH,
2383                REASONING_MAX,
2384                REASONING_ULTRA,
2385            ],
2386            372_000,
2387            372_000,
2388        );
2389        assert_model(
2390            "gpt-5.6-luna",
2391            "GPT-5.6-Luna",
2392            "Fast and affordable agentic coding model.",
2393            REASONING_MEDIUM,
2394            &[
2395                REASONING_LOW,
2396                REASONING_MEDIUM,
2397                REASONING_HIGH,
2398                REASONING_XHIGH,
2399                REASONING_MAX,
2400            ],
2401            372_000,
2402            372_000,
2403        );
2404        assert_model(
2405            "gpt-5.4",
2406            "GPT-5.4",
2407            "Strong model for everyday coding.",
2408            REASONING_MEDIUM,
2409            &[
2410                REASONING_LOW,
2411                REASONING_MEDIUM,
2412                REASONING_HIGH,
2413                REASONING_XHIGH,
2414            ],
2415            272_000,
2416            1_000_000,
2417        );
2418    }
2419
2420    #[test]
2421    fn deepseek_catalog_defaults_to_chat_model() {
2422        let provider = BUILT_IN_PROVIDERS
2423            .iter()
2424            .find(|provider| provider.id == PROVIDER_DEEPSEEK)
2425            .expect("deepseek provider registered");
2426        assert_eq!(provider.name, "DeepSeek Platform");
2427        assert_eq!(provider.default_model, "deepseek-chat");
2428        assert_eq!(provider.base_url, Some("https://api.deepseek.com/v1"));
2429        assert_eq!(provider.env_key, Some("DEEPSEEK_API_KEY"));
2430        assert_eq!(
2431            normalize_provider_id("deepseek-platform"),
2432            PROVIDER_DEEPSEEK
2433        );
2434        assert_eq!(
2435            provider_family_for_provider(PROVIDER_DEEPSEEK),
2436            ProviderFamily::OpenAi
2437        );
2438
2439        let models = models_for_provider(PROVIDER_DEEPSEEK, false);
2440        assert_eq!(models.len(), 4);
2441        assert!(models.iter().any(|model| model.id == "deepseek-chat"));
2442        assert!(models.iter().any(|model| model.id == "deepseek-reasoner"));
2443        assert!(models.iter().any(|model| model.id == "deepseek-v4-flash"));
2444        assert!(models.iter().any(|model| model.id == "deepseek-v4-pro"));
2445
2446        let flash = models
2447            .iter()
2448            .find(|model| model.id == "deepseek-v4-flash")
2449            .expect("flash model");
2450        assert_eq!(flash.default_reasoning, Some(REASONING_HIGH.to_string()));
2451        assert_eq!(
2452            flash
2453                .supported_reasoning
2454                .iter()
2455                .map(|option| option.effort.as_str())
2456                .collect::<Vec<_>>(),
2457            vec![
2458                REASONING_NONE,
2459                REASONING_LOW,
2460                REASONING_HIGH,
2461                REASONING_XHIGH,
2462                REASONING_MAX,
2463            ]
2464        );
2465
2466        let chat = models
2467            .iter()
2468            .find(|model| model.id == "deepseek-chat")
2469            .expect("chat model");
2470        assert_eq!(chat.default_reasoning, Some(REASONING_NONE.to_string()));
2471        assert!(
2472            chat.supported_reasoning
2473                .iter()
2474                .any(|option| option.effort == REASONING_HIGH)
2475        );
2476    }
2477
2478    #[test]
2479    fn synthetic_catalog_defaults_to_large_text_alias() {
2480        let provider = built_in_providers()
2481            .iter()
2482            .find(|provider| provider.id == PROVIDER_SYNTHETIC)
2483            .expect("synthetic provider registered");
2484        assert_eq!(provider.name, "Synthetic");
2485        assert_eq!(provider.default_model, "syn:large:text");
2486        assert_eq!(
2487            provider.base_url,
2488            Some("https://api.synthetic.new/openai/v1")
2489        );
2490        assert_eq!(provider.env_key, Some("SYNTHETIC_API_KEY"));
2491        assert!(provider.env_aliases.contains(&"RODER_SYNTHETIC_API_KEY"));
2492        assert!(!provider.supports_websockets);
2493
2494        let models = models_for_provider(PROVIDER_SYNTHETIC, false);
2495        let default = models
2496            .iter()
2497            .find(|model| model.id == provider.default_model)
2498            .expect("default synthetic model present");
2499        assert_eq!(default.name, "Synthetic Large (Text)");
2500        assert!(models.iter().any(|model| model.id == "syn:small:text"));
2501        let vision = lookup_model_for_provider(PROVIDER_SYNTHETIC, "syn:large:vision")
2502            .expect("vision alias present");
2503        assert!(vision.supports_images);
2504        assert_eq!(
2505            provider_family_for_provider(PROVIDER_SYNTHETIC),
2506            ProviderFamily::OpenAi
2507        );
2508    }
2509
2510    #[test]
2511    fn synthetic_model_ids_preserve_alias_and_hf_segments() {
2512        assert_eq!(normalize_provider_id("synthetic"), PROVIDER_SYNTHETIC);
2513        assert_eq!(normalize_provider_id("synthetic.new"), PROVIDER_SYNTHETIC);
2514        // syn: aliases keep their colon-delimited segments verbatim.
2515        let alias = lookup_model_for_provider(PROVIDER_SYNTHETIC, "syn:large:text")
2516            .expect("syn alias resolves");
2517        assert_eq!(alias.id, "syn:large:text");
2518        assert_eq!(alias.provider, PROVIDER_SYNTHETIC);
2519        // hf: concrete ids are pinned in the catalog for the always-on models
2520        // but must still keep their owner/model segments when prefixed and
2521        // parsed by the provider.
2522        let label = "synthetic/hf:zai-org/GLM-5.2";
2523        let (provider, model) = label.split_once('/').unwrap();
2524        assert_eq!(provider, PROVIDER_SYNTHETIC);
2525        assert_eq!(model, "hf:zai-org/GLM-5.2");
2526    }
2527
2528    #[test]
2529    fn synthetic_always_on_models_are_pinned_with_documented_context_windows() {
2530        let glm_5_2 = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:zai-org/GLM-5.2")
2531            .expect("GLM-5.2 pinned");
2532        assert_eq!(glm_5_2.provider, PROVIDER_SYNTHETIC);
2533        assert_eq!(glm_5_2.context_window, 524_288);
2534        assert!(!glm_5_2.supports_images);
2535
2536        let minimax = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:MiniMaxAI/MiniMax-M3")
2537            .expect("MiniMax-M3 pinned");
2538        assert_eq!(minimax.context_window, 524_288);
2539
2540        let glm_4_7 = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:zai-org/GLM-4.7")
2541            .expect("GLM-4.7 pinned");
2542        assert_eq!(glm_4_7.context_window, 202_752);
2543
2544        let gpt_oss = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:openai/gpt-oss-120b")
2545            .expect("gpt-oss-120b pinned");
2546        assert_eq!(gpt_oss.context_window, 131_072);
2547
2548        let qwen_3_5 = lookup_model_for_provider(PROVIDER_SYNTHETIC, "hf:Qwen/Qwen3.5-397B-A17B")
2549            .expect("Qwen3.5 397B pinned");
2550        assert_eq!(qwen_3_5.context_window, 262_144);
2551
2552        // Every documented always-on id resolves to a catalog entry.
2553        let always_on = [
2554            "hf:MiniMaxAI/MiniMax-M3",
2555            "hf:Qwen/Qwen3.6-27B",
2556            "hf:moonshotai/Kimi-K2.6",
2557            "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
2558            "hf:zai-org/GLM-4.7",
2559            "hf:zai-org/GLM-4.7-Flash",
2560            "hf:zai-org/GLM-5.1",
2561            "hf:zai-org/GLM-5.2",
2562            "hf:openai/gpt-oss-120b",
2563            "hf:Qwen/Qwen3.5-397B-A17B",
2564        ];
2565        for id in always_on {
2566            let entry = lookup_model_for_provider(PROVIDER_SYNTHETIC, id)
2567                .unwrap_or_else(|| panic!("{id} should be pinned in the synthetic catalog"));
2568            assert_eq!(entry.provider, PROVIDER_SYNTHETIC);
2569            assert!(entry.supports_tools);
2570            assert!(entry.supports_structured);
2571        }
2572    }
2573
2574    #[test]
2575    fn claude_code_catalog_uses_long_context_windows() {
2576        let direct = lookup_model_for_provider(PROVIDER_ANTHROPIC, "claude-sonnet-4-6").unwrap();
2577        let claude_code =
2578            lookup_model_for_provider(PROVIDER_CLAUDE_CODE, "claude-sonnet-4-6").unwrap();
2579
2580        assert_eq!(direct.context_window, 1_000_000);
2581        assert_eq!(claude_code.context_window, 1_000_000);
2582        assert_eq!(claude_code.auto_compact_token_limit, 900_000);
2583        // The Claude Code provider has no server-side compaction, so Roder must
2584        // compact the transcript locally before the prompt overflows the window.
2585        assert!(!claude_code.supports_compaction);
2586        // The direct Anthropic API does support native server-side compaction,
2587        // so the threshold is forwarded to the server instead of compacting the
2588        // transcript locally.
2589        assert!(direct.supports_compaction);
2590        assert_eq!(direct.auto_compact_token_limit, 900_000);
2591    }
2592
2593    #[test]
2594    fn claude_haiku_does_not_advertise_server_side_compaction() {
2595        let haiku = lookup_model("claude-haiku-4-5-20251001").unwrap();
2596
2597        // The live API rejects every request carrying the `compact_20260112`
2598        // edit for Haiku 4.5 ("does not support the 'compact_20260112'
2599        // context management strategy"), so the entry must keep Roder on
2600        // client-side compaction at the auto-compact threshold.
2601        assert!(!haiku.supports_compaction);
2602        assert_eq!(haiku.auto_compact_token_limit, 180_000);
2603    }
2604
2605    #[test]
2606    fn claude_fable_5_1_is_offered_directly_and_through_the_claude_code_harness() {
2607        let direct = lookup_model_for_provider(PROVIDER_ANTHROPIC, "claude-fable-5-1").unwrap();
2608        assert_eq!(direct.display_name, "Claude Fable 5.1");
2609        assert_eq!(direct.context_window, 1_000_000);
2610        assert_eq!(direct.auto_compact_token_limit, 900_000);
2611        assert_eq!(direct.default_reasoning, REASONING_HIGH);
2612        assert!(direct.supports_compaction);
2613        assert_eq!(
2614            direct
2615                .supported_reasoning
2616                .iter()
2617                .map(|option| option.effort)
2618                .collect::<Vec<_>>(),
2619            vec![
2620                REASONING_LOW,
2621                REASONING_MEDIUM,
2622                REASONING_HIGH,
2623                REASONING_XHIGH,
2624                REASONING_MAX
2625            ]
2626        );
2627
2628        let harness = lookup_model_for_provider(PROVIDER_CLAUDE_CODE, "claude-fable-5-1").unwrap();
2629        assert_eq!(harness.provider, PROVIDER_CLAUDE_CODE);
2630        assert_eq!(harness.context_window, 1_000_000);
2631        // The Claude Code provider replays the whole transcript, so Roder
2632        // compacts client-side rather than deferring to server-side compaction.
2633        assert!(!harness.supports_compaction);
2634    }
2635
2636    #[test]
2637    fn google_embedding_model_is_hidden_from_chat_lists() {
2638        assert!(lookup_model("gemini-embedding-2").is_some());
2639        assert!(
2640            models_for_provider(PROVIDER_GOOGLE, false)
2641                .iter()
2642                .all(|model| model.id != "gemini-embedding-2")
2643        );
2644        let model = lookup_model("gemini-embedding-2").unwrap();
2645        assert!(model.hidden);
2646        assert!(!model.supports_tools);
2647    }
2648
2649    #[test]
2650    fn zeroentropy_embedding_model_is_hidden_from_chat_lists() {
2651        assert!(lookup_model("zembed-1").is_some());
2652        assert!(
2653            models_for_provider(PROVIDER_ZEROENTROPY, false)
2654                .iter()
2655                .all(|model| model.id != "zembed-1")
2656        );
2657        let model = lookup_model("zembed-1").unwrap();
2658        assert!(model.hidden);
2659        assert!(!model.supports_tools);
2660    }
2661
2662    #[test]
2663    fn catalog_model_profile_derives_openai_defaults() {
2664        let profile = built_in_model_profile("gpt-5.5").unwrap();
2665
2666        assert_eq!(profile.provider_family, ProviderFamily::OpenAi);
2667        assert_eq!(profile.edit_tool.as_deref(), Some(EDIT_TOOL_PATCH));
2668        assert_eq!(profile.schema_policy, ModelSchemaPolicy::RequiredFirstFlat);
2669        assert_eq!(
2670            profile.instruction_overlay,
2671            ModelInstructionOverlay::LiteralToolOutputs
2672        );
2673        assert_eq!(profile.reasoning.execution.as_deref(), Some(REASONING_LOW));
2674        assert_eq!(profile.parallel_tool_calls, Some(true));
2675    }
2676
2677    #[test]
2678    fn poolside_catalog_defaults_to_thinking_enabled() {
2679        let laguna = lookup_model("poolside/laguna-m.1").unwrap();
2680        assert_eq!(laguna.default_reasoning, REASONING_MEDIUM);
2681        assert_eq!(
2682            laguna
2683                .supported_reasoning
2684                .iter()
2685                .map(|option| option.effort)
2686                .collect::<Vec<_>>(),
2687            vec![REASONING_NONE, REASONING_MEDIUM]
2688        );
2689    }
2690
2691    #[test]
2692    fn xiaomi_mimo_catalog_uses_chat_completions_kind_and_exact_model_ids() {
2693        let provider = BUILT_IN_PROVIDERS
2694            .iter()
2695            .find(|provider| provider.id == PROVIDER_XIAOMI_MIMO)
2696            .unwrap();
2697        let token_plan = BUILT_IN_PROVIDERS
2698            .iter()
2699            .find(|provider| provider.id == PROVIDER_XIAOMI_MIMO_TOKEN_PLAN)
2700            .unwrap();
2701
2702        assert_eq!(provider.kind, PROVIDER_KIND_CHAT_COMPLETIONS);
2703        assert_eq!(token_plan.kind, PROVIDER_KIND_CHAT_COMPLETIONS);
2704        assert_eq!(provider.env_key, Some("MIMO_API_KEY"));
2705        assert_eq!(token_plan.env_key, Some("MIMO_TOKEN_PLAN_API_KEY"));
2706
2707        let ids = models_for_provider(PROVIDER_XIAOMI_MIMO, false)
2708            .into_iter()
2709            .map(|model| model.id)
2710            .collect::<Vec<_>>();
2711        assert_eq!(
2712            ids,
2713            vec![
2714                "mimo-v2.5-pro",
2715                "mimo-v2-pro",
2716                "mimo-v2.5",
2717                "mimo-v2-omni",
2718                "mimo-v2-flash"
2719            ]
2720        );
2721        assert!(lookup_model("out-of-v2-flash").is_none());
2722    }
2723
2724    #[test]
2725    fn supergrok_catalog_exposes_grok_47_46_and_composer_with_expected_context_windows() {
2726        let grok47 = lookup_model_for_provider(PROVIDER_SUPERGROK, "grok-4.7").unwrap();
2727        assert_eq!(grok47.display_name, "Grok 4.7");
2728        assert_eq!(grok47.context_window, 500_000);
2729        assert_eq!(grok47.auto_compact_token_limit, 450_000);
2730        assert_eq!(grok47.default_reasoning, REASONING_HIGH);
2731
2732        let grok46 = lookup_model_for_provider(PROVIDER_SUPERGROK, "grok-4.6").unwrap();
2733        assert_eq!(grok46.display_name, "Grok 4.6");
2734        assert_eq!(grok46.context_window, 500_000);
2735        assert_eq!(grok46.auto_compact_token_limit, 450_000);
2736        assert_eq!(grok46.default_reasoning, REASONING_HIGH);
2737
2738        let composer =
2739            lookup_model_for_provider(PROVIDER_SUPERGROK, "grok-composer-2.5-fast").unwrap();
2740        assert_eq!(composer.display_name, "Grok Composer 2.5 Fast");
2741        assert_eq!(composer.context_window, 200_000);
2742        assert_eq!(composer.auto_compact_token_limit, 180_000);
2743        assert!(composer.supported_reasoning.is_empty());
2744        assert!(!composer.supports_images);
2745
2746        let visible = models_for_provider(PROVIDER_SUPERGROK, false)
2747            .into_iter()
2748            .map(|model| model.id)
2749            .collect::<Vec<_>>();
2750        assert_eq!(
2751            visible,
2752            vec![
2753                "grok-4.7".to_string(),
2754                "grok-4.6".to_string(),
2755                "grok-composer-2.5-fast".to_string(),
2756            ]
2757        );
2758    }
2759
2760    #[test]
2761    fn xai_catalog_entries_match_current_grok_contract() {
2762        let grok46 = models_for_provider(PROVIDER_XAI, false)
2763            .into_iter()
2764            .find(|model| model.id == "grok-4.6")
2765            .unwrap();
2766        assert_eq!(grok46.context_window, Some(500_000));
2767        assert_eq!(grok46.default_reasoning.as_deref(), Some(REASONING_HIGH));
2768        assert_eq!(
2769            grok46
2770                .supported_reasoning
2771                .iter()
2772                .map(|option| option.effort.as_str())
2773                .collect::<Vec<_>>(),
2774            vec![
2775                REASONING_LOW,
2776                REASONING_MEDIUM,
2777                REASONING_HIGH,
2778                REASONING_XHIGH
2779            ]
2780        );
2781
2782        let grok43 = models_for_provider(PROVIDER_XAI, false)
2783            .into_iter()
2784            .find(|model| model.id == "grok-4.3")
2785            .unwrap();
2786        assert_eq!(grok43.context_window, Some(1_000_000));
2787        assert_eq!(grok43.default_reasoning.as_deref(), Some(REASONING_LOW));
2788        assert_eq!(
2789            grok43
2790                .supported_reasoning
2791                .iter()
2792                .map(|option| option.effort.as_str())
2793                .collect::<Vec<_>>(),
2794            vec![
2795                REASONING_NONE,
2796                REASONING_LOW,
2797                REASONING_MEDIUM,
2798                REASONING_HIGH,
2799                REASONING_XHIGH
2800            ]
2801        );
2802
2803        let grok420 = lookup_model("grok-4.20-multi-agent-0309").unwrap();
2804        assert_eq!(grok420.context_window, 2_000_000);
2805        assert_eq!(grok420.auto_compact_token_limit, 1_800_000);
2806        assert_eq!(grok420.provider, PROVIDER_XAI);
2807    }
2808
2809    #[test]
2810    fn provider_aliases_normalize_xai_and_supergrok() {
2811        assert_eq!(normalize_provider_id("grok"), PROVIDER_XAI);
2812        assert_eq!(normalize_provider_id("x.ai"), PROVIDER_XAI);
2813        assert_eq!(normalize_provider_id("x-ai"), PROVIDER_XAI);
2814        assert_eq!(normalize_provider_id("xai-oauth"), PROVIDER_SUPERGROK);
2815        assert_eq!(normalize_provider_id("grok-oauth"), PROVIDER_SUPERGROK);
2816        assert_eq!(normalize_provider_id("supergrok"), PROVIDER_SUPERGROK);
2817        assert_eq!(normalize_provider_id("laguna"), PROVIDER_POOLSIDE);
2818        assert_eq!(normalize_provider_id("composer"), PROVIDER_CURSOR);
2819    }
2820
2821    #[test]
2822    fn fireworks_catalog_preserves_account_scoped_default_model() {
2823        let provider = BUILT_IN_PROVIDERS
2824            .iter()
2825            .find(|provider| provider.id == PROVIDER_FIREWORKS)
2826            .unwrap();
2827
2828        assert_eq!(
2829            provider.default_model,
2830            "accounts/fireworks/models/qwen3-235b-a22b"
2831        );
2832        assert_eq!(provider.env_key, Some("FIREWORKS_API_KEY"));
2833        assert_eq!(provider.env_aliases, &["RODER_FIREWORKS_API_KEY"]);
2834
2835        let model = lookup_model_for_provider(PROVIDER_FIREWORKS, provider.default_model).unwrap();
2836        assert_eq!(model.provider, PROVIDER_FIREWORKS);
2837        assert!(model.supports_tools);
2838        assert!(model.supports_structured);
2839        assert_eq!(
2840            provider_family_for_provider(PROVIDER_FIREWORKS),
2841            ProviderFamily::OpenAi
2842        );
2843    }
2844
2845    #[test]
2846    fn cursor_catalog_profile_is_text_only_agentservice() {
2847        let composer = lookup_model("composer-2.5").unwrap();
2848        assert_eq!(composer.provider, PROVIDER_CURSOR);
2849        assert!(!composer.supports_tools);
2850        assert!(!composer.supports_structured);
2851
2852        let profile = built_in_model_profile("composer-2.5").unwrap();
2853        assert_eq!(profile.provider_family, ProviderFamily::Cursor);
2854        assert_eq!(profile.parallel_tool_calls, Some(false));
2855    }
2856
2857    #[test]
2858    fn provider_aware_lookup_resolves_cursor_proxied_models_to_cursor_family() {
2859        // Id-only lookup resolves shared ids to the first (native) entry.
2860        let id_only = built_in_model_profile("claude-opus-4-8").unwrap();
2861        assert_eq!(id_only.provider_family, ProviderFamily::Anthropic);
2862
2863        // Provider-aware lookup resolves to the Cursor catalog entry/family.
2864        let cursor =
2865            built_in_model_profile_for_provider(PROVIDER_CURSOR, "claude-opus-4-8").unwrap();
2866        assert_eq!(cursor.provider_family, ProviderFamily::Cursor);
2867        assert_eq!(cursor.provider, PROVIDER_CURSOR);
2868        assert_eq!(cursor.parallel_tool_calls, Some(false));
2869
2870        let anthropic =
2871            built_in_model_profile_for_provider(PROVIDER_ANTHROPIC, "claude-opus-4-8").unwrap();
2872        assert_eq!(anthropic.provider_family, ProviderFamily::Anthropic);
2873
2874        // Unknown provider falls back to id-only resolution.
2875        let fallback =
2876            built_in_model_profile_for_provider("does-not-exist", "claude-opus-4-8").unwrap();
2877        assert_eq!(fallback.provider_family, ProviderFamily::Anthropic);
2878    }
2879
2880    #[test]
2881    fn opencode_default_model_is_a_listed_opencode_model() {
2882        let provider = built_in_providers()
2883            .iter()
2884            .find(|provider| provider.id == PROVIDER_OPENCODE)
2885            .expect("opencode provider");
2886        assert!(
2887            models_for_provider(PROVIDER_OPENCODE, false)
2888                .iter()
2889                .any(|model| model.id == provider.default_model),
2890            "opencode default {} should be listed for opencode",
2891            provider.default_model
2892        );
2893    }
2894
2895    #[test]
2896    fn retired_gpt_5_models_are_hidden_but_still_resolvable() {
2897        let visible = built_in_models(false)
2898            .into_iter()
2899            .map(|model| model.id)
2900            .collect::<Vec<_>>();
2901        for id in [
2902            "gpt-5.6-sol",
2903            "gpt-5.6-terra",
2904            "gpt-5.6-luna",
2905            "gpt-5.5",
2906            "gpt-5.5-fast",
2907            "gpt-5.4",
2908            "gpt-5.4-mini",
2909            "gpt-5.3-codex-spark",
2910            "roder.cloud/openai/gpt-5.5",
2911        ] {
2912            assert!(
2913                !visible.contains(&id),
2914                "{id} should be hidden from model lists"
2915            );
2916            // Existing threads, profiles, and compaction policy still resolve these ids.
2917            assert!(lookup_model(id).is_some(), "{id} should still resolve");
2918        }
2919        assert!(
2920            !visible.contains(&"gpt-6-sol")
2921                && visible.contains(&"gpt-6.1-sol")
2922                && visible.contains(&"gpt-6.1-terra")
2923        );
2924        assert!(lookup_model("gpt-6-sol").is_some());
2925    }
2926
2927    #[test]
2928    fn retired_cursor_gpt55_keeps_standard_reasoning_metadata() {
2929        let gpt55 = ModelDescriptor::from(
2930            lookup_model_for_provider(PROVIDER_CURSOR, "gpt-5.5")
2931                .expect("cursor catalog should still resolve retired gpt-5.5"),
2932        );
2933
2934        assert_eq!(gpt55.default_reasoning.as_deref(), Some(REASONING_MEDIUM));
2935        assert_eq!(
2936            gpt55
2937                .supported_reasoning
2938                .iter()
2939                .map(|option| option.effort.as_str())
2940                .collect::<Vec<_>>(),
2941            vec![
2942                REASONING_LOW,
2943                REASONING_MEDIUM,
2944                REASONING_HIGH,
2945                REASONING_XHIGH
2946            ]
2947        );
2948
2949        let gpt55_fast = ModelDescriptor::from(
2950            lookup_model_for_provider(PROVIDER_CURSOR, "gpt-5.5-fast")
2951                .expect("cursor catalog should still resolve retired gpt-5.5-fast"),
2952        );
2953        assert_eq!(
2954            gpt55_fast.default_reasoning.as_deref(),
2955            Some(REASONING_MEDIUM)
2956        );
2957        assert_eq!(gpt55_fast.supported_reasoning.len(), 4);
2958    }
2959
2960    #[test]
2961    fn cursor_opus_advertises_configurable_reasoning_effort() {
2962        let opus = models_for_provider(PROVIDER_CURSOR, false)
2963            .into_iter()
2964            .find(|model| model.id == "claude-opus-4-8")
2965            .expect("cursor catalog should expose claude-opus-4-8");
2966
2967        assert_eq!(opus.default_reasoning.as_deref(), Some(REASONING_HIGH));
2968        assert_eq!(
2969            opus.supported_reasoning
2970                .iter()
2971                .map(|option| option.effort.as_str())
2972                .collect::<Vec<_>>(),
2973            vec![
2974                REASONING_LOW,
2975                REASONING_MEDIUM,
2976                REASONING_HIGH,
2977                REASONING_XHIGH,
2978                REASONING_MAX
2979            ]
2980        );
2981
2982        // Sonnet 4.6 on Cursor advertises the same effort ladder as Anthropic
2983        // (including max), so Ctrl+P / thinking menus can offer it.
2984        let sonnet = models_for_provider(PROVIDER_CURSOR, false)
2985            .into_iter()
2986            .find(|model| model.id == "claude-sonnet-4-6")
2987            .expect("cursor catalog should expose claude-sonnet-4-6");
2988        assert_eq!(sonnet.default_reasoning.as_deref(), Some(REASONING_MEDIUM));
2989        assert_eq!(
2990            sonnet
2991                .supported_reasoning
2992                .iter()
2993                .map(|option| option.effort.as_str())
2994                .collect::<Vec<_>>(),
2995            vec![
2996                REASONING_LOW,
2997                REASONING_MEDIUM,
2998                REASONING_HIGH,
2999                REASONING_MAX
3000            ]
3001        );
3002    }
3003
3004    #[test]
3005    fn claude_opus_and_sonnet_advertise_max_effort() {
3006        let efforts = |id: &str| {
3007            lookup_model(id)
3008                .unwrap()
3009                .supported_reasoning
3010                .iter()
3011                .map(|option| option.effort)
3012                .collect::<Vec<_>>()
3013        };
3014
3015        // Opus 4.7/4.8 support both xhigh and max.
3016        for id in ["claude-opus-4-8", "claude-opus-4-7"] {
3017            assert_eq!(
3018                efforts(id),
3019                vec![
3020                    REASONING_LOW,
3021                    REASONING_MEDIUM,
3022                    REASONING_HIGH,
3023                    REASONING_XHIGH,
3024                    REASONING_MAX
3025                ],
3026                "{id} effort levels"
3027            );
3028        }
3029
3030        // Sonnet 4.6 supports max but not xhigh.
3031        assert_eq!(
3032            efforts("claude-sonnet-4-6"),
3033            vec![
3034                REASONING_LOW,
3035                REASONING_MEDIUM,
3036                REASONING_HIGH,
3037                REASONING_MAX
3038            ]
3039        );
3040
3041        // max stays Anthropic-specific; shared STANDARD_REASONING models do not gain it.
3042        assert!(!efforts("gpt-5.5").contains(&REASONING_MAX));
3043    }
3044
3045    #[test]
3046    fn claude_haiku_does_not_advertise_reasoning_effort() {
3047        let haiku = lookup_model("claude-haiku-4-5-20251001").unwrap();
3048
3049        assert_eq!(haiku.default_reasoning, REASONING_NONE);
3050        assert!(haiku.supported_reasoning.is_empty());
3051
3052        let descriptor = ModelDescriptor::from(haiku);
3053        assert_eq!(descriptor.default_reasoning, None);
3054        assert!(descriptor.supported_reasoning.is_empty());
3055    }
3056
3057    #[test]
3058    fn openai_context_windows_match_current_catalog_values() {
3059        let gpt55 = lookup_model("gpt-5.5").unwrap();
3060        assert_eq!(gpt55.context_window, 1_050_000);
3061        assert_eq!(gpt55.max_context_window, 1_050_000);
3062        assert_eq!(gpt55.auto_compact_token_limit, 945_000);
3063
3064        let mini = lookup_model("gpt-5.4-mini").unwrap();
3065        assert_eq!(mini.context_window, 400_000);
3066        assert_eq!(mini.max_context_window, 400_000);
3067        assert_eq!(mini.auto_compact_token_limit, 360_000);
3068
3069        let spark = lookup_model("gpt-5.3-codex-spark").unwrap();
3070        assert_eq!(spark.provider, PROVIDER_CODEX);
3071        assert_eq!(spark.context_window, 128_000);
3072        assert_eq!(spark.max_context_window, 128_000);
3073        assert_eq!(spark.auto_compact_token_limit, 115_200);
3074    }
3075
3076    #[test]
3077    fn auto_compact_defaults_to_ninety_percent_of_context_window() {
3078        for model in BUILT_IN_MODELS {
3079            if model.context_window == 0 || model.auto_compact_token_limit == 0 {
3080                continue;
3081            }
3082            assert_eq!(
3083                model.auto_compact_token_limit,
3084                model.context_window.saturating_mul(9) / 10,
3085                "{} should compact at 90% of its context window",
3086                model.id
3087            );
3088        }
3089    }
3090}