Skip to main content

oxicode_catalog/catalog/
provider.rs

1//! Provider metadata structures — TOML ↔ Rust.
2
3use crate::catalog::BuiltinModelEntry;
4use serde::{Deserialize, Serialize};
5
6/// How a provider passes its API key in HTTP headers.
7///
8/// Maps to `register_builtins::AuthMethod` 1:1.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10#[serde(rename_all = "kebab-case")]
11pub enum AuthMethod {
12    /// `Authorization: Bearer <key>` — most OpenAI-compatible providers.
13    #[default]
14    Bearer,
15    /// `x-api-key: <key>` — Anthropic and Anthropic-compatible providers.
16    #[serde(rename = "x-api-key")]
17    XApiKey,
18    /// `api-key: <key>` — Azure OpenAI.
19    #[serde(rename = "api-key")]
20    ApiKey,
21    /// No API key header (uses other auth like OAuth, SigV4).
22    None,
23}
24
25/// A single built-in provider entry, deserialized from `data/catalog/providers.toml`.
26///
27/// All fields except `id`, `display_name`, `api`, `env_key`, `auth_method`,
28/// `category`, `description` are optional with sensible defaults.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct BuiltinProviderEntry {
31    /// Primary provider name (e.g. "openai")
32    pub id: String,
33    /// Display name (e.g. "OpenAI")
34    pub display_name: String,
35    /// Alternative names that resolve to this provider
36    #[serde(default)]
37    pub aliases: Vec<String>,
38    /// API type used by this provider
39    pub api: String,
40    /// Environment variable that may hold the API key
41    pub env_key: String,
42    /// Additional environment variables to check
43    #[serde(default)]
44    pub extra_env_keys: Vec<String>,
45    /// Default base URL for the API (empty = computed at runtime)
46    #[serde(default)]
47    pub base_url: String,
48    /// How to pass the API key
49    pub auth_method: AuthMethod,
50    /// Extra HTTP headers required by this provider
51    #[serde(default)]
52    pub extra_headers: Vec<(String, String)>,
53    /// Provider category for UI grouping
54    pub category: String,
55    /// Short human-readable description
56    pub description: String,
57    /// Whether this provider is enabled by default
58    #[serde(default = "default_enabled")]
59    pub default_enabled: bool,
60}
61
62fn default_enabled() -> bool {
63    true
64}
65
66impl BuiltinProviderEntry {
67    /// Get all environment variable names for this provider (primary + extras).
68    pub fn all_env_keys(&self) -> impl Iterator<Item = &str> {
69        std::iter::once(self.env_key.as_str()).chain(self.extra_env_keys.iter().map(|s| s.as_str()))
70    }
71
72    /// Check if this is an OpenAI-compatible provider.
73    pub fn is_openai_compatible(&self) -> bool {
74        matches!(self.api.as_str(), "openai-completions" | "openai-responses")
75    }
76}
77
78/// Load all built-in providers from the materialized models.dev snapshot.
79///
80/// This is cached after first call. The result is leaked to `'static`.
81pub fn load_builtin_providers() -> &'static [BuiltinProviderEntry] {
82    static CACHE: std::sync::OnceLock<&'static [BuiltinProviderEntry]> = std::sync::OnceLock::new();
83    CACHE.get_or_init(|| {
84        let providers = crate::catalog::materialize::materialize_providers();
85        // Box::leak the Vec to obtain `&'static [BuiltinProviderEntry]`.
86        // This happens once at startup; bounded by provider count (~145).
87        Box::leak(providers.into_boxed_slice())
88    })
89}
90
91/// Number of built-in providers.
92pub fn builtin_providers_count() -> usize {
93    load_builtin_providers().len()
94}
95
96// ---------------------------------------------------------------------------
97// Legacy TOML-based model API — now empty (models come from materialize).
98// Retained for SDK backwards compatibility. Returns an empty map.
99// ---------------------------------------------------------------------------
100
101/// Empty BTreeMap stub — models are now loaded from the materialize
102/// pipeline. This function is retained for SDK backwards compatibility
103/// and will return an empty map.
104pub fn load_builtin_models() -> &'static std::collections::BTreeMap<String, Vec<BuiltinModelEntry>>
105{
106    static EMPTY: std::sync::OnceLock<std::collections::BTreeMap<String, Vec<BuiltinModelEntry>>> =
107        std::sync::OnceLock::new();
108    EMPTY.get_or_init(std::collections::BTreeMap::new)
109}
110
111/// Always returns 0 — models are loaded from the materialize pipeline.
112/// Retained for SDK backwards compatibility.
113pub fn builtin_model_count() -> usize {
114    0
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn all_providers_have_valid_auth_method() {
123        for p in load_builtin_providers() {
124            // All four variants are valid; this just ensures none are missing
125            match p.auth_method {
126                AuthMethod::Bearer
127                | AuthMethod::XApiKey
128                | AuthMethod::ApiKey
129                | AuthMethod::None => {}
130            }
131        }
132    }
133
134    #[test]
135    fn all_providers_have_non_empty_env_key() {
136        for p in load_builtin_providers() {
137            assert!(!p.env_key.is_empty(), "Provider {} has empty env_key", p.id);
138        }
139    }
140
141    #[test]
142    fn openai_compatible_providers_use_bearer() {
143        for p in load_builtin_providers() {
144            if p.is_openai_compatible() {
145                assert_eq!(
146                    p.auth_method,
147                    AuthMethod::Bearer,
148                    "OpenAI-compatible provider {} should use Bearer auth",
149                    p.id
150                );
151            }
152        }
153    }
154}