systemprompt_cli/commands/admin/setup/secrets/
data.rs1use anyhow::{Result, bail};
11use serde::{Deserialize, Serialize};
12use systemprompt_identifiers::ProviderId;
13
14use super::super::SetupArgs;
15
16pub(super) const STANDARD_PROVIDERS: [&str; 3] = ["gemini", "anthropic", "openai"];
17
18const PROVIDER_PRIORITY: [&str; 3] = ["anthropic", "openai", "gemini"];
19
20#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct SecretsData {
22 pub oauth_at_rest_pepper: String,
23
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub database_url: Option<String>,
26
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub gemini: Option<String>,
29
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub anthropic: Option<String>,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub openai: Option<String>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub github: Option<String>,
38}
39
40impl SecretsData {
41 pub(crate) const fn has_ai_provider(&self) -> bool {
42 self.gemini.is_some() || self.anthropic.is_some() || self.openai.is_some()
43 }
44
45 fn key_for(&self, provider: &str) -> Option<&String> {
46 match provider {
47 "gemini" => self.gemini.as_ref(),
48 "anthropic" => self.anthropic.as_ref(),
49 "openai" => self.openai.as_ref(),
50 _ => None,
51 }
52 }
53
54 pub(crate) fn present_providers(&self) -> Vec<&'static str> {
55 STANDARD_PROVIDERS
56 .into_iter()
57 .filter(|p| self.key_for(p).is_some())
58 .collect()
59 }
60
61 pub(crate) fn summary(&self) -> String {
62 let mut keys = Vec::new();
63 if self.gemini.is_some() {
64 keys.push("Gemini");
65 }
66 if self.anthropic.is_some() {
67 keys.push("Anthropic");
68 }
69 if self.openai.is_some() {
70 keys.push("OpenAI");
71 }
72 if self.github.is_some() {
73 keys.push("GitHub");
74 }
75
76 if keys.is_empty() {
77 "None".to_owned()
78 } else {
79 keys.join(", ")
80 }
81 }
82}
83
84fn first_present_by_priority(secrets: &SecretsData) -> Option<ProviderId> {
85 PROVIDER_PRIORITY
86 .into_iter()
87 .find(|p| secrets.key_for(p).is_some())
88 .map(ProviderId::new)
89}
90
91pub(super) fn resolve_primary(
92 args: &SetupArgs,
93 secrets: &SecretsData,
94) -> Result<Option<ProviderId>> {
95 let Some(name) = args.default_provider.as_deref().map(str::trim) else {
96 return Ok(first_present_by_priority(secrets));
97 };
98 if !STANDARD_PROVIDERS.contains(&name) {
99 bail!("--default-provider must be one of: gemini, anthropic, openai (got '{name}')");
100 }
101 if secrets.key_for(name).is_none() {
102 bail!(
103 "--default-provider '{name}' has no API key; pass --{name}-key or drop \
104 --default-provider"
105 );
106 }
107 Ok(Some(ProviderId::new(name)))
108}