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