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: "inferx",
142 name: "InferX",
143 base_url: "https://model.inferx.net/endpoints/v1",
144 env_vars: &["INFERX_API_KEY"],
145 },
146 StaticProviderSpec {
147 key: "kimi",
148 name: "Kimi (Moonshot AI)",
149 base_url: "https://api.moonshot.ai/v1",
150 env_vars: &["MOONSHOT_API_KEY", "KIMI_API_KEY"],
151 },
152];
153
154pub const OPENAI_COMPAT_ALLOWED_PATHS: &[&str] = &["/models", "/chat/completions"];
159
160pub fn allowed_proxy_paths(key: &str) -> &'static [&'static str] {
165 if key == LOCAL_PROVIDER_KEY || static_provider(key).is_some() {
166 OPENAI_COMPAT_ALLOWED_PATHS
167 } else {
168 &[]
169 }
170}
171
172pub fn static_provider(key: &str) -> Option<&'static StaticProviderSpec> {
174 STATIC_PROVIDERS.iter().find(|s| s.key == key)
175}
176
177pub fn is_static_provider(key: &str) -> bool {
179 static_provider(key).is_some()
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::collections::HashSet;
186 use std::str::FromStr;
187
188 #[test]
190 fn static_provider_keys_are_unique() {
191 let mut seen = HashSet::new();
192 for spec in STATIC_PROVIDERS {
193 assert!(
194 seen.insert(spec.key),
195 "duplicate static provider key: {}",
196 spec.key
197 );
198 }
199 }
200
201 #[test]
204 fn static_provider_base_urls_are_https() {
205 for spec in STATIC_PROVIDERS {
206 assert!(
207 spec.base_url.starts_with("https://"),
208 "{} base_url must be https: {}",
209 spec.key,
210 spec.base_url
211 );
212 }
213 }
214
215 #[test]
218 fn static_keys_do_not_collide_with_oauth_ids_or_local() {
219 for spec in STATIC_PROVIDERS {
220 assert!(
221 crate::core::auth::provider::OAuthProviderId::from_str(spec.key).is_err(),
222 "static provider key {} collides with an OAuth provider id",
223 spec.key
224 );
225 assert_ne!(spec.key, LOCAL_PROVIDER_KEY);
226 }
227 }
228
229 #[test]
230 fn lookup_finds_known_and_rejects_unknown() {
231 assert_eq!(static_provider("groq").unwrap().name, "Groq");
232 assert!(static_provider("local").is_none());
233 assert!(static_provider("anthropic").is_none());
234 assert!(!is_static_provider("no-such-provider"));
235 }
236
237 #[test]
240 fn allowed_proxy_paths_fail_closed() {
241 for spec in STATIC_PROVIDERS {
242 assert_eq!(allowed_proxy_paths(spec.key), OPENAI_COMPAT_ALLOWED_PATHS);
243 }
244 assert_eq!(
245 allowed_proxy_paths(LOCAL_PROVIDER_KEY),
246 OPENAI_COMPAT_ALLOWED_PATHS
247 );
248 assert!(allowed_proxy_paths("anthropic").is_empty());
249 assert!(allowed_proxy_paths("no-such-provider").is_empty());
250 for path in OPENAI_COMPAT_ALLOWED_PATHS {
251 assert!(path.starts_with('/'), "catalog paths are relative: {path}");
252 }
253 }
254}