Skip to main content

systemprompt_api/routes/gateway/
models.rs

1use axum::Json;
2use axum::http::StatusCode;
3use serde::Serialize;
4use std::collections::BTreeMap;
5use systemprompt_config::ProfileBootstrap;
6
7#[derive(Debug, Serialize)]
8pub struct RootResponse {
9    pub service: &'static str,
10    pub version: &'static str,
11    pub endpoints: Vec<&'static str>,
12}
13
14pub async fn root() -> Json<RootResponse> {
15    Json(RootResponse {
16        service: "systemprompt-gateway",
17        version: env!("CARGO_PKG_VERSION"),
18        endpoints: vec!["/v1/models", "/v1/messages"],
19    })
20}
21
22#[derive(Debug, Serialize)]
23pub struct ModelEntry {
24    #[serde(rename = "type")]
25    pub kind: &'static str,
26    pub id: String,
27    pub display_name: String,
28    pub created_at: String,
29}
30
31#[derive(Debug, Serialize)]
32pub struct ModelsResponse {
33    pub data: Vec<ModelEntry>,
34    pub has_more: bool,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub first_id: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub last_id: Option<String>,
39}
40
41pub async fn list() -> Result<Json<ModelsResponse>, (StatusCode, String)> {
42    let profile = ProfileBootstrap::get().map_err(|e| {
43        (
44            StatusCode::SERVICE_UNAVAILABLE,
45            format!("Profile not ready: {e}"),
46        )
47    })?;
48
49    let gateway = profile
50        .gateway
51        .as_ref()
52        .filter(|g| g.enabled)
53        .ok_or_else(|| (StatusCode::NOT_FOUND, "Gateway not enabled".to_string()))?;
54
55    let mut by_id: BTreeMap<String, ModelEntry> = BTreeMap::new();
56
57    if let Some(catalog) = gateway.catalog.as_ref() {
58        for m in &catalog.models {
59            by_id.insert(
60                m.id.clone(),
61                ModelEntry {
62                    kind: "model",
63                    display_name: m
64                        .display_name
65                        .clone()
66                        .unwrap_or_else(|| humanize_model_id(&m.id)),
67                    id: m.id.clone(),
68                    created_at: "1970-01-01T00:00:00Z".to_string(),
69                },
70            );
71        }
72    }
73
74    let entries: Vec<ModelEntry> = by_id.into_values().collect();
75    let first_id = entries.first().map(|e| e.id.clone());
76    let last_id = entries.last().map(|e| e.id.clone());
77
78    Ok(Json(ModelsResponse {
79        data: entries,
80        has_more: false,
81        first_id,
82        last_id,
83    }))
84}
85
86fn humanize_model_id(id: &str) -> String {
87    id.split('-')
88        .map(|part| {
89            let mut chars = part.chars();
90            chars.next().map_or_else(String::new, |c| {
91                c.to_ascii_uppercase().to_string() + chars.as_str()
92            })
93        })
94        .collect::<Vec<_>>()
95        .join(" ")
96}