systemprompt-api 0.14.1

Axum-based HTTP server and API gateway for systemprompt.io AI governance infrastructure. Exposes governed agents, MCP, A2A, and admin endpoints with rate limiting and RBAC.
Documentation
use axum::Json;
use axum::http::StatusCode;
use serde::Serialize;
use std::collections::BTreeMap;
use systemprompt_config::ProfileBootstrap;
use systemprompt_identifiers::ModelId;

#[derive(Debug, Serialize)]
pub struct RootResponse {
    pub service: &'static str,
    pub version: &'static str,
    pub endpoints: Vec<&'static str>,
}

pub async fn root() -> Json<RootResponse> {
    Json(RootResponse {
        service: "systemprompt-gateway",
        version: env!("CARGO_PKG_VERSION"),
        endpoints: vec!["/v1/models", "/v1/messages"],
    })
}

#[derive(Debug, Serialize)]
pub struct ModelEntry {
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub id: String,
    pub display_name: String,
    pub created_at: String,
}

#[derive(Debug, Serialize)]
pub struct ModelsResponse {
    pub data: Vec<ModelEntry>,
    pub has_more: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub first_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_id: Option<String>,
}

pub async fn list() -> Result<Json<ModelsResponse>, (StatusCode, String)> {
    let profile = ProfileBootstrap::get().map_err(|e| {
        (
            StatusCode::SERVICE_UNAVAILABLE,
            format!("Profile not ready: {e}"),
        )
    })?;

    profile
        .gateway
        .as_ref()
        .and_then(systemprompt_models::profile::GatewayState::resolved)
        .filter(|g| g.enabled)
        .ok_or_else(|| (StatusCode::NOT_FOUND, "Gateway not enabled".to_owned()))?;

    let mut by_id: BTreeMap<String, ModelEntry> = BTreeMap::new();

    for entry in &profile.providers.providers {
        for m in &entry.models {
            for id in std::iter::once(m.id.as_str()).chain(m.aliases.iter().map(ModelId::as_str)) {
                by_id.insert(
                    id.to_owned(),
                    ModelEntry {
                        kind: "model",
                        display_name: humanize_model_id(id),
                        id: id.to_owned(),
                        created_at: "1970-01-01T00:00:00Z".to_owned(),
                    },
                );
            }
        }
    }

    let entries: Vec<ModelEntry> = by_id.into_values().collect();
    let first_id = entries.first().map(|e| e.id.clone());
    let last_id = entries.last().map(|e| e.id.clone());

    Ok(Json(ModelsResponse {
        data: entries,
        has_more: false,
        first_id,
        last_id,
    }))
}

fn humanize_model_id(id: &str) -> String {
    id.split('-')
        .map(|part| {
            let mut chars = part.chars();
            chars.next().map_or_else(String::new, |c| {
                c.to_ascii_uppercase().to_string() + chars.as_str()
            })
        })
        .collect::<Vec<_>>()
        .join(" ")
}