1use std::env;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum Flavor {
26 OpenAi,
28 Anthropic,
30}
31
32#[derive(Clone, Debug)]
34pub struct PresetModel {
35 pub id: &'static str,
36 pub display_name: &'static str,
37 pub context_window: usize,
38 pub max_output_tokens: Option<u64>,
39 pub supports_images: bool,
41}
42
43#[derive(Clone, Debug)]
45pub struct BackendModel {
46 pub upstream: String,
48 pub display: String,
50 pub context_window: usize,
52 pub max_output_tokens: Option<u64>,
54 pub supports_images: bool,
56}
57
58#[derive(Clone, Debug)]
60pub struct Preset {
61 pub id: &'static str,
64 pub prefix: &'static str,
66 pub key_envs: &'static [&'static str],
68 pub base_envs: &'static [&'static str],
70 pub default_base_url: Option<&'static str>,
72 pub flavor: Flavor,
73 pub models: &'static [PresetModel],
74 pub no_auth: bool,
76 pub oauth_subscription: bool,
78 pub discover: bool,
82}
83
84pub const PRESETS: &[Preset] = &[
85 Preset {
86 id: "openai",
87 prefix: "OPENAI",
88 key_envs: &["OPENAI_API_KEY"],
89 base_envs: &["OPENAI_BASE_URL"],
90 default_base_url: Some("https://api.openai.com/v1"),
91 flavor: Flavor::OpenAi,
92 no_auth: false,
93 oauth_subscription: false,
94 discover: false,
95 models: crate::catalog_generated::OPENAI,
96 },
97 Preset {
98 id: "anthropic",
99 prefix: "ANTHROPIC",
100 key_envs: &[
101 "ANTHROPIC_API_KEY",
102 "ANTHROPIC_AUTH_TOKEN",
103 "ANTHROPIC_OAUTH_TOKEN",
104 ],
105 base_envs: &["ANTHROPIC_BASE_URL"],
106 default_base_url: Some("https://api.anthropic.com/v1"),
107 flavor: Flavor::Anthropic,
108 no_auth: false,
109 oauth_subscription: false,
110 discover: false,
111 models: crate::catalog_generated::ANTHROPIC,
112 },
113 Preset {
114 id: "claude-oauth",
115 prefix: "CLAUDE_OAUTH",
116 key_envs: &[],
117 base_envs: &["CLAUDE_OAUTH_BASE_URL"],
118 default_base_url: Some("https://api.anthropic.com"),
119 flavor: Flavor::Anthropic,
120 no_auth: false,
121 oauth_subscription: true,
122 discover: false,
123 models: crate::catalog_generated::CLAUDE_OAUTH,
124 },
125 Preset {
126 id: "deepseek",
127 prefix: "DEEPSEEK",
128 key_envs: &["DEEPSEEK_API_KEY"],
129 base_envs: &["DEEPSEEK_BASE_URL"],
130 default_base_url: Some("https://api.deepseek.com/v1"),
131 flavor: Flavor::OpenAi,
132 no_auth: false,
133 oauth_subscription: false,
134 discover: false,
135 models: crate::catalog_generated::DEEPSEEK,
136 },
137 Preset {
138 id: "groq",
139 prefix: "GROQ",
140 key_envs: &["GROQ_API_KEY"],
141 base_envs: &["GROQ_BASE_URL"],
142 default_base_url: Some("https://api.groq.com/openai/v1"),
143 flavor: Flavor::OpenAi,
144 no_auth: false,
145 oauth_subscription: false,
146 discover: false,
147 models: crate::catalog_generated::GROQ,
148 },
149 Preset {
150 id: "cerebras",
151 prefix: "CEREBRAS",
152 key_envs: &["CEREBRAS_API_KEY"],
153 base_envs: &["CEREBRAS_BASE_URL"],
154 default_base_url: Some("https://api.cerebras.ai/v1"),
155 flavor: Flavor::OpenAi,
156 no_auth: false,
157 oauth_subscription: false,
158 discover: false,
159 models: crate::catalog_generated::CEREBRAS,
160 },
161 Preset {
162 id: "xai",
163 prefix: "XAI",
164 key_envs: &["XAI_API_KEY"],
165 base_envs: &["XAI_BASE_URL"],
166 default_base_url: Some("https://api.x.ai/v1"),
167 flavor: Flavor::OpenAi,
168 no_auth: false,
169 oauth_subscription: false,
170 discover: false,
171 models: crate::catalog_generated::XAI,
172 },
173 Preset {
174 id: "mistral",
175 prefix: "MISTRAL",
176 key_envs: &["MISTRAL_API_KEY"],
177 base_envs: &["MISTRAL_BASE_URL"],
178 default_base_url: Some("https://api.mistral.ai/v1"),
179 flavor: Flavor::OpenAi,
180 no_auth: false,
181 oauth_subscription: false,
182 discover: false,
183 models: crate::catalog_generated::MISTRAL,
184 },
185 Preset {
186 id: "gemini",
187 prefix: "GEMINI",
188 key_envs: &[
189 "GEMINI_API_KEY",
190 "GOOGLE_GENERATIVE_AI_API_KEY",
191 "GOOGLE_API_KEY",
192 ],
193 base_envs: &["GEMINI_BASE_URL", "GOOGLE_GENERATIVE_AI_BASE_URL"],
194 default_base_url: Some("https://generativelanguage.googleapis.com/v1beta/openai/"),
195 flavor: Flavor::OpenAi,
196 no_auth: false,
197 oauth_subscription: false,
198 discover: false,
199 models: crate::catalog_generated::GEMINI,
200 },
201 Preset {
202 id: "together",
203 prefix: "TOGETHER",
204 key_envs: &["TOGETHER_API_KEY"],
205 base_envs: &["TOGETHER_BASE_URL"],
206 default_base_url: Some("https://api.together.xyz/v1"),
207 flavor: Flavor::OpenAi,
208 no_auth: false,
209 oauth_subscription: false,
210 discover: false,
211 models: crate::catalog_generated::TOGETHER,
212 },
213 Preset {
214 id: "fireworks",
215 prefix: "FIREWORKS",
216 key_envs: &["FIREWORKS_API_KEY"],
217 base_envs: &["FIREWORKS_BASE_URL"],
218 default_base_url: Some("https://api.fireworks.ai/inference/v1"),
219 flavor: Flavor::OpenAi,
220 no_auth: false,
221 oauth_subscription: false,
222 discover: false,
223 models: crate::catalog_generated::FIREWORKS,
224 },
225 Preset {
226 id: "openrouter",
227 prefix: "OPENROUTER",
228 key_envs: &["OPENROUTER_API_KEY"],
229 base_envs: &["OPENROUTER_BASE_URL"],
230 default_base_url: Some("https://openrouter.ai/api/v1"),
231 flavor: Flavor::OpenAi,
232 no_auth: false,
233 oauth_subscription: false,
234 discover: false,
235 models: crate::catalog_generated::OPENROUTER,
236 },
237 Preset {
238 id: "nvidia",
239 prefix: "NVIDIA",
240 key_envs: &["NVIDIA_API_KEY"],
241 base_envs: &["NVIDIA_BASE_URL"],
242 default_base_url: Some("https://integrate.api.nvidia.com/v1"),
243 flavor: Flavor::OpenAi,
244 no_auth: false,
245 oauth_subscription: false,
246 discover: false,
247 models: crate::catalog_generated::NVIDIA,
248 },
249 Preset {
250 id: "moonshot",
251 prefix: "MOONSHOT",
252 key_envs: &["MOONSHOT_API_KEY", "KIMI_API_KEY"],
253 base_envs: &["MOONSHOT_BASE_URL", "KIMI_BASE_URL"],
254 default_base_url: Some("https://api.moonshot.ai/v1"),
255 flavor: Flavor::OpenAi,
256 no_auth: false,
257 oauth_subscription: false,
258 discover: false,
259 models: crate::catalog_generated::MOONSHOT,
260 },
261 Preset {
262 id: "zai",
263 prefix: "ZAI",
264 key_envs: &["ZAI_API_KEY"],
265 base_envs: &["ZAI_BASE_URL"],
266 default_base_url: Some("https://api.z.ai/api/coding/paas/v4"),
267 flavor: Flavor::OpenAi,
268 no_auth: false,
269 oauth_subscription: false,
270 discover: false,
271 models: crate::catalog_generated::ZAI,
272 },
273 Preset {
274 id: "minimax",
275 prefix: "MINIMAX",
276 key_envs: &["MINIMAX_API_KEY", "MINIMAX_CN_API_KEY"],
277 base_envs: &["MINIMAX_BASE_URL"],
278 default_base_url: Some("https://api.minimax.io/v1"),
279 flavor: Flavor::OpenAi,
280 no_auth: false,
281 oauth_subscription: false,
282 discover: false,
283 models: crate::catalog_generated::MINIMAX,
284 },
285 Preset {
286 id: "qwen",
287 prefix: "QWEN",
288 key_envs: &[
289 "QWEN_TOKEN_PLAN_API_KEY",
290 "QWEN_TOKEN_PLAN_CN_API_KEY",
291 "DASHSCOPE_API_KEY",
292 ],
293 base_envs: &["QWEN_BASE_URL", "DASHSCOPE_BASE_URL"],
294 default_base_url: Some("https://dashscope-intl.aliyuncs.com/compatible-mode/v1"),
295 flavor: Flavor::OpenAi,
296 no_auth: false,
297 oauth_subscription: false,
298 discover: false,
299 models: crate::catalog_generated::QWEN,
300 },
301 Preset {
302 id: "baseten",
303 prefix: "BASETEN",
304 key_envs: &["BASETEN_API_KEY"],
305 base_envs: &["BASETEN_BASE_URL"],
306 default_base_url: None, flavor: Flavor::OpenAi,
308 no_auth: false,
309 oauth_subscription: false,
310 discover: false,
311 models: &[],
312 },
313 Preset {
314 id: "huggingface",
315 prefix: "HF",
316 key_envs: &["HF_TOKEN", "HUGGINGFACE_API_KEY"],
317 base_envs: &["HF_BASE_URL"],
318 default_base_url: Some("https://router.huggingface.co/v1"),
319 flavor: Flavor::OpenAi,
320 no_auth: false,
321 oauth_subscription: false,
322 discover: false,
323 models: crate::catalog_generated::HUGGINGFACE,
324 },
325 Preset {
326 id: "vercel-gateway",
327 prefix: "AI_GATEWAY",
328 key_envs: &["AI_GATEWAY_API_KEY"],
329 base_envs: &["AI_GATEWAY_BASE_URL"],
330 default_base_url: Some("https://ai-gateway.vercel.sh/v1"),
331 flavor: Flavor::OpenAi,
332 no_auth: false,
333 oauth_subscription: false,
334 discover: false,
335 models: crate::catalog_generated::VERCEL_GATEWAY,
336 },
337 Preset {
338 id: "opencode",
339 prefix: "OPENCODE",
340 key_envs: &["OPENCODE_API_KEY"],
341 base_envs: &["OPENCODE_BASE_URL"],
342 default_base_url: Some("https://opencode.ai/inference/openai/v1"),
343 flavor: Flavor::OpenAi,
344 no_auth: false,
345 oauth_subscription: false,
346 discover: true,
347 models: crate::catalog_generated::OPENCODE,
348 },
349 Preset {
350 id: "opencode-go",
351 prefix: "OPENCODE_GO",
352 key_envs: &["OPENCODE_API_KEY"],
353 base_envs: &["OPENCODE_GO_BASE_URL"],
354 default_base_url: Some("https://opencode.ai/inference/go/openai/v1"),
355 flavor: Flavor::OpenAi,
356 no_auth: false,
357 oauth_subscription: false,
358 discover: true,
359 models: crate::catalog_generated::OPENCODE_GO,
360 },
361 Preset {
362 id: "azure",
363 prefix: "AZURE_OPENAI",
364 key_envs: &["AZURE_OPENAI_API_KEY", "AZURE_API_KEY"],
365 base_envs: &["AZURE_OPENAI_BASE_URL", "AZURE_OPENAI_ENDPOINT"],
366 default_base_url: None, flavor: Flavor::OpenAi,
368 no_auth: false,
369 oauth_subscription: false,
370 discover: false,
371 models: crate::catalog_generated::AZURE,
372 },
373 Preset {
374 id: "ollama",
375 prefix: "OLLAMA",
376 key_envs: &[],
377 base_envs: &["OLLAMA_HOST", "OLLAMA_BASE_URL"],
378 default_base_url: Some("http://127.0.0.1:11434/v1"),
379 flavor: Flavor::OpenAi,
380 models: &[PresetModel {
381 id: "qwen3-coder:32b",
382 display_name: "Qwen3 Coder 32B (Ollama)",
383 context_window: 128_000,
384 max_output_tokens: Some(32_000),
385 supports_images: true,
386 }],
387 no_auth: true,
388 oauth_subscription: false,
389 discover: true,
390 },
391 Preset {
392 id: "lmstudio",
393 prefix: "LMSTUDIO",
394 key_envs: &[],
395 base_envs: &["LMSTUDIO_BASE_URL"],
396 default_base_url: Some("http://127.0.0.1:1234/v1"),
397 flavor: Flavor::OpenAi,
398 models: &[],
399 no_auth: true,
400 oauth_subscription: false,
401 discover: true,
402 },
403 Preset {
404 id: "vllm",
405 prefix: "VLLM",
406 key_envs: &[],
407 base_envs: &["VLLM_BASE_URL"],
408 default_base_url: Some("http://127.0.0.1:8000/v1"),
409 flavor: Flavor::OpenAi,
410 models: &[],
411 no_auth: true,
412 oauth_subscription: false,
413 discover: true,
414 },
415];
416
417#[derive(Clone, Debug)]
419pub struct ResolvedBackend {
420 pub preset_id: &'static str,
421 pub flavor: Flavor,
422 pub base_url: String,
423 pub api_key: String,
424 pub models: Vec<BackendModel>,
426 pub oauth: Option<crate::oauth::OAuthSessionHandle>,
428 pub discover: bool,
430 pub extra_headers: Vec<(String, String)>,
432}
433
434fn non_empty(var: &str) -> Option<String> {
435 env::var(var)
436 .ok()
437 .map(|v| v.trim().to_owned())
438 .filter(|v| !v.is_empty())
439}
440
441fn first_set(vars: &[&str]) -> Option<String> {
442 vars.iter().find_map(|v| non_empty(v))
443}
444
445fn model_overrides(prefix: &str) -> Option<Vec<String>> {
446 if let Some(list) = non_empty(&format!("{prefix}_MODELS")) {
447 let ids: Vec<String> = list
448 .split(',')
449 .map(|s| s.trim().to_owned())
450 .filter(|s| !s.is_empty())
451 .collect();
452 if !ids.is_empty() {
453 return Some(ids);
454 }
455 }
456 non_empty(&format!("{prefix}_MODEL")).map(|m| vec![m])
457}
458
459fn display_for(id: &str) -> String {
460 id.to_owned()
461}
462
463fn session_id() -> String {
466 if let Some(id) = non_empty("OPENCODE_SESSION_ID") {
467 return id;
468 }
469 let mut random = [0_u8; 16];
470 if getrandom::fill(&mut random).is_err() {
471 return "infinity-provider-models-dev".to_owned();
472 }
473 random
474 .iter()
475 .fold(String::with_capacity(32), |mut hex, byte| {
476 use std::fmt::Write as _;
477 let _ = write!(hex, "{byte:02x}");
478 hex
479 })
480}
481
482impl Preset {
483 fn resolve(&self) -> Option<ResolvedBackend> {
484 if self.oauth_subscription {
485 return self.resolve_oauth();
486 }
487 if self.no_auth {
488 let enabled = env::var("INFINITY_LOCAL").ok().as_deref() == Some("1")
491 || self.base_envs.iter().any(|v| non_empty(v).is_some())
492 || non_empty(&format!("{}_MODELS", self.prefix)).is_some()
493 || non_empty(&format!("{}_MODEL", self.prefix)).is_some();
494 if !enabled {
495 return None;
496 }
497 let base_url =
498 first_set(self.base_envs).or_else(|| self.default_base_url.map(str::to_owned))?;
499 let models = resolve_models(self, &base_url);
500 if models.is_empty() {
501 return None;
502 }
503 return Some(ResolvedBackend {
506 preset_id: self.id,
507 flavor: self.flavor,
508 base_url: trim_trailing_slash(&base_url).to_owned(),
509 api_key: String::new(),
510 oauth: None,
511 discover: true,
512 models,
513 extra_headers: Vec::new(),
514 });
515 }
516
517 let api_key = first_set(self.key_envs)?;
518 let discover = self.discover
522 || std::env::var(format!("{}_DISCOVER", self.prefix))
523 .ok()
524 .as_deref()
525 == Some("1");
526 let mut extra_headers = Vec::new();
530 if self.id == "opencode-go" {
531 extra_headers.push(("x-opencode-session".to_owned(), session_id()));
532 }
533 let mut base_url = first_set(self.base_envs)
534 .or_else(|| non_empty(&format!("{}_BASE_URL", self.prefix)))
535 .or_else(|| self.default_base_url.map(str::to_owned));
536
537 if self.id == "azure" && base_url.is_none() {
540 if let Some(resource) =
541 non_empty("AZURE_OPENAI_RESOURCE_NAME").or_else(|| non_empty("AZURE_RESOURCE_NAME"))
542 {
543 base_url = Some(format!(
544 "https://{}.openai.azure.com/openai/v1",
545 resource.trim()
546 ));
547 }
548 }
549 let base_url = base_url?;
551 let models = resolve_models(self, &base_url);
552 if models.is_empty() && !discover {
553 return None;
554 }
555
556 if self.id == "openrouter" {
557 extra_headers.push((
558 "HTTP-Referer".to_owned(),
559 non_empty("OPENROUTER_APP_URL")
560 .unwrap_or_else(|| "https://github.com/hydro-project/infinity".to_owned()),
561 ));
562 extra_headers.push((
563 "X-Title".to_owned(),
564 non_empty("OPENROUTER_APP_NAME").unwrap_or_else(|| "infinity".to_owned()),
565 ));
566 }
567
568 Some(ResolvedBackend {
569 preset_id: self.id,
570 flavor: self.flavor,
571 base_url: trim_trailing_slash(&base_url).to_owned(),
572 api_key,
573 oauth: None,
574 discover,
575 models,
576 extra_headers,
577 })
578 }
579}
580
581impl Preset {
582 fn resolve_oauth(&self) -> Option<ResolvedBackend> {
585 use crate::oauth::{self, OAuthSession, OAuthSessionHandle};
586 use std::sync::{Arc, RwLock};
587
588 let static_token = non_empty(oauth::STATIC_TOKEN_ENV);
589 let opted_in =
590 std::env::var(oauth::OPT_IN_ENV).ok().as_deref() == Some("1") || static_token.is_some();
591 if !opted_in {
592 return None;
593 }
594 let session = if let Some(token) = static_token {
595 OAuthSession {
596 access_token: token,
597 refresh_token: String::new(),
598 expires_at_ms: i64::MAX,
599 persist_path: None,
600 }
601 } else if let Some(loaded) = oauth::load_credentials() {
602 if loaded.access_token.trim().is_empty() {
603 return None;
604 }
605 OAuthSession {
606 access_token: loaded.access_token,
607 refresh_token: loaded.refresh_token,
608 expires_at_ms: loaded.expires_at_ms,
609 persist_path: loaded.persist_path,
610 }
611 } else {
612 tracing::warn!(
613 "claude-oauth: opted in but no credentials found; run \
614 `infinity-provider-models-dev login`, `claude auth login`, \
615 or set ANTHROPIC_OAUTH_TOKEN"
616 );
617 return None;
618 };
619 if !session.refresh_token.trim().is_empty()
620 && oauth::needs_refresh(session.expires_at_ms, oauth::now_ms())
621 {
622 tracing::info!("claude-oauth: stored bearer expired; will refresh on first use");
623 }
624 let base_url =
625 first_set(self.base_envs).or_else(|| self.default_base_url.map(str::to_owned))?;
626 let base_url = trim_trailing_slash(&base_url).to_owned();
627 let models = resolve_models(self, &base_url);
628 if models.is_empty() {
629 return None;
630 }
631 let handle: OAuthSessionHandle = Arc::new(RwLock::new(session));
632 Some(ResolvedBackend {
633 preset_id: self.id,
634 flavor: self.flavor,
635 base_url,
636 api_key: String::new(),
637 oauth: Some(handle),
638 discover: false,
639 models,
640 extra_headers: Vec::new(),
641 })
642 }
643}
644
645fn preset_to_backend(m: &PresetModel) -> BackendModel {
646 BackendModel {
647 upstream: m.id.to_owned(),
648 display: m.display_name.to_owned(),
649 context_window: m.context_window,
650 max_output_tokens: m.max_output_tokens,
651 supports_images: m.supports_images,
652 }
653}
654
655fn resolve_models(preset: &Preset, _base_url: &str) -> Vec<BackendModel> {
656 if let Some(ids) = model_overrides(preset.prefix) {
657 return ids
658 .into_iter()
659 .map(|id| {
660 let builtin = preset.models.iter().find(|m| m.id == id);
661 builtin.map_or_else(
662 || BackendModel {
663 upstream: id.clone(),
664 display: display_for(&id),
665 context_window: 200_000,
666 max_output_tokens: Some(32_000),
667 supports_images: true,
668 },
669 preset_to_backend,
670 )
671 })
672 .collect();
673 }
674 preset.models.iter().map(preset_to_backend).collect()
675}
676
677pub fn resolve_backends() -> Vec<ResolvedBackend> {
679 let mut backends: Vec<ResolvedBackend> = PRESETS.iter().filter_map(Preset::resolve).collect();
680
681 if let Some(base_url) = non_empty("INFINITY_OPENAI_BASE_URL") {
685 let api_key = non_empty("INFINITY_OPENAI_API_KEY").unwrap_or_default();
686 let ids = non_empty("INFINITY_OPENAI_MODELS")
687 .map(|list| {
688 list.split(',')
689 .map(|s| s.trim().to_owned())
690 .filter(|s| !s.is_empty())
691 .collect::<Vec<_>>()
692 })
693 .filter(|v| !v.is_empty())
694 .unwrap_or_default();
695 backends.push(ResolvedBackend {
696 preset_id: "custom",
697 flavor: Flavor::OpenAi,
698 base_url: trim_trailing_slash(&base_url).to_owned(),
699 api_key,
700 oauth: None,
701 discover: true,
702 models: ids
703 .into_iter()
704 .map(|id| BackendModel {
705 upstream: id.clone(),
706 display: display_for(&id),
707 context_window: 200_000,
708 max_output_tokens: Some(32_000),
709 supports_images: true,
710 })
711 .collect(),
712 extra_headers: Vec::new(),
713 });
714 }
715
716 backends
717}
718
719fn trim_trailing_slash(s: &str) -> &str {
720 s.trim_end_matches('/')
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726
727 #[test]
728 fn model_overrides_empty_without_env() {
729 assert!(model_overrides("INFINITY_PROVIDER_MODELS_DEV_UNSET_TEST_PREFIX").is_none());
731 }
732
733 #[test]
736 fn oauth_preset_opt_in_flow() {
737 let saved_token = std::env::var("ANTHROPIC_OAUTH_TOKEN").ok();
738 let saved_opt_in = std::env::var("INFINITY_CLAUDE_OAUTH").ok();
739 let restore = |saved_token: &Option<String>, saved_opt_in: &Option<String>| {
740 if let Some(token) = saved_token {
741 unsafe {
742 std::env::set_var("ANTHROPIC_OAUTH_TOKEN", token);
743 }
744 } else {
745 unsafe {
746 std::env::remove_var("ANTHROPIC_OAUTH_TOKEN");
747 }
748 }
749 if let Some(opt_in) = saved_opt_in {
750 unsafe {
751 std::env::set_var("INFINITY_CLAUDE_OAUTH", opt_in);
752 }
753 } else {
754 unsafe {
755 std::env::remove_var("INFINITY_CLAUDE_OAUTH");
756 }
757 }
758 };
759 let preset = PRESETS
760 .iter()
761 .find(|p| p.id == "claude-oauth")
762 .expect("claude-oauth preset exists");
763
764 unsafe {
765 std::env::remove_var("ANTHROPIC_OAUTH_TOKEN");
766 std::env::remove_var("INFINITY_CLAUDE_OAUTH");
767 }
768 assert!(preset.resolve().is_none());
769
770 unsafe {
771 std::env::set_var("ANTHROPIC_OAUTH_TOKEN", "sk-ant-oauth01-test");
772 }
773 let backend = preset.resolve().expect("static token activates preset");
774 assert_eq!(backend.preset_id, "claude-oauth");
775 assert!(backend.oauth.is_some());
776 assert_eq!(backend.models.len(), 3);
777
778 restore(&saved_token, &saved_opt_in);
779 }
780
781 #[test]
784 fn opencode_presets_share_key_and_go_sends_session() {
785 let saved_key = std::env::var("OPENCODE_API_KEY").ok();
786 let saved_session = std::env::var("OPENCODE_SESSION_ID").ok();
787 let restore = |saved_key: &Option<String>, saved_session: &Option<String>| {
788 if let Some(key) = saved_key {
789 unsafe {
790 std::env::set_var("OPENCODE_API_KEY", key);
791 }
792 } else {
793 unsafe {
794 std::env::remove_var("OPENCODE_API_KEY");
795 }
796 }
797 if let Some(session) = saved_session {
798 unsafe {
799 std::env::set_var("OPENCODE_SESSION_ID", session);
800 }
801 } else {
802 unsafe {
803 std::env::remove_var("OPENCODE_SESSION_ID");
804 }
805 }
806 };
807 let find = |id: &str| {
808 PRESETS
809 .iter()
810 .find(|p| p.id == id)
811 .unwrap_or_else(|| panic!("{id} preset exists"))
812 };
813
814 unsafe {
815 std::env::remove_var("OPENCODE_API_KEY");
816 std::env::remove_var("OPENCODE_SESSION_ID");
817 }
818 assert!(find("opencode").resolve().is_none());
819 assert!(find("opencode-go").resolve().is_none());
820
821 unsafe {
822 std::env::set_var("OPENCODE_API_KEY", "ok-test");
823 }
824 let base = find("opencode").resolve().expect("opencode activates");
825 assert!(base.discover);
826 assert!(base.models.len() >= 2);
827 assert!(
828 base.extra_headers
829 .iter()
830 .all(|(k, _)| k != "x-opencode-session"),
831 "session header is Go-only"
832 );
833 let go = find("opencode-go")
834 .resolve()
835 .expect("opencode-go activates");
836 assert!(go.discover);
837 let session = go
838 .extra_headers
839 .iter()
840 .find(|(k, _)| k == "x-opencode-session")
841 .map(|(_, v)| v.clone());
842 assert!(session.is_some_and(|s| !s.is_empty()));
843
844 unsafe {
845 std::env::set_var("OPENCODE_SESSION_ID", "pinned-session");
846 }
847 let go = find("opencode-go")
848 .resolve()
849 .expect("opencode-go activates");
850 assert!(
851 go.extra_headers
852 .iter()
853 .any(|(k, v)| k == "x-opencode-session" && v == "pinned-session"),
854 "explicit session id wins"
855 );
856
857 restore(&saved_key, &saved_session);
858 }
859
860 #[test]
861 fn model_overrides_round_trip() {
862 let prefix = "INFINITY_PROVIDER_MODELS_DEV_TEST";
863 let var = format!("{prefix}_MODELS");
864 unsafe {
865 std::env::set_var(&var, "a-model,b-model");
866 }
867 assert_eq!(
868 model_overrides(prefix),
869 Some(vec!["a-model".to_owned(), "b-model".to_owned()])
870 );
871 unsafe {
872 std::env::remove_var(&var);
873 }
874 assert!(model_overrides(prefix).is_none());
875 }
876}