1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct StaticProviderSpec {
17 pub key: &'static str,
19 pub name: &'static str,
21 pub base_url: &'static str,
24 pub env_vars: &'static [&'static str],
26}
27
28pub const LOCAL_PROVIDER_KEY: &str = "local";
32
33pub const LOCAL_DEFAULT_BASE_URL: &str = "http://localhost:11434/v1";
35
36pub const STATIC_PROVIDERS: &[StaticProviderSpec] = &[
38 StaticProviderSpec {
39 key: "groq",
40 name: "Groq",
41 base_url: "https://api.groq.com/openai/v1",
42 env_vars: &["GROQ_API_KEY"],
43 },
44 StaticProviderSpec {
45 key: "cerebras",
46 name: "Cerebras",
47 base_url: "https://api.cerebras.ai/v1",
48 env_vars: &["CEREBRAS_API_KEY"],
49 },
50 StaticProviderSpec {
51 key: "nvidia",
52 name: "NVIDIA NIM",
53 base_url: "https://integrate.api.nvidia.com/v1",
54 env_vars: &["NVIDIA_API_KEY"],
55 },
56 StaticProviderSpec {
57 key: "sambanova",
58 name: "SambaNova",
59 base_url: "https://api.sambanova.ai/v1",
60 env_vars: &["SAMBANOVA_API_KEY"],
61 },
62 StaticProviderSpec {
63 key: "openrouter",
64 name: "OpenRouter",
65 base_url: "https://openrouter.ai/api/v1",
66 env_vars: &["OPENROUTER_API_KEY"],
67 },
68 StaticProviderSpec {
69 key: "google",
70 name: "Google AI Studio",
71 base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
72 env_vars: &["GOOGLE_API_KEY"],
73 },
74 StaticProviderSpec {
75 key: "deepinfra",
76 name: "DeepInfra",
77 base_url: "https://api.deepinfra.com/v1/openai",
78 env_vars: &["DEEPINFRA_API_KEY", "DEEPINFRA_TOKEN"],
79 },
80 StaticProviderSpec {
81 key: "huggingface",
82 name: "HuggingFace",
83 base_url: "https://router.huggingface.co/v1",
84 env_vars: &["HUGGINGFACE_API_KEY", "HF_TOKEN"],
85 },
86 StaticProviderSpec {
87 key: "fireworks",
88 name: "Fireworks AI",
89 base_url: "https://api.fireworks.ai/inference/v1",
90 env_vars: &["FIREWORKS_API_KEY"],
91 },
92 StaticProviderSpec {
93 key: "hyperbolic",
94 name: "Hyperbolic",
95 base_url: "https://api.hyperbolic.xyz/v1",
96 env_vars: &["HYPERBOLIC_API_KEY"],
97 },
98 StaticProviderSpec {
99 key: "scaleway",
100 name: "Scaleway",
101 base_url: "https://api.scaleway.ai/v1",
102 env_vars: &["SCALEWAY_API_KEY"],
103 },
104 StaticProviderSpec {
105 key: "siliconflow",
106 name: "SiliconFlow",
107 base_url: "https://api.siliconflow.cn/v1",
108 env_vars: &["SILICONFLOW_API_KEY"],
109 },
110 StaticProviderSpec {
111 key: "together",
112 name: "Together AI",
113 base_url: "https://api.together.xyz/v1",
114 env_vars: &["TOGETHER_API_KEY"],
115 },
116 StaticProviderSpec {
117 key: "chutes",
118 name: "Chutes AI",
119 base_url: "https://llm.chutes.ai/v1",
120 env_vars: &["CHUTES_API_KEY"],
121 },
122 StaticProviderSpec {
123 key: "codestral",
124 name: "Codestral (Mistral)",
125 base_url: "https://api.mistral.ai/v1",
126 env_vars: &["CODESTRAL_API_KEY"],
127 },
128 StaticProviderSpec {
129 key: "perplexity",
130 name: "Perplexity",
131 base_url: "https://api.perplexity.ai",
132 env_vars: &["PERPLEXITY_API_KEY", "PPLX_API_KEY"],
133 },
134 StaticProviderSpec {
135 key: "ovhcloud",
136 name: "OVHcloud",
137 base_url: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1",
138 env_vars: &["OVH_AI_ENDPOINTS_ACCESS_TOKEN"],
139 },
140 StaticProviderSpec {
141 key: "kimi",
142 name: "Kimi (Moonshot AI)",
143 base_url: "https://api.moonshot.ai/v1",
144 env_vars: &["MOONSHOT_API_KEY", "KIMI_API_KEY"],
145 },
146];
147
148pub const OPENAI_COMPAT_ALLOWED_PATHS: &[&str] = &["/models", "/chat/completions"];
153
154pub fn allowed_proxy_paths(key: &str) -> &'static [&'static str] {
159 if key == LOCAL_PROVIDER_KEY || static_provider(key).is_some() {
160 OPENAI_COMPAT_ALLOWED_PATHS
161 } else {
162 &[]
163 }
164}
165
166pub fn static_provider(key: &str) -> Option<&'static StaticProviderSpec> {
168 STATIC_PROVIDERS.iter().find(|s| s.key == key)
169}
170
171pub fn is_static_provider(key: &str) -> bool {
173 static_provider(key).is_some()
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use std::collections::HashSet;
180 use std::str::FromStr;
181
182 #[test]
184 fn static_provider_keys_are_unique() {
185 let mut seen = HashSet::new();
186 for spec in STATIC_PROVIDERS {
187 assert!(
188 seen.insert(spec.key),
189 "duplicate static provider key: {}",
190 spec.key
191 );
192 }
193 }
194
195 #[test]
198 fn static_provider_base_urls_are_https() {
199 for spec in STATIC_PROVIDERS {
200 assert!(
201 spec.base_url.starts_with("https://"),
202 "{} base_url must be https: {}",
203 spec.key,
204 spec.base_url
205 );
206 }
207 }
208
209 #[test]
212 fn static_keys_do_not_collide_with_oauth_ids_or_local() {
213 for spec in STATIC_PROVIDERS {
214 assert!(
215 crate::core::auth::provider::OAuthProviderId::from_str(spec.key).is_err(),
216 "static provider key {} collides with an OAuth provider id",
217 spec.key
218 );
219 assert_ne!(spec.key, LOCAL_PROVIDER_KEY);
220 }
221 }
222
223 #[test]
224 fn lookup_finds_known_and_rejects_unknown() {
225 assert_eq!(static_provider("groq").unwrap().name, "Groq");
226 assert!(static_provider("local").is_none());
227 assert!(static_provider("anthropic").is_none());
228 assert!(!is_static_provider("no-such-provider"));
229 }
230
231 #[test]
234 fn allowed_proxy_paths_fail_closed() {
235 for spec in STATIC_PROVIDERS {
236 assert_eq!(allowed_proxy_paths(spec.key), OPENAI_COMPAT_ALLOWED_PATHS);
237 }
238 assert_eq!(
239 allowed_proxy_paths(LOCAL_PROVIDER_KEY),
240 OPENAI_COMPAT_ALLOWED_PATHS
241 );
242 assert!(allowed_proxy_paths("anthropic").is_empty());
243 assert!(allowed_proxy_paths("no-such-provider").is_empty());
244 for path in OPENAI_COMPAT_ALLOWED_PATHS {
245 assert!(path.starts_with('/'), "catalog paths are relative: {path}");
246 }
247 }
248}