Skip to main content

systemprompt_cli/commands/admin/config/
gateway.rs

1//! `admin config gateway` — edit the profile's gateway section: enable state,
2//! routing patterns, and the default provider.
3//!
4//! Every mutation resolves the resulting spec and validates it against the
5//! profile's provider registry (`profile.providers`), so a route or
6//! default-provider that names a provider absent from the registry fails at the
7//! edit rather than at the next boot. The gateway owns no catalog: providers
8//! and models live in `profile.providers` (see `admin config catalog`).
9
10use 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::{CommandResult, render_result};
23
24#[derive(Debug, Subcommand)]
25pub enum GatewayCommands {
26    #[command(about = "Enable the gateway")]
27    Enable,
28
29    #[command(about = "Disable the gateway")]
30    Disable,
31
32    #[command(subcommand, about = "Manage gateway routes")]
33    Route(RouteCommands),
34
35    #[command(
36        subcommand,
37        about = "Manage the default provider (catch-all fallback route)"
38    )]
39    DefaultProvider(DefaultProviderCommands),
40}
41
42#[derive(Debug, Subcommand)]
43pub enum DefaultProviderCommands {
44    #[command(about = "Set the default provider (must exist in profile.providers)")]
45    Set {
46        #[arg(long, help = "Provider name declared in profile.providers")]
47        provider: String,
48    },
49
50    #[command(about = "Clear the default provider")]
51    Clear,
52}
53
54#[derive(Debug, Subcommand)]
55pub enum RouteCommands {
56    #[command(about = "Add or replace a route (upsert by model pattern)")]
57    Add(RouteAddArgs),
58
59    #[command(about = "Remove a route by model pattern")]
60    Remove {
61        #[arg(long, help = "Model pattern to remove (e.g. claude-*)")]
62        model_pattern: String,
63    },
64
65    #[command(about = "List configured routes")]
66    List,
67}
68
69#[derive(Debug, Clone, Args)]
70pub struct RouteAddArgs {
71    #[arg(long, help = "Model pattern (e.g. claude-*)")]
72    pub model_pattern: String,
73
74    #[arg(long, help = "Provider name (must exist in profile.providers)")]
75    pub provider: String,
76
77    #[arg(long, help = "Upstream model name the provider expects (optional)")]
78    pub upstream_model: Option<String>,
79}
80
81pub fn execute(command: &GatewayCommands, _config: &CliConfig) -> Result<()> {
82    if matches!(command, GatewayCommands::Route(RouteCommands::List)) {
83        return list_routes();
84    }
85
86    let profile_path = ProfileBootstrap::get_path()?;
87    let mut profile = load_profile(profile_path)?;
88
89    let message = match command {
90        GatewayCommands::Enable => set_enabled(&mut profile, true)?,
91        GatewayCommands::Disable => set_enabled(&mut profile, false)?,
92        GatewayCommands::Route(RouteCommands::Add(args)) => add_route(&mut profile, args)?,
93        GatewayCommands::Route(RouteCommands::Remove { model_pattern }) => {
94            remove_route(&mut profile, model_pattern)?
95        },
96        GatewayCommands::Route(RouteCommands::List) => unreachable!("handled above"),
97        GatewayCommands::DefaultProvider(DefaultProviderCommands::Set { provider }) => {
98            set_default_provider(&mut profile, provider)?
99        },
100        GatewayCommands::DefaultProvider(DefaultProviderCommands::Clear) => {
101            clear_default_provider(&mut profile)?
102        },
103    };
104
105    validate_gateway(&profile)?;
106    save_profile(&profile, profile_path)?;
107
108    render_result(
109        &CommandResult::text(ConfigMutationOutput {
110            field: "gateway".to_owned(),
111            message,
112        })
113        .with_title("Gateway Updated"),
114    );
115    Ok(())
116}
117
118fn spec_mut(profile: &mut Profile) -> Result<&mut GatewayConfigSpec> {
119    profile
120        .gateway
121        .get_or_insert_with(|| GatewayState::Spec(GatewayConfigSpec::default()))
122        .as_spec_mut()
123        .ok_or_else(|| anyhow!("gateway is in a resolved state and cannot be edited"))
124}
125
126fn set_enabled(profile: &mut Profile, enabled: bool) -> Result<String> {
127    spec_mut(profile)?.enabled = enabled;
128    Ok(format!("Gateway enabled = {}", enabled))
129}
130
131fn add_route(profile: &mut Profile, args: &RouteAddArgs) -> Result<String> {
132    let mut route = GatewayRoute {
133        id: RouteId::new(""),
134        model_pattern: args.model_pattern.clone(),
135        provider: ProviderId::new(&args.provider),
136        upstream_model: args.upstream_model.clone(),
137        extra_headers: HashMap::new(),
138        pricing: None,
139    };
140    route.ensure_id();
141    let spec = spec_mut(profile)?;
142    spec.routes
143        .retain(|r| r.model_pattern != args.model_pattern);
144    spec.routes.push(route);
145    Ok(format!(
146        "Route {} -> {} added",
147        args.model_pattern, args.provider
148    ))
149}
150
151fn set_default_provider(profile: &mut Profile, provider: &str) -> Result<String> {
152    spec_mut(profile)?.default_provider = Some(ProviderId::new(provider));
153    Ok(format!("Gateway default provider set to {}", provider))
154}
155
156fn clear_default_provider(profile: &mut Profile) -> Result<String> {
157    spec_mut(profile)?.default_provider = None;
158    Ok("Gateway default provider cleared".to_owned())
159}
160
161fn remove_route(profile: &mut Profile, model_pattern: &str) -> Result<String> {
162    let spec = spec_mut(profile)?;
163    let before = spec.routes.len();
164    spec.routes.retain(|r| r.model_pattern != model_pattern);
165    if spec.routes.len() == before {
166        bail!("No route found for model pattern {}", model_pattern);
167    }
168    Ok(format!("Route {} removed", model_pattern))
169}
170
171fn validate_gateway(profile: &Profile) -> Result<()> {
172    let Some(state) = &profile.gateway else {
173        return Ok(());
174    };
175    let resolved = state.clone().into_spec().resolve();
176    resolved
177        .validate(&profile.providers)
178        .map_err(|e| anyhow!("gateway validation failed: {e}"))
179}
180
181fn list_routes() -> Result<()> {
182    let profile_path = ProfileBootstrap::get_path()?;
183    let profile = load_profile(profile_path)?;
184    let routes: Vec<String> = profile
185        .gateway
186        .map(|state| state.into_spec().routes)
187        .unwrap_or_default()
188        .iter()
189        .map(|r| format!("{} -> {}", r.model_pattern, r.provider.as_str()))
190        .collect();
191
192    render_result(&CommandResult::list(routes).with_title("Gateway Routes"));
193    Ok(())
194}