sim-codec-chat 0.2.1

Canonical chat transcript codec for SIM.
Documentation
use serde_json::{Map, Value};
use sim_kernel::{Error, Expr, Result};

pub(in crate::providers) fn attach_bridge_model_params(
    entries: &[(Expr, Expr)],
    payload: &mut Map<String, Value>,
    reserved_fields: &[&str],
    provider: &str,
) -> Result<()> {
    let Some(calls) = optional_field(entries, "bridge-calls") else {
        return Ok(());
    };
    for call in list_items(calls, "bridge-calls")? {
        let call_entries = map_entries(call, "bridge call")?;
        let Some(params) = optional_field(call_entries, "model-params") else {
            continue;
        };
        for (key, value) in map_entries(params, "bridge model-params")? {
            let key = model_param_key(key)?;
            if reserved_fields.contains(&key.as_str()) {
                return Err(Error::Eval(format!(
                    "{provider} model parameter {key} cannot override provider request field"
                )));
            }
            payload.insert(key, model_param_value(value));
        }
    }
    Ok(())
}

fn optional_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
    entries.iter().find_map(|(key, value)| match key {
        Expr::Symbol(symbol) | Expr::Local(symbol) if symbol.name.as_ref() == name => Some(value),
        Expr::String(text) if text == name => Some(value),
        _ => None,
    })
}

fn list_items<'a>(expr: &'a Expr, context: &str) -> Result<&'a [Expr]> {
    match expr {
        Expr::List(items) | Expr::Vector(items) => Ok(items),
        _ => Err(Error::Eval(format!("{context} must be a list"))),
    }
}

fn map_entries<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
    match expr {
        Expr::Map(entries) => Ok(entries),
        _ => Err(Error::Eval(format!("{context} must be a map"))),
    }
}

fn model_param_key(expr: &Expr) -> Result<String> {
    match expr {
        Expr::Symbol(symbol) | Expr::Local(symbol) => {
            Ok(symbol.as_qualified_str().replace('-', "_"))
        }
        Expr::String(text) => Ok(text.replace('-', "_")),
        other => Err(Error::Eval(format!(
            "bridge model parameter key must be a symbol or string, found {other:?}"
        ))),
    }
}

fn model_param_value(expr: &Expr) -> Value {
    match expr {
        Expr::Nil => Value::Null,
        Expr::Bool(flag) => Value::Bool(*flag),
        Expr::Number(number) => json_number(&number.canonical)
            .map(Value::Number)
            .unwrap_or_else(|| Value::String(number.canonical.clone())),
        Expr::String(text) => json_number(text)
            .map(Value::Number)
            .unwrap_or_else(|| Value::String(text.clone())),
        Expr::Symbol(symbol) | Expr::Local(symbol) => Value::String(symbol.as_qualified_str()),
        Expr::List(items) | Expr::Vector(items) => {
            Value::Array(items.iter().map(model_param_value).collect())
        }
        Expr::Map(entries) => {
            let mut object = Map::new();
            for (key, value) in entries {
                let key = match model_param_key(key) {
                    Ok(key) => key,
                    Err(_) => format!("{key:?}"),
                };
                object.insert(key, model_param_value(value));
            }
            Value::Object(object)
        }
        other => Value::String(format!("{other:?}")),
    }
}

fn json_number(text: &str) -> Option<serde_json::Number> {
    let Ok(Value::Number(number)) = serde_json::from_str::<Value>(text) else {
        return None;
    };
    Some(number)
}