Skip to main content

systemprompt_config/services/
provider_catalog.rs

1//! Provider-registry mutations behind the `admin config catalog` surface.
2//!
3//! [`ProviderCatalogService`] edits the typed
4//! [`ProviderRegistry`] on a profile: declaring or removing upstream providers
5//! and the models each provider serves. Upserts replace an entry in place;
6//! a provider upsert preserves the existing model catalog so connectivity can
7//! be re-declared without re-listing models. Callers revalidate and persist
8//! the profile after a successful mutation.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use std::collections::HashMap;
14
15use systemprompt_identifiers::{ModelId, ProviderId, SecretName};
16use systemprompt_models::profile::{
17    ApiSurface, ProviderEntry, ProviderModel, ProviderRegistry, WireProtocol,
18};
19use systemprompt_models::services::ai::{ModelCapabilities, ModelLimits, ModelPricing};
20
21use crate::error::{ConfigError, ConfigResult};
22
23#[derive(Debug, Clone)]
24pub struct ProviderSpec {
25    pub name: ProviderId,
26    pub wire: WireProtocol,
27    pub surface: ApiSurface,
28    pub endpoint: String,
29    pub api_key_secret: SecretName,
30    pub extra_headers: HashMap<String, String>,
31}
32
33#[derive(Debug, Clone)]
34pub struct ModelSpec {
35    pub provider: ProviderId,
36    pub id: ModelId,
37    pub aliases: Vec<ModelId>,
38    pub upstream_model: Option<String>,
39}
40
41#[derive(Debug, Clone, Copy)]
42pub struct ProviderCatalogService;
43
44impl ProviderCatalogService {
45    pub fn upsert_provider(registry: &mut ProviderRegistry, spec: ProviderSpec) {
46        let models = registry
47            .find_provider(spec.name.as_str())
48            .map(|p| p.models.clone())
49            .unwrap_or_default();
50        registry
51            .providers
52            .retain(|p| p.name.as_str() != spec.name.as_str());
53        registry.providers.push(ProviderEntry {
54            name: spec.name,
55            wire: spec.wire,
56            surface: spec.surface,
57            endpoint: spec.endpoint,
58            api_key_secret: spec.api_key_secret,
59            extra_headers: spec.extra_headers,
60            models,
61        });
62    }
63
64    pub fn remove_provider(registry: &mut ProviderRegistry, name: &ProviderId) -> ConfigResult<()> {
65        let before = registry.providers.len();
66        registry
67            .providers
68            .retain(|p| p.name.as_str() != name.as_str());
69        if registry.providers.len() == before {
70            return Err(ConfigError::ProviderNotFound {
71                name: name.to_string(),
72            });
73        }
74        Ok(())
75    }
76
77    pub fn upsert_model(registry: &mut ProviderRegistry, spec: ModelSpec) -> ConfigResult<()> {
78        let provider = registry
79            .providers
80            .iter_mut()
81            .find(|p| p.name.as_str() == spec.provider.as_str())
82            .ok_or_else(|| ConfigError::ProviderNotFound {
83                name: spec.provider.to_string(),
84            })?;
85        provider
86            .models
87            .retain(|m| m.id.as_str() != spec.id.as_str());
88        provider.models.push(ProviderModel {
89            id: spec.id,
90            aliases: spec.aliases,
91            upstream_model: spec.upstream_model,
92            pricing: ModelPricing::default(),
93            capabilities: ModelCapabilities::default(),
94            limits: ModelLimits::default(),
95        });
96        Ok(())
97    }
98
99    pub fn remove_model(
100        registry: &mut ProviderRegistry,
101        provider_name: &ProviderId,
102        id: &ModelId,
103    ) -> ConfigResult<()> {
104        let provider = registry
105            .providers
106            .iter_mut()
107            .find(|p| p.name.as_str() == provider_name.as_str())
108            .ok_or_else(|| ConfigError::ProviderNotFound {
109                name: provider_name.to_string(),
110            })?;
111        let before = provider.models.len();
112        provider.models.retain(|m| m.id.as_str() != id.as_str());
113        if provider.models.len() == before {
114            return Err(ConfigError::ModelNotFound {
115                id: id.to_string(),
116                provider: provider_name.to_string(),
117            });
118        }
119        Ok(())
120    }
121}