Skip to main content

sim_lib_openai_server/routes/
models.rs

1use std::collections::BTreeSet;
2
3use serde_json::{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
9/// Route path for the OpenAI-compatible `GET /v1/models` discovery endpoint.
10pub 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/// Represents a single entry in the model catalog, mapped to the OpenAI
16/// `model` object shape (`id` plus `owned_by`).
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct OpenAiModel {
19    id: String,
20    owned_by: String,
21}
22
23impl OpenAiModel {
24    /// Builds a model entry from an id and the owning provider name.
25    pub fn new(id: impl Into<String>, owned_by: impl Into<String>) -> Self {
26        Self {
27            id: id.into(),
28            owned_by: owned_by.into(),
29        }
30    }
31
32    /// Returns the built-in `fixture/echo` model owned by `sim`.
33    pub fn fixture_echo() -> Self {
34        Self::new(FIXTURE_ECHO_MODEL, "sim")
35    }
36
37    /// Returns a SIM-native model with the given id owned by `sim`.
38    pub fn sim_native(id: impl Into<String>) -> Self {
39        Self::new(id, "sim")
40    }
41
42    /// Builds a model entry from a runner [`ModelCard`], taking the card's
43    /// model id and provider.
44    pub fn from_model_card(card: ModelCard) -> Self {
45        Self::new(card.model, card.provider.to_string())
46    }
47
48    /// Returns the model id.
49    pub fn id(&self) -> &str {
50        &self.id
51    }
52
53    fn to_json(&self) -> Value {
54        json!({
55            "id": self.id,
56            "object": "model",
57            "created": 0,
58            "owned_by": self.owned_by,
59        })
60    }
61}
62
63/// Represents the deduplicated set of models advertised by `/v1/models`,
64/// always including the built-in fixture and `sim/browse/plan` entries.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct ModelCatalog {
67    models: Vec<OpenAiModel>,
68}
69
70impl ModelCatalog {
71    /// Returns the catalog containing only the built-in fixture models.
72    pub fn default_fixture() -> Self {
73        Self::from_parts(Vec::new())
74    }
75
76    /// Builds a catalog from runner model-card transcripts, parsing each
77    /// expression into a [`ModelCard`] and failing on a malformed transcript.
78    pub fn from_runner_card_exprs(cards: impl IntoIterator<Item = Expr>) -> Result<Self> {
79        let cards = cards
80            .into_iter()
81            .map(ModelCard::try_from)
82            .collect::<Result<Vec<_>>>()?;
83        Ok(Self::from_model_cards(cards))
84    }
85
86    /// Builds a catalog from already-parsed runner model cards.
87    pub fn from_model_cards(cards: impl IntoIterator<Item = ModelCard>) -> Self {
88        let runner_models = cards
89            .into_iter()
90            .map(OpenAiModel::from_model_card)
91            .collect::<Vec<_>>();
92        Self::from_parts(runner_models)
93    }
94
95    /// Builds a catalog by calling the `runner/cards` function with the given
96    /// arguments and parsing the returned list of model-card transcripts.
97    ///
98    /// Returns the fixture-only catalog when no arguments are supplied, and
99    /// errors if `runner/cards` does not return a list.
100    pub fn from_runner_args(cx: &mut Cx, args: Vec<SimValue>) -> Result<Self> {
101        if args.is_empty() {
102            return Ok(Self::default_fixture());
103        }
104        let cards = cx.call_function(&runner_cards_symbol(), Args::new(args))?;
105        let expr = cards.object().as_expr(cx)?;
106        let Expr::List(items) = expr else {
107            return Err(Error::Eval(
108                "runner/cards must return a list of model-card transcripts".to_owned(),
109            ));
110        };
111        Self::from_runner_card_exprs(items)
112    }
113
114    /// Returns the catalog's models in advertised order.
115    pub fn models(&self) -> &[OpenAiModel] {
116        &self.models
117    }
118
119    fn from_parts(runner_models: Vec<OpenAiModel>) -> Self {
120        let mut seen = BTreeSet::new();
121        let mut models = Vec::new();
122        push_unique(&mut models, &mut seen, OpenAiModel::fixture_echo());
123        for model in runner_models {
124            push_unique(&mut models, &mut seen, model);
125        }
126        push_unique(
127            &mut models,
128            &mut seen,
129            OpenAiModel::sim_native(SIM_BROWSE_PLAN_MODEL),
130        );
131        Self { models }
132    }
133
134    fn to_json(&self) -> Value {
135        json!({
136            "object": "list",
137            "data": self.models.iter().map(OpenAiModel::to_json).collect::<Vec<_>>(),
138        })
139    }
140}
141
142/// Handles `GET /v1/models`, returning the catalog built from the registered
143/// runner model cards.
144pub fn handle_models(_request: &GatewayRequest, state: &GatewayRouteState) -> GatewayResponse {
145    models_response_for_catalog(&ModelCatalog::from_model_cards(state.runners().cards()))
146}
147
148/// Returns a `/v1/models` response for the fixture-only catalog.
149pub fn models_response() -> GatewayResponse {
150    models_response_for_catalog(&ModelCatalog::default_fixture())
151}
152
153/// Returns a `/v1/models` response for the catalog produced by calling
154/// `runner/cards` with the given arguments.
155pub fn models_response_for_runner_args(
156    cx: &mut Cx,
157    args: Vec<SimValue>,
158) -> Result<GatewayResponse> {
159    let catalog = ModelCatalog::from_runner_args(cx, args)?;
160    Ok(models_response_for_catalog(&catalog))
161}
162
163/// Encodes the given catalog as the OpenAI `list`-of-models JSON body.
164pub fn models_response_for_catalog(catalog: &ModelCatalog) -> GatewayResponse {
165    GatewayResponse::json(200, catalog.to_json().to_string().into_bytes())
166}
167
168/// Returns the `runner/cards` function symbol used to fetch model cards.
169pub fn runner_cards_symbol() -> Symbol {
170    Symbol::qualified("runner", "cards")
171}
172
173fn push_unique(models: &mut Vec<OpenAiModel>, seen: &mut BTreeSet<String>, model: OpenAiModel) {
174    if seen.insert(model.id.clone()) {
175        models.push(model);
176    }
177}