Skip to main content

recursive/
providers.rs

1//! Static vendor preset catalog, embedded at compile time.
2
3use serde::Deserialize;
4
5/// Per-million-token pricing embedded in a provider preset model entry. USD.
6#[derive(Debug, Clone, Copy, Deserialize)]
7pub struct ModelPricingSpec {
8    pub input_per_million: f64,
9    pub output_per_million: f64,
10    /// Cache-hit input price per million tokens. Defaults to input_per_million when absent.
11    pub cache_hit_input_per_million: Option<f64>,
12}
13
14/// A single model entry within a provider preset.
15#[derive(Debug, Clone, Deserialize)]
16pub struct ModelSpec {
17    pub name: String,
18    /// Maximum input context window in tokens for this model.
19    pub context_window: usize,
20    /// Optional pricing. When present, used by `pricing_for()` instead of hard-coded values.
21    pub pricing: Option<ModelPricingSpec>,
22}
23
24#[derive(Debug, Clone, Deserialize)]
25pub struct ProviderPreset {
26    pub id: String,
27    pub name: String,
28    pub provider_type: String,
29    pub api_base: String,
30    pub default_model: String,
31    pub models: Vec<ModelSpec>,
32    pub mainland_accessible: bool,
33    pub key_env: String,
34    pub key_url: String,
35}
36
37#[derive(Deserialize)]
38struct PresetsFile {
39    providers: Vec<ProviderPreset>,
40}
41
42static PRESETS_TOML: &str = include_str!("../providers.toml");
43
44pub fn all_presets() -> &'static [ProviderPreset] {
45    use std::sync::OnceLock;
46    static CACHE: OnceLock<Vec<ProviderPreset>> = OnceLock::new();
47    CACHE.get_or_init(|| {
48        toml::from_str::<PresetsFile>(PRESETS_TOML)
49            .expect("providers.toml is bundled at compile time and must be valid")
50            .providers
51    })
52}
53
54pub fn find_preset(id: &str) -> Option<&'static ProviderPreset> {
55    all_presets().iter().find(|p| p.id == id)
56}
57
58/// Look up a preset by its API base URL. Used to recover the preset id
59/// from a config file that only stores `api_base` (e.g. `recursive config show`)
60/// and to pick sensible defaults in the manual branch of `recursive init` instead
61/// of guessing the model from URL substrings.
62pub fn find_preset_by_api_base(url: &str) -> Option<&'static ProviderPreset> {
63    all_presets().iter().find(|p| p.api_base == url)
64}
65
66/// Look up pricing for a model name across all presets.
67/// Returns `None` if the model is not listed or has no pricing field.
68pub fn find_model_pricing(model: &str) -> Option<&'static ModelPricingSpec> {
69    for preset in all_presets() {
70        for spec in &preset.models {
71            if spec.name == model {
72                return spec.pricing.as_ref();
73            }
74        }
75    }
76    None
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn all_presets_non_empty() {
85        assert!(all_presets().len() > 10);
86    }
87
88    #[test]
89    fn find_preset_anthropic() {
90        let preset = find_preset("anthropic").unwrap();
91        assert_eq!(preset.provider_type, "anthropic");
92    }
93
94    #[test]
95    fn find_preset_unknown_returns_none() {
96        assert!(find_preset("bogus").is_none());
97    }
98
99    #[test]
100    fn all_presets_have_valid_provider_type() {
101        for p in all_presets() {
102            assert!(
103                p.provider_type == "openai" || p.provider_type == "anthropic",
104                "preset {} has invalid provider_type: {}",
105                p.id,
106                p.provider_type
107            );
108        }
109    }
110
111    #[test]
112    fn default_preset_is_anthropic() {
113        assert_eq!(all_presets()[0].id, "anthropic");
114    }
115
116    #[test]
117    fn find_preset_deepseek_api_base() {
118        let preset = find_preset("deepseek").unwrap();
119        assert_eq!(preset.api_base, "https://api.deepseek.com/v1");
120    }
121
122    #[test]
123    fn find_preset_by_api_base_known() {
124        let preset =
125            find_preset_by_api_base("https://api.deepseek.com/v1").expect("deepseek preset");
126        assert_eq!(preset.id, "deepseek");
127    }
128
129    #[test]
130    fn find_preset_by_api_base_unknown() {
131        assert!(find_preset_by_api_base("https://example.com/v1").is_none());
132    }
133}