Skip to main content

vtcode_config/models/
provider.rs

1//! Provider extension methods that depend on vtcode-config model catalogs.
2//!
3//! The `Provider` enum and its core methods are defined in `vtcode-commons`.
4//! This module adds vtcode-config-specific extension methods via the
5//! [`ProviderModelSupport`] trait.
6
7pub use vtcode_commons::provider::Provider;
8
9use super::{ModelId, model_catalog_entry};
10use crate::core::OpenAIServiceTier;
11use std::str::FromStr;
12
13const GENERIC_REASONING_EFFORTS: &[&str] = &["low", "medium", "high"];
14
15/// Extension trait on `Provider` for model-specific capability queries.
16///
17/// These methods require vtcode-config model catalogs and constants,
18/// so they cannot live in vtcode-commons with the core `Provider` type.
19pub trait ProviderModelSupport: AsRef<str> {
20    /// Determine if the provider/model exposes structured reasoning output.
21    ///
22    /// Catalog metadata is authoritative for curated models. Unknown routes
23    /// retain the provider's legacy capability fallback so custom endpoints
24    /// continue to work without a catalog entry.
25    fn supports_reasoning(&self, model: &str) -> bool {
26        model_catalog_entry(self.as_ref(), model)
27            .map(|entry| entry.reasoning)
28            .unwrap_or_else(|| self.supports_reasoning_effort(model))
29    }
30
31    /// Determine if the provider supports configurable reasoning effort for the model.
32    fn supports_reasoning_effort(&self, model: &str) -> bool;
33
34    /// Exact effort levels accepted by the provider/model route.
35    fn supported_reasoning_efforts(&self, model: &str) -> &'static [&'static str] {
36        if let Some(entry) = model_catalog_entry(self.as_ref(), model) {
37            return entry.reasoning_efforts;
38        }
39        if self.supports_reasoning_effort(model) {
40            GENERIC_REASONING_EFFORTS
41        } else {
42            &[]
43        }
44    }
45
46    /// Determine if the provider supports the `service_tier` request parameter.
47    fn supports_service_tier(&self, model: &str) -> bool;
48
49    /// Determine if the provider supports a specific `service_tier` value.
50    /// `ultrafast` is a native-OpenAI-only tier: every other provider,
51    /// including OpenAI-compatible gateways that omit it wire-side, reports
52    /// false even when it accepts `flex`/`priority`.
53    fn supports_service_tier_value(&self, model: &str, tier: OpenAIServiceTier) -> bool {
54        match tier {
55            OpenAIServiceTier::Ultrafast => {
56                const ULTRAFAST_MODELS: &[&str] = &[
57                    crate::constants::models::openai::GPT_6_ASTRA,
58                    crate::constants::models::openai::GPT_6_1_SOL,
59                ];
60                self.as_ref() == Provider::OpenAI.as_ref() && ULTRAFAST_MODELS.contains(&model)
61            }
62            _ => self.supports_service_tier(model),
63        }
64    }
65}
66
67impl ProviderModelSupport for Provider {
68    fn supports_reasoning_effort(&self, model: &str) -> bool {
69        if let Some(entry) = model_catalog_entry(self.as_ref(), model) {
70            // Curated metadata is the single source of truth for known routes.
71            // This keeps newly generated catalog entries from depending on a
72            // second hand-maintained provider table and blocks effort payloads
73            // when a model only exposes structured reasoning.
74            return !entry.reasoning_efforts.is_empty();
75        }
76
77        use crate::constants::models;
78
79        match self {
80            Provider::Gemini => models::google::REASONING_MODELS.contains(&model),
81            Provider::OpenAI => models::openai::REASONING_MODELS.contains(&model),
82            Provider::Anthropic => models::anthropic::REASONING_MODELS.contains(&model),
83            Provider::Copilot => false,
84            Provider::DeepSeek => model == models::deepseek::DEEPSEEK_FLASH || model == "deepseek-reasoner",
85            Provider::Meta => models::meta::REASONING_MODELS.contains(&model),
86            Provider::OpenRouter => {
87                if let Ok(model_id) = ModelId::from_str(model) {
88                    if let Some(meta) = crate::models::openrouter_generated::metadata_for(model_id) {
89                        return meta.reasoning;
90                    }
91                    return false;
92                }
93                models::openrouter::REASONING_MODELS.contains(&model)
94            }
95            Provider::Ollama => models::ollama::REASONING_LEVEL_MODELS.contains(&model),
96            Provider::OllamaCloud => models::ollama::REASONING_LEVEL_MODELS.contains(&model),
97            Provider::LmStudio => models::lmstudio::REASONING_MODELS.contains(&model),
98            Provider::LlamaCpp => models::llamacpp::REASONING_MODELS.contains(&model),
99            Provider::Moonshot => models::moonshot::REASONING_MODELS.contains(&model),
100            Provider::ZAI => models::zai::REASONING_MODELS.contains(&model),
101            Provider::Minimax => models::minimax::SUPPORTED_MODELS.contains(&model),
102            Provider::MiMo => models::mimo::SUPPORTED_MODELS.contains(&model),
103            Provider::Mistral => models::mistral::SUPPORTED_MODELS.contains(&model),
104            Provider::HuggingFace => models::huggingface::REASONING_MODELS.contains(&model),
105            Provider::OpenCodeZen => {
106                if models::opencode_zen::OPENAI_MODELS.contains(&model) {
107                    Provider::OpenAI.supports_reasoning_effort(model)
108                } else if models::opencode_zen::ANTHROPIC_MODELS.contains(&model) {
109                    Provider::Anthropic.supports_reasoning_effort(model)
110                } else {
111                    false
112                }
113            }
114            Provider::OpenCodeGo => false,
115            Provider::Qwen => models::qwen::REASONING_MODELS.contains(&model),
116            Provider::StepFun => models::stepfun::REASONING_MODELS.contains(&model),
117            Provider::Evolink => models::evolink::REASONING_MODELS.contains(&model),
118            Provider::Poolside => false,
119            Provider::Vercel => !models::vercel::NON_REASONING_MODELS.contains(&model),
120            Provider::XAI => models::xai::REASONING_MODELS.contains(&model),
121            Provider::NVIDIA => models::nvidia::REASONING_MODELS.contains(&model),
122            Provider::MergeGateway => models::merge_gateway::route_supports_reasoning(model),
123        }
124    }
125
126    fn supports_service_tier(&self, model: &str) -> bool {
127        use crate::constants::models;
128
129        match self {
130            Provider::OpenAI => models::openai::SERVICE_TIER_MODELS.contains(&model),
131            Provider::MergeGateway => model.trim().starts_with("openai/"),
132            // All other OpenAI-compatible wire providers accept `service_tier`
133            // (including `ultrafast`); native non-OpenAI APIs do not.
134            Provider::DeepSeek
135            | Provider::Meta
136            | Provider::OpenRouter
137            | Provider::Ollama
138            | Provider::OllamaCloud
139            | Provider::LmStudio
140            | Provider::LlamaCpp
141            | Provider::Moonshot
142            | Provider::ZAI
143            | Provider::Minimax
144            | Provider::MiMo
145            | Provider::Mistral
146            | Provider::HuggingFace
147            | Provider::OpenCodeZen
148            | Provider::OpenCodeGo
149            | Provider::Qwen
150            | Provider::StepFun
151            | Provider::Evolink
152            | Provider::Poolside
153            | Provider::XAI
154            | Provider::NVIDIA
155            | Provider::Vercel => true,
156            Provider::Gemini | Provider::Anthropic | Provider::Copilot => false,
157        }
158    }
159}