Skip to main content

roder_api/
catalog.rs

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