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}