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