sim-codec-chat 0.1.4

Canonical chat transcript codec for SIM.
Documentation
use serde_json::{Map, Value};
use sim_codec::DecodeBudget;
use sim_codec_json::{JsonProjectionMode, json_number_to_u64, project_json_to_expr_budgeted};
use sim_kernel::{CodecId, Expr, Result, Symbol};

use crate::{model_error_expr, usage_record};

use super::super::ANTHROPIC_CODEC_ID;
use super::super::common::{codec_error, provider_symbol};

pub(in crate::providers::anthropic::decode) fn tool_call_part(
    codec: CodecId,
    object: &Map<String, Value>,
    budget: &mut DecodeBudget,
) -> Result<Expr> {
    Ok(Expr::Map(vec![
        (
            Expr::Symbol(Symbol::new("type")),
            Expr::Symbol(Symbol::new("tool-call")),
        ),
        (
            Expr::Symbol(Symbol::new("id")),
            Expr::String(string_member(codec, object, "id")?.to_owned()),
        ),
        (
            Expr::Symbol(Symbol::new("name")),
            Expr::String(string_member(codec, object, "name")?.to_owned()),
        ),
        (
            Expr::Symbol(Symbol::new("arguments")),
            project_json_to_expr_budgeted(
                object.get("input").unwrap_or(&Value::Object(Map::new())),
                JsonProjectionMode::UntaggedInterop,
                codec,
                budget,
                0,
            )?,
        ),
    ]))
}

pub(in crate::providers::anthropic::decode) fn tool_result_part(
    codec: CodecId,
    object: &Map<String, Value>,
    budget: &mut DecodeBudget,
) -> Result<Expr> {
    let output = object
        .get("content")
        .map(|content| {
            project_json_to_expr_budgeted(
                content,
                JsonProjectionMode::UntaggedInterop,
                codec,
                budget,
                0,
            )
        })
        .transpose()?
        .unwrap_or(Expr::Nil);
    let status = if object
        .get("is_error")
        .and_then(Value::as_bool)
        .unwrap_or(false)
    {
        "error"
    } else {
        "ok"
    };
    Ok(Expr::Map(vec![
        (
            Expr::Symbol(Symbol::new("type")),
            Expr::Symbol(Symbol::new("tool-result")),
        ),
        (
            Expr::Symbol(Symbol::new("tool-call-id")),
            Expr::String(string_member(codec, object, "tool_use_id")?.to_owned()),
        ),
        (
            Expr::Symbol(Symbol::new("status")),
            Expr::Symbol(Symbol::new(status)),
        ),
        (Expr::Symbol(Symbol::new("output")), output),
    ]))
}

pub(in crate::providers::anthropic::decode) fn raw_content_part(
    codec: CodecId,
    kind: &str,
    value: &Value,
    budget: &mut DecodeBudget,
) -> Result<Expr> {
    Ok(Expr::Map(vec![
        (
            Expr::Symbol(Symbol::new("type")),
            Expr::Symbol(provider_symbol(kind)),
        ),
        (
            Expr::Symbol(Symbol::new("raw-provider-part")),
            project_json_to_expr_budgeted(
                value,
                JsonProjectionMode::UntaggedInterop,
                codec,
                budget,
                0,
            )?,
        ),
    ]))
}

pub(in crate::providers::anthropic::decode) fn error_response_expr(
    runner: Symbol,
    model: &str,
    message: String,
    include_raw: bool,
    raw: Option<&Value>,
    budget: &mut DecodeBudget,
) -> Result<Expr> {
    let mut entries = match model_error_expr(runner, model, message) {
        Expr::Map(entries) => entries,
        _ => unreachable!("model_error_expr always returns a map"),
    };
    if include_raw && let Some(raw) = raw {
        entries.push((
            Expr::Symbol(Symbol::new("raw-provider-response")),
            raw_provider_expr(raw, budget)?,
        ));
    }
    Ok(Expr::Map(entries))
}

pub(in crate::providers::anthropic::decode) fn usage_expr_from_value(
    value: &Value,
) -> Result<Option<Expr>> {
    let Some(usage) = value.get("usage").and_then(Value::as_object) else {
        return Ok(None);
    };
    let input = usage.get("input_tokens").and_then(json_number_to_u64);
    let output = usage.get("output_tokens").and_then(json_number_to_u64);
    let total = input
        .zip(output)
        .map(|(input, output)| input.saturating_add(output));
    let fields = usage_record(input, output, total);
    Ok((!fields.is_empty()).then_some(Expr::Map(fields)))
}

pub(in crate::providers::anthropic::decode) fn raw_provider_expr(
    value: &Value,
    budget: &mut DecodeBudget,
) -> Result<Expr> {
    project_json_to_expr_budgeted(
        value,
        JsonProjectionMode::UntaggedInterop,
        ANTHROPIC_CODEC_ID,
        budget,
        0,
    )
}

pub(in crate::providers::anthropic::decode) fn error_message(
    object: Option<&Map<String, Value>>,
) -> Option<String> {
    let error = object?.get("error")?;
    match error {
        Value::String(message) => Some(message.clone()),
        Value::Object(fields) => fields
            .get("message")
            .and_then(Value::as_str)
            .map(str::to_owned)
            .or_else(|| {
                fields
                    .get("type")
                    .and_then(Value::as_str)
                    .map(str::to_owned)
            }),
        _ => None,
    }
}

pub(in crate::providers::anthropic::decode) fn model_event_expr(
    event: &str,
    runner: Symbol,
    model: &str,
    span_id: Expr,
    extra: Vec<(Expr, Expr)>,
) -> Expr {
    let mut entries = vec![
        (Expr::Symbol(Symbol::new("model-event")), Expr::Bool(true)),
        (
            Expr::Symbol(Symbol::new("event")),
            Expr::Symbol(Symbol::new(event)),
        ),
        (Expr::Symbol(Symbol::new("runner")), Expr::Symbol(runner)),
        (
            Expr::Symbol(Symbol::new("model")),
            Expr::String(model.to_owned()),
        ),
        (Expr::Symbol(Symbol::new("span-id")), span_id),
    ];
    entries.extend(extra);
    Expr::Map(entries)
}

pub(in crate::providers::anthropic::decode) fn final_event_expr(
    runner: Symbol,
    model: &str,
    span_id: Expr,
    response: Expr,
) -> Expr {
    model_event_expr(
        "final",
        runner,
        model,
        span_id,
        vec![(Expr::Symbol(Symbol::new("response")), response)],
    )
}

pub(in crate::providers::anthropic::decode) fn event_response(event: &Expr) -> Option<&Expr> {
    let Expr::Map(entries) = event else {
        return None;
    };
    entries.iter().find_map(|(key, value)| match key {
        Expr::Symbol(symbol) if symbol.name.as_ref() == "response" => Some(value),
        _ => None,
    })
}

pub(in crate::providers::anthropic::decode) fn string_member<'a>(
    codec: CodecId,
    object: &'a Map<String, Value>,
    name: &str,
) -> Result<&'a str> {
    object
        .get(name)
        .and_then(Value::as_str)
        .ok_or_else(|| codec_error(codec, format!("anthropic missing string {name}")))
}