sim-lib-openai-server 0.1.1

OpenAI-compatible gateway skeleton for SIM.
Documentation
use std::collections::BTreeSet;

use serde_json::{Map, Value};
use sim_codec_json::{JsonProjectionMode, project_expr_to_json, project_json_to_expr};
use sim_kernel::{Cx, Error, Expr, Result, Symbol};

pub(crate) fn default_parameters() -> Value {
    serde_json::json!({"type":"object","properties":{},"required":[]})
}

pub(crate) fn argument_order(parameters: &Value) -> Vec<String> {
    let mut seen = BTreeSet::new();
    let mut order = Vec::new();
    if let Some(required) = parameters.get("required").and_then(Value::as_array) {
        for name in required.iter().filter_map(Value::as_str) {
            if seen.insert(name.to_owned()) {
                order.push(name.to_owned());
            }
        }
    }
    if let Some(properties) = parameters.get("properties").and_then(Value::as_object) {
        for name in properties.keys() {
            if seen.insert(name.clone()) {
                order.push(name.clone());
            }
        }
    }
    order
}

pub(crate) fn validate_schema(
    schema: &Value,
    value: &Value,
    path: &str,
) -> std::result::Result<(), String> {
    if let Some(kind) = schema.get("type").and_then(Value::as_str) {
        validate_type(kind, value, path)?;
    }
    if let Some(object) = value.as_object() {
        validate_required(schema, object, path)?;
        validate_properties(schema, object, path)?;
    }
    Ok(())
}

pub(crate) fn json_to_expr(value: &Value) -> Expr {
    project_json_to_expr(value, JsonProjectionMode::UntaggedInterop)
}

pub(crate) fn json_to_value(cx: &mut Cx, value: &Value) -> Result<sim_kernel::Value> {
    match value {
        Value::Null => cx.factory().nil(),
        Value::Bool(value) => cx.factory().bool(*value),
        Value::Number(number) => cx
            .factory()
            .number_literal(Symbol::qualified("numbers", "f64"), number.to_string()),
        Value::String(value) => cx.factory().string(value.clone()),
        Value::Array(items) => items
            .iter()
            .map(|item| json_to_value(cx, item))
            .collect::<Result<Vec<_>>>()
            .and_then(|items| cx.factory().list(items)),
        Value::Object(entries) => entries
            .iter()
            .map(|(key, value)| {
                json_to_value(cx, value).map(|value| (Symbol::new(key.clone()), value))
            })
            .collect::<Result<Vec<_>>>()
            .and_then(|entries| cx.factory().table(entries)),
    }
}

pub(crate) fn arguments_json(expr: &Expr) -> Result<Value> {
    match expr {
        Expr::String(text) => serde_json::from_str(text)
            .map_err(|err| Error::Eval(format!("tool-call arguments must be JSON: {err}"))),
        other => Ok(project_expr_to_json(
            other,
            JsonProjectionMode::UntaggedInterop,
        )),
    }
}

pub(crate) fn expr_to_json_string(expr: &Expr) -> Value {
    project_expr_to_json(expr, JsonProjectionMode::TextLossy)
}

pub(crate) fn canonical_json(value: &Value) -> String {
    serde_json::to_string(value).unwrap_or_else(|_| format!("{value:?}"))
}

pub(crate) fn expr_text(expr: &Expr) -> String {
    match expr {
        Expr::String(text) => text.clone(),
        Expr::Number(number) => number.canonical.clone(),
        Expr::Bool(value) => value.to_string(),
        Expr::Nil => "nil".to_owned(),
        other => format!("{other:?}"),
    }
}

fn validate_type(kind: &str, value: &Value, path: &str) -> std::result::Result<(), String> {
    let ok = match kind {
        "object" => value.is_object(),
        "array" => value.is_array(),
        "string" => value.is_string(),
        "number" => value.is_number(),
        "integer" => value.as_i64().is_some() || value.as_u64().is_some(),
        "boolean" => value.is_boolean(),
        "null" => value.is_null(),
        other => return Err(format!("{path} uses unsupported json schema type {other}")),
    };
    if ok {
        Ok(())
    } else {
        Err(format!("{path} must be {kind}"))
    }
}

fn validate_required(
    schema: &Value,
    object: &Map<String, Value>,
    path: &str,
) -> std::result::Result<(), String> {
    let Some(required) = schema.get("required").and_then(Value::as_array) else {
        return Ok(());
    };
    for item in required {
        let Some(name) = item.as_str() else {
            return Err(format!("{path}.required must contain strings"));
        };
        if !object.contains_key(name) {
            return Err(format!("{path}.{name} is required"));
        }
    }
    Ok(())
}

fn validate_properties(
    schema: &Value,
    object: &Map<String, Value>,
    path: &str,
) -> std::result::Result<(), String> {
    let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
        return Ok(());
    };
    for (name, property_schema) in properties {
        if let Some(value) = object.get(name) {
            validate_schema(property_schema, value, &format!("{path}.{name}"))?;
        }
    }
    Ok(())
}