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
137pub 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
163pub 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
325pub 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 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
533const 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 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 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 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 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
1704pub 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
1724pub 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 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 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 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 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 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 assert!(!claude_code.supports_compaction);
2559 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 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 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 let id_only = built_in_model_profile("claude-opus-4-8").unwrap();
2834 assert_eq!(id_only.provider_family, ProviderFamily::Anthropic);
2835
2836 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 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 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 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 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 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 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}