systemprompt_cli/commands/admin/config/
gateway.rs1use std::collections::HashMap;
11
12use anyhow::{Result, anyhow, bail};
13use clap::{Args, Subcommand};
14use systemprompt_config::ProfileBootstrap;
15use systemprompt_identifiers::{ProviderId, RouteId};
16use systemprompt_models::Profile;
17use systemprompt_models::profile::{GatewayConfigSpec, GatewayRoute, GatewayState};
18
19use super::profile_io::{load_profile, save_profile};
20use super::types::ConfigMutationOutput;
21use crate::CliConfig;
22use crate::shared::{CommandOutput, render_result};
23use systemprompt_models::artifacts::ListItem;
24
25#[derive(Debug, Subcommand)]
26pub enum GatewayCommands {
27 #[command(about = "Enable the gateway")]
28 Enable,
29
30 #[command(about = "Disable the gateway")]
31 Disable,
32
33 #[command(subcommand, about = "Manage gateway routes")]
34 Route(RouteCommands),
35
36 #[command(
37 subcommand,
38 about = "Manage the default provider (catch-all fallback route)"
39 )]
40 DefaultProvider(DefaultProviderCommands),
41}
42
43#[derive(Debug, Subcommand)]
44pub enum DefaultProviderCommands {
45 #[command(about = "Set the default provider (must exist in profile.providers)")]
46 Set {
47 #[arg(long, help = "Provider name declared in profile.providers")]
48 provider: String,
49 },
50
51 #[command(about = "Clear the default provider")]
52 Clear,
53}
54
55#[derive(Debug, Subcommand)]
56pub enum RouteCommands {
57 #[command(about = "Add or replace a route (upsert by model pattern)")]
58 Add(RouteAddArgs),
59
60 #[command(about = "Remove a route by model pattern")]
61 Remove {
62 #[arg(long, help = "Model pattern to remove (e.g. claude-*)")]
63 model_pattern: String,
64 },
65
66 #[command(about = "List configured routes")]
67 List,
68}
69
70#[derive(Debug, Clone, Args)]
71pub struct RouteAddArgs {
72 #[arg(long, help = "Model pattern (e.g. claude-*)")]
73 pub model_pattern: String,
74
75 #[arg(long, help = "Provider name (must exist in profile.providers)")]
76 pub provider: String,
77
78 #[arg(long, help = "Upstream model name the provider expects (optional)")]
79 pub upstream_model: Option<String>,
80}
81
82pub async fn execute(command: &GatewayCommands, config: &CliConfig) -> Result<()> {
83 if matches!(command, GatewayCommands::Route(RouteCommands::List)) {
84 return list_routes(config);
85 }
86
87 let profile_path = ProfileBootstrap::get_path()?;
88 let mut profile = load_profile(profile_path)?;
89
90 let message = match command {
91 GatewayCommands::Enable => set_enabled(&mut profile, true)?,
92 GatewayCommands::Disable => set_enabled(&mut profile, false)?,
93 GatewayCommands::Route(RouteCommands::Add(args)) => add_route(&mut profile, args)?,
94 GatewayCommands::Route(RouteCommands::Remove { model_pattern }) => {
95 remove_route(&mut profile, model_pattern)?
96 },
97 GatewayCommands::Route(RouteCommands::List) => unreachable!("handled above"),
98 GatewayCommands::DefaultProvider(DefaultProviderCommands::Set { provider }) => {
99 set_default_provider(&mut profile, provider)?
100 },
101 GatewayCommands::DefaultProvider(DefaultProviderCommands::Clear) => {
102 clear_default_provider(&mut profile)?
103 },
104 };
105
106 validate_gateway(&profile)?;
107 save_profile(&profile, profile_path)?;
108 let outcome = super::reconcile::reconcile_authz(&profile, profile_path).await;
109
110 render_result(
111 &CommandOutput::card_value(
112 "Gateway Updated",
113 &ConfigMutationOutput {
114 field: "gateway".to_owned(),
115 message: super::reconcile::append_reconcile_notice(message, &outcome),
116 },
117 ),
118 config,
119 );
120 Ok(())
121}
122
123fn spec_mut(profile: &mut Profile) -> Result<&mut GatewayConfigSpec> {
124 profile
125 .gateway
126 .get_or_insert_with(|| GatewayState::Spec(GatewayConfigSpec::default()))
127 .as_spec_mut()
128 .ok_or_else(|| anyhow!("gateway is in a resolved state and cannot be edited"))
129}
130
131fn set_enabled(profile: &mut Profile, enabled: bool) -> Result<String> {
132 spec_mut(profile)?.enabled = enabled;
133 Ok(format!("Gateway enabled = {}", enabled))
134}
135
136fn add_route(profile: &mut Profile, args: &RouteAddArgs) -> Result<String> {
137 let mut route = GatewayRoute {
138 id: RouteId::new(""),
139 model_pattern: args.model_pattern.clone(),
140 provider: ProviderId::new(&args.provider),
141 upstream_model: args.upstream_model.clone(),
142 extra_headers: HashMap::new(),
143 pricing: None,
144 when: None,
145 };
146 route.ensure_id();
147 let spec = spec_mut(profile)?;
148 spec.routes
149 .retain(|r| r.model_pattern != args.model_pattern);
150 spec.routes.push(route);
151 Ok(format!(
152 "Route {} -> {} added",
153 args.model_pattern, args.provider
154 ))
155}
156
157fn set_default_provider(profile: &mut Profile, provider: &str) -> Result<String> {
158 spec_mut(profile)?.default_provider = Some(ProviderId::new(provider));
159 Ok(format!("Gateway default provider set to {}", provider))
160}
161
162fn clear_default_provider(profile: &mut Profile) -> Result<String> {
163 spec_mut(profile)?.default_provider = None;
164 Ok("Gateway default provider cleared".to_owned())
165}
166
167fn remove_route(profile: &mut Profile, model_pattern: &str) -> Result<String> {
168 let spec = spec_mut(profile)?;
169 let before = spec.routes.len();
170 spec.routes.retain(|r| r.model_pattern != model_pattern);
171 if spec.routes.len() == before {
172 bail!("No route found for model pattern {}", model_pattern);
173 }
174 Ok(format!("Route {} removed", model_pattern))
175}
176
177fn validate_gateway(profile: &Profile) -> Result<()> {
178 let Some(state) = &profile.gateway else {
179 return Ok(());
180 };
181 let resolved = state.clone().into_spec().resolve();
182 resolved
183 .validate(&profile.providers)
184 .map_err(|e| anyhow!("gateway validation failed: {e}"))
185}
186
187fn list_routes(config: &CliConfig) -> Result<()> {
188 let profile_path = ProfileBootstrap::get_path()?;
189 let profile = load_profile(profile_path)?;
190 let items: Vec<ListItem> = profile
191 .gateway
192 .map(|state| state.into_spec().routes)
193 .unwrap_or_default()
194 .iter()
195 .map(|r| {
196 let route = format!("{} -> {}", r.model_pattern, r.provider.as_str());
197 ListItem::new(route, String::new(), String::new())
198 })
199 .collect();
200 render_result(
201 &CommandOutput::list(items).with_title("Gateway Routes"),
202 config,
203 );
204 Ok(())
205}