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