use std::collections::BTreeSet;
use serde_json::{Map, Value, json};
use sim_kernel::{Args, Cx, Error, Expr, Result, Symbol, Value as SimValue};
use sim_lib_agent_runner_core::ModelCard;
use crate::server::{GatewayRequest, GatewayResponse, GatewayRouteState};
pub const MODELS_PATH: &str = "/v1/models";
const FIXTURE_ECHO_MODEL: &str = "fixture/echo";
const SIM_BROWSE_PLAN_MODEL: &str = "sim/browse/plan";
#[derive(Clone, Debug, PartialEq)]
pub struct OpenAiModel {
id: String,
owned_by: String,
metadata: Map<String, Value>,
}
impl OpenAiModel {
pub fn new(id: impl Into<String>, owned_by: impl Into<String>) -> Self {
Self {
id: id.into(),
owned_by: owned_by.into(),
metadata: Map::new(),
}
}
pub fn fixture_echo() -> Self {
Self::new(FIXTURE_ECHO_MODEL, "sim")
}
pub fn sim_native(id: impl Into<String>) -> Self {
Self::new(id, "sim")
}
pub fn from_model_card(card: ModelCard) -> Self {
let mut metadata = Map::new();
metadata.insert("runner".to_owned(), Value::String(card.runner.to_string()));
metadata.insert(
"locality".to_owned(),
Value::String(card.locality.to_string()),
);
for (key, value) in card.extra {
metadata.insert(metadata_key(&key), metadata_value(&value));
}
Self {
id: card.model,
owned_by: card.provider.to_string(),
metadata,
}
}
pub fn id(&self) -> &str {
&self.id
}
fn to_json(&self) -> Value {
let mut object = Map::new();
object.insert("id".to_owned(), Value::String(self.id.clone()));
object.insert("object".to_owned(), Value::String("model".to_owned()));
object.insert("created".to_owned(), json!(0));
object.insert("owned_by".to_owned(), Value::String(self.owned_by.clone()));
if !self.metadata.is_empty() {
object.insert("metadata".to_owned(), Value::Object(self.metadata.clone()));
}
Value::Object(object)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ModelCatalog {
models: Vec<OpenAiModel>,
}
impl ModelCatalog {
pub fn default_fixture() -> Self {
Self::from_parts(Vec::new())
}
pub fn from_runner_card_exprs(cards: impl IntoIterator<Item = Expr>) -> Result<Self> {
let cards = cards
.into_iter()
.map(ModelCard::try_from)
.collect::<Result<Vec<_>>>()?;
Ok(Self::from_model_cards(cards))
}
pub fn from_model_cards(cards: impl IntoIterator<Item = ModelCard>) -> Self {
let runner_models = cards
.into_iter()
.map(OpenAiModel::from_model_card)
.collect::<Vec<_>>();
Self::from_parts(runner_models)
}
pub fn from_runner_args(cx: &mut Cx, args: Vec<SimValue>) -> Result<Self> {
if args.is_empty() {
return Ok(Self::default_fixture());
}
let cards = cx.call_function(&runner_cards_symbol(), Args::new(args))?;
let expr = cards.object().as_expr(cx)?;
let Expr::List(items) = expr else {
return Err(Error::Eval(
"runner/cards must return a list of model-card transcripts".to_owned(),
));
};
Self::from_runner_card_exprs(items)
}
pub fn models(&self) -> &[OpenAiModel] {
&self.models
}
fn from_parts(runner_models: Vec<OpenAiModel>) -> Self {
let mut seen = BTreeSet::new();
let mut models = Vec::new();
push_unique(&mut models, &mut seen, OpenAiModel::fixture_echo());
for model in runner_models {
push_unique(&mut models, &mut seen, model);
}
push_unique(
&mut models,
&mut seen,
OpenAiModel::sim_native(SIM_BROWSE_PLAN_MODEL),
);
Self { models }
}
fn to_json(&self) -> Value {
json!({
"object": "list",
"data": self.models.iter().map(OpenAiModel::to_json).collect::<Vec<_>>(),
})
}
}
pub fn handle_models(_request: &GatewayRequest, state: &GatewayRouteState) -> GatewayResponse {
models_response_for_catalog(&ModelCatalog::from_model_cards(state.runners().cards()))
}
pub fn models_response() -> GatewayResponse {
models_response_for_catalog(&ModelCatalog::default_fixture())
}
pub fn models_response_for_runner_args(
cx: &mut Cx,
args: Vec<SimValue>,
) -> Result<GatewayResponse> {
let catalog = ModelCatalog::from_runner_args(cx, args)?;
Ok(models_response_for_catalog(&catalog))
}
pub fn models_response_for_catalog(catalog: &ModelCatalog) -> GatewayResponse {
GatewayResponse::json(200, catalog.to_json().to_string().into_bytes())
}
pub fn runner_cards_symbol() -> Symbol {
Symbol::qualified("runner", "cards")
}
fn push_unique(models: &mut Vec<OpenAiModel>, seen: &mut BTreeSet<String>, model: OpenAiModel) {
if seen.insert(model.id.clone()) {
models.push(model);
}
}
fn metadata_key(expr: &Expr) -> String {
match expr {
Expr::Symbol(symbol) | Expr::Local(symbol) if symbol.namespace.is_none() => {
normalize_metadata_key(symbol.name.as_ref())
}
Expr::Symbol(symbol) | Expr::Local(symbol) => normalize_metadata_key(&symbol.to_string()),
Expr::String(value) => normalize_metadata_key(value),
other => normalize_metadata_key(&format!("{other:?}")),
}
}
fn normalize_metadata_key(value: &str) -> String {
value
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || ch == '_' {
ch
} else {
'_'
}
})
.collect()
}
fn metadata_value(expr: &Expr) -> Value {
match expr {
Expr::Nil => Value::Null,
Expr::Bool(value) => Value::Bool(*value),
Expr::Number(value) => number_json(&value.canonical),
Expr::Symbol(symbol) | Expr::Local(symbol) if symbol.namespace.is_none() => {
Value::String(symbol.name.as_ref().to_owned())
}
Expr::Symbol(symbol) | Expr::Local(symbol) => Value::String(symbol.to_string()),
Expr::String(value) => Value::String(value.clone()),
Expr::Bytes(bytes) => Value::Array(bytes.iter().map(|byte| json!(byte)).collect()),
Expr::List(values) | Expr::Vector(values) | Expr::Set(values) | Expr::Block(values) => {
Value::Array(values.iter().map(metadata_value).collect())
}
Expr::Map(entries) => {
let mut object = Map::new();
for (key, value) in entries {
object.insert(metadata_key(key), metadata_value(value));
}
Value::Object(object)
}
other => Value::String(format!("{other:?}")),
}
}
fn number_json(canonical: &str) -> Value {
serde_json::from_str(canonical).unwrap_or_else(|_| Value::String(canonical.to_owned()))
}