sim_lib_openai_server/routes/
models.rs1use std::collections::BTreeSet;
2
3use serde_json::{Map, Value, json};
4use sim_kernel::{Args, Cx, Error, Expr, Result, Symbol, Value as SimValue};
5use sim_lib_agent_runner_core::ModelCard;
6
7use crate::server::{GatewayRequest, GatewayResponse, GatewayRouteState};
8
9pub const MODELS_PATH: &str = "/v1/models";
11
12const FIXTURE_ECHO_MODEL: &str = "fixture/echo";
13const SIM_BROWSE_PLAN_MODEL: &str = "sim/browse/plan";
14
15#[derive(Clone, Debug, PartialEq)]
18pub struct OpenAiModel {
19 id: String,
20 owned_by: String,
21 metadata: Map<String, Value>,
22}
23
24impl OpenAiModel {
25 pub fn new(id: impl Into<String>, owned_by: impl Into<String>) -> Self {
27 Self {
28 id: id.into(),
29 owned_by: owned_by.into(),
30 metadata: Map::new(),
31 }
32 }
33
34 pub fn fixture_echo() -> Self {
36 Self::new(FIXTURE_ECHO_MODEL, "sim")
37 }
38
39 pub fn sim_native(id: impl Into<String>) -> Self {
41 Self::new(id, "sim")
42 }
43
44 pub fn from_model_card(card: ModelCard) -> Self {
47 let mut metadata = Map::new();
48 metadata.insert("runner".to_owned(), Value::String(card.runner.to_string()));
49 metadata.insert(
50 "locality".to_owned(),
51 Value::String(card.locality.to_string()),
52 );
53 for (key, value) in card.extra {
54 metadata.insert(metadata_key(&key), metadata_value(&value));
55 }
56 Self {
57 id: card.model,
58 owned_by: card.provider.to_string(),
59 metadata,
60 }
61 }
62
63 pub fn id(&self) -> &str {
65 &self.id
66 }
67
68 fn to_json(&self) -> Value {
69 let mut object = Map::new();
70 object.insert("id".to_owned(), Value::String(self.id.clone()));
71 object.insert("object".to_owned(), Value::String("model".to_owned()));
72 object.insert("created".to_owned(), json!(0));
73 object.insert("owned_by".to_owned(), Value::String(self.owned_by.clone()));
74 if !self.metadata.is_empty() {
75 object.insert("metadata".to_owned(), Value::Object(self.metadata.clone()));
76 }
77 Value::Object(object)
78 }
79}
80
81#[derive(Clone, Debug, PartialEq)]
84pub struct ModelCatalog {
85 models: Vec<OpenAiModel>,
86}
87
88impl ModelCatalog {
89 pub fn default_fixture() -> Self {
91 Self::from_parts(Vec::new())
92 }
93
94 pub fn from_runner_card_exprs(cards: impl IntoIterator<Item = Expr>) -> Result<Self> {
97 let cards = cards
98 .into_iter()
99 .map(ModelCard::try_from)
100 .collect::<Result<Vec<_>>>()?;
101 Ok(Self::from_model_cards(cards))
102 }
103
104 pub fn from_model_cards(cards: impl IntoIterator<Item = ModelCard>) -> Self {
106 let runner_models = cards
107 .into_iter()
108 .map(OpenAiModel::from_model_card)
109 .collect::<Vec<_>>();
110 Self::from_parts(runner_models)
111 }
112
113 pub fn from_runner_args(cx: &mut Cx, args: Vec<SimValue>) -> Result<Self> {
119 if args.is_empty() {
120 return Ok(Self::default_fixture());
121 }
122 let cards = cx.call_function(&runner_cards_symbol(), Args::new(args))?;
123 let expr = cards.object().as_expr(cx)?;
124 let Expr::List(items) = expr else {
125 return Err(Error::Eval(
126 "runner/cards must return a list of model-card transcripts".to_owned(),
127 ));
128 };
129 Self::from_runner_card_exprs(items)
130 }
131
132 pub fn models(&self) -> &[OpenAiModel] {
134 &self.models
135 }
136
137 fn from_parts(runner_models: Vec<OpenAiModel>) -> Self {
138 let mut seen = BTreeSet::new();
139 let mut models = Vec::new();
140 push_unique(&mut models, &mut seen, OpenAiModel::fixture_echo());
141 for model in runner_models {
142 push_unique(&mut models, &mut seen, model);
143 }
144 push_unique(
145 &mut models,
146 &mut seen,
147 OpenAiModel::sim_native(SIM_BROWSE_PLAN_MODEL),
148 );
149 Self { models }
150 }
151
152 fn to_json(&self) -> Value {
153 json!({
154 "object": "list",
155 "data": self.models.iter().map(OpenAiModel::to_json).collect::<Vec<_>>(),
156 })
157 }
158}
159
160pub fn handle_models(_request: &GatewayRequest, state: &GatewayRouteState) -> GatewayResponse {
163 models_response_for_catalog(&ModelCatalog::from_model_cards(state.runners().cards()))
164}
165
166pub fn models_response() -> GatewayResponse {
168 models_response_for_catalog(&ModelCatalog::default_fixture())
169}
170
171pub fn models_response_for_runner_args(
174 cx: &mut Cx,
175 args: Vec<SimValue>,
176) -> Result<GatewayResponse> {
177 let catalog = ModelCatalog::from_runner_args(cx, args)?;
178 Ok(models_response_for_catalog(&catalog))
179}
180
181pub fn models_response_for_catalog(catalog: &ModelCatalog) -> GatewayResponse {
183 GatewayResponse::json_value(200, catalog.to_json())
184}
185
186pub fn runner_cards_symbol() -> Symbol {
188 Symbol::qualified("runner", "cards")
189}
190
191fn push_unique(models: &mut Vec<OpenAiModel>, seen: &mut BTreeSet<String>, model: OpenAiModel) {
192 if seen.insert(model.id.clone()) {
193 models.push(model);
194 }
195}
196
197fn metadata_key(expr: &Expr) -> String {
198 match expr {
199 Expr::Symbol(symbol) | Expr::Local(symbol) if symbol.namespace.is_none() => {
200 normalize_metadata_key(symbol.name.as_ref())
201 }
202 Expr::Symbol(symbol) | Expr::Local(symbol) => normalize_metadata_key(&symbol.to_string()),
203 Expr::String(value) => normalize_metadata_key(value),
204 other => normalize_metadata_key(&format!("{other:?}")),
205 }
206}
207
208fn normalize_metadata_key(value: &str) -> String {
209 value
210 .chars()
211 .map(|ch| {
212 if ch.is_ascii_alphanumeric() || ch == '_' {
213 ch
214 } else {
215 '_'
216 }
217 })
218 .collect()
219}
220
221fn metadata_value(expr: &Expr) -> Value {
222 match expr {
223 Expr::Nil => Value::Null,
224 Expr::Bool(value) => Value::Bool(*value),
225 Expr::Number(value) => number_json(&value.canonical),
226 Expr::Symbol(symbol) | Expr::Local(symbol) if symbol.namespace.is_none() => {
227 Value::String(symbol.name.as_ref().to_owned())
228 }
229 Expr::Symbol(symbol) | Expr::Local(symbol) => Value::String(symbol.to_string()),
230 Expr::String(value) => Value::String(value.clone()),
231 Expr::Bytes(bytes) => Value::Array(bytes.iter().map(|byte| json!(byte)).collect()),
232 Expr::List(values) | Expr::Vector(values) | Expr::Set(values) | Expr::Block(values) => {
233 Value::Array(values.iter().map(metadata_value).collect())
234 }
235 Expr::Map(entries) => {
236 let mut object = Map::new();
237 for (key, value) in entries {
238 object.insert(metadata_key(key), metadata_value(value));
239 }
240 Value::Object(object)
241 }
242 other => Value::String(format!("{other:?}")),
243 }
244}
245
246fn number_json(canonical: &str) -> Value {
247 serde_json::from_str(canonical).unwrap_or_else(|_| Value::String(canonical.to_owned()))
248}