typedlm 0.1.0

Typed, testable LLM programs for Rust
Documentation
//! Turning a provider response into a validated, typed output.

use serde_json::Value;

use crate::json::extract_json;
use crate::schema::validate;
use crate::{Error, FinishReason, Response, Signature};

/// JSON Schema of a signature's output, as generated by `schemars`.
pub fn output_schema<S: Signature>() -> Value {
    schemars::schema_for!(S::Output).to_value()
}

/// Runs the validation pipeline on one response:
/// finish reason → JSON extraction → schema validation → deserialization →
/// [`Signature::validate`].
///
/// `schema` is the output schema as generated ([`output_schema`]), not a
/// provider-specific variant of it.
pub fn parse_output<S: Signature>(response: &Response, schema: &Value) -> Result<S::Output, Error> {
    match response.finish {
        FinishReason::Refusal => {
            return Err(Error::Refusal {
                model: response.model.clone(),
                message: response.content.clone(),
            });
        }
        FinishReason::Length => {
            return Err(Error::Truncated {
                model: response.model.clone(),
            });
        }
        FinishReason::Stop => {}
    }

    let value = extract_json(&response.content).map_err(|reason| Error::InvalidJson {
        reason,
        response: response.content.clone(),
    })?;

    let violations = validate(schema, &value);
    if !violations.is_empty() {
        return Err(Error::SchemaViolation {
            violations,
            response: response.content.clone(),
        });
    }

    let output: S::Output = serde_json::from_value(value).map_err(|e| Error::Deserialize {
        message: e.to_string(),
        response: response.content.clone(),
    })?;

    S::validate(&output).map_err(|errors| Error::Validation { errors })?;
    Ok(output)
}