systemprompt_api/routes/gateway/
models.rs1use axum::Json;
7use axum::http::{HeaderMap, StatusCode};
8use serde::Serialize;
9use std::collections::BTreeMap;
10use systemprompt_config::ProfileBootstrap;
11use systemprompt_identifiers::headers::INFERENCE_PROTOCOL;
12use systemprompt_models::profile::{ApiSurface, ProviderRegistry};
13
14#[derive(Debug, Serialize)]
15pub struct RootResponse {
16 pub service: &'static str,
17 pub version: &'static str,
18 pub endpoints: Vec<&'static str>,
19}
20
21pub async fn root() -> Json<RootResponse> {
22 Json(RootResponse {
23 service: "systemprompt-gateway",
24 version: env!("CARGO_PKG_VERSION"),
25 endpoints: vec![
26 "/v1/models",
27 "/v1/messages",
28 "/v1/responses",
29 "/v1/chat/completions",
30 ],
31 })
32}
33
34#[derive(Debug, Serialize)]
35pub struct ModelEntry {
36 #[serde(rename = "type")]
37 pub kind: &'static str,
38 pub id: String,
39 pub display_name: String,
40 pub created_at: String,
41}
42
43#[derive(Debug, Serialize)]
44pub struct ModelsResponse {
45 pub data: Vec<ModelEntry>,
46 pub has_more: bool,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub first_id: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub last_id: Option<String>,
51}
52
53#[derive(Debug, Default, Clone, serde::Deserialize)]
60pub struct ListQuery {
61 pub limit: Option<usize>,
62 pub format: Option<String>,
63}
64
65#[derive(Debug, Serialize)]
66pub struct OpenAiModelEntry {
67 pub id: String,
68 pub object: &'static str,
69 pub created: u64,
70 pub owned_by: &'static str,
71}
72
73#[derive(Debug, Serialize)]
74pub struct OpenAiModelsResponse {
75 pub object: &'static str,
76 pub data: Vec<OpenAiModelEntry>,
77}
78
79pub async fn list(
80 headers: HeaderMap,
81 axum::extract::Query(query): axum::extract::Query<ListQuery>,
82) -> Result<axum::response::Response, (StatusCode, String)> {
83 let profile = ProfileBootstrap::get().map_err(|e| {
84 (
85 StatusCode::SERVICE_UNAVAILABLE,
86 format!("Profile not ready: {e}"),
87 )
88 })?;
89
90 profile
91 .gateway
92 .as_ref()
93 .and_then(systemprompt_models::profile::GatewayState::resolved)
94 .filter(|g| g.enabled)
95 .ok_or_else(|| (StatusCode::NOT_FOUND, "Gateway not enabled".to_owned()))?;
96
97 let surfaces = surfaces_from_header(&headers)?;
98 let mut entries = model_entries(&profile.providers, &surfaces);
99 let total = entries.len();
100 let has_more = match query.limit {
101 Some(limit) if limit < total => {
102 entries.truncate(limit);
103 true
104 },
105 _ => false,
106 };
107
108 if query.format.as_deref() == Some("openai") {
109 let data = entries
110 .into_iter()
111 .map(|e| OpenAiModelEntry {
112 id: e.id,
113 object: "model",
114 created: 0,
115 owned_by: "systemprompt",
116 })
117 .collect();
118 return Ok(axum::response::IntoResponse::into_response(Json(
119 OpenAiModelsResponse {
120 object: "list",
121 data,
122 },
123 )));
124 }
125
126 let first_id = entries.first().map(|e| e.id.clone());
127 let last_id = entries.last().map(|e| e.id.clone());
128
129 Ok(axum::response::IntoResponse::into_response(Json(
130 ModelsResponse {
131 data: entries,
132 has_more,
133 first_id,
134 last_id,
135 },
136 )))
137}
138
139pub fn surfaces_from_header(headers: &HeaderMap) -> Result<Vec<ApiSurface>, (StatusCode, String)> {
140 let Some(raw) = headers
141 .get(INFERENCE_PROTOCOL)
142 .and_then(|v| v.to_str().ok())
143 else {
144 return Ok(Vec::new());
145 };
146 let mut surfaces = Vec::new();
147 for tag in raw.split(',').map(str::trim).filter(|t| !t.is_empty()) {
148 let surface = ApiSurface::from_tag(tag)
149 .filter(|s| *s != ApiSurface::Backend)
150 .ok_or_else(|| {
151 (
152 StatusCode::BAD_REQUEST,
153 format!("unknown {INFERENCE_PROTOCOL} value: {tag}"),
154 )
155 })?;
156 surfaces.push(surface);
157 }
158 Ok(surfaces)
159}
160
161pub fn model_entries(registry: &ProviderRegistry, surfaces: &[ApiSurface]) -> Vec<ModelEntry> {
162 let mut by_id: BTreeMap<String, ModelEntry> = BTreeMap::new();
163 for id in registry.advertised_model_ids(surfaces) {
164 by_id.insert(
165 id.clone(),
166 ModelEntry {
167 kind: "model",
168 display_name: humanize_model_id(&id),
169 id,
170 created_at: "1970-01-01T00:00:00Z".to_owned(),
171 },
172 );
173 }
174 by_id.into_values().collect()
175}
176
177pub fn humanize_model_id(id: &str) -> String {
178 id.split('-')
179 .map(|part| {
180 let mut chars = part.chars();
181 chars.next().map_or_else(String::new, |c| {
182 c.to_ascii_uppercase().to_string() + chars.as_str()
183 })
184 })
185 .collect::<Vec<_>>()
186 .join(" ")
187}