systemprompt-api 0.4.2

Axum-based HTTP server and API gateway for systemprompt.io AI governance infrastructure. Exposes governed agents, MCP, A2A, and admin endpoints with rate limiting and RBAC.
Documentation
use bytes::Bytes;
use serde_json::Value;

pub fn flatten_system_prompt(system: &Value) -> Option<String> {
    match system {
        Value::String(s) if !s.is_empty() => Some(s.clone()),
        Value::Array(arr) => {
            let joined = arr
                .iter()
                .filter_map(|block| block.get("text").and_then(Value::as_str))
                .collect::<Vec<_>>()
                .join("\n");
            if joined.is_empty() {
                None
            } else {
                Some(joined)
            }
        },
        _ => None,
    }
}

pub fn flatten_message_content(content: &Value) -> String {
    match content {
        Value::String(s) => s.clone(),
        Value::Array(blocks) => {
            let mut out = String::new();
            for block in blocks {
                append_block(&mut out, block);
            }
            out
        },
        _ => serialize_or_warn(content, "flatten: content serialize failed"),
    }
}

fn append_block(out: &mut String, block: &Value) {
    let kind = block.get("type").and_then(Value::as_str).unwrap_or("");
    if kind == "text" {
        if let Some(text) = block.get("text").and_then(Value::as_str) {
            push_with_sep(out, text);
        }
        return;
    }
    match serde_json::to_string(block) {
        Ok(s) => push_with_sep(out, &s),
        Err(e) => tracing::warn!(error = %e, "flatten: block serialize failed"),
    }
}

fn push_with_sep(out: &mut String, fragment: &str) {
    if !out.is_empty() {
        out.push('\n');
    }
    out.push_str(fragment);
}

fn serialize_or_warn(value: &Value, context: &'static str) -> String {
    match serde_json::to_string(value) {
        Ok(s) => s,
        Err(e) => {
            tracing::warn!(error = %e, "{context}");
            String::new()
        },
    }
}

pub fn rewrite_request_model(raw_body: Bytes, upstream_model: &str) -> anyhow::Result<Bytes> {
    serde_json::from_slice::<Value>(&raw_body).map_or_else(
        |_| Ok(raw_body),
        |mut v| {
            if let Some(obj) = v.as_object_mut() {
                obj.insert(
                    "model".to_string(),
                    Value::String(upstream_model.to_string()),
                );
            }
            serde_json::to_vec(&v)
                .map(Bytes::from)
                .map_err(|e| anyhow::anyhow!("re-serialize request body with upstream model: {e}"))
        },
    )
}

pub fn parse_served_model(response_bytes: &Bytes) -> Option<String> {
    serde_json::from_slice::<Value>(response_bytes)
        .ok()
        .and_then(|v| {
            v.get("model")
                .and_then(Value::as_str)
                .map(ToString::to_string)
        })
}