va-ai-api-bridge 0.1.7

Protocol translation primitives for AI API request and response shapes.
Documentation
use serde_json::{Number, Value};

pub(super) fn prepare_chat_request(chat_request: &mut Value) {
    let Some(object) = chat_request.as_object_mut() else {
        return;
    };

    normalize_system_messages(object);
    clamp_f64_setting(object, "temperature", 1.0);
    clamp_f64_setting(object, "top_p", 0.95);
    clamp_u64_setting(object, "max_completion_tokens", 2048);
}

fn normalize_system_messages(object: &mut serde_json::Map<String, Value>) {
    let Some(messages) = object.get_mut("messages").and_then(Value::as_array_mut) else {
        return;
    };

    let mut system_parts = Vec::new();
    let mut rest = Vec::with_capacity(messages.len());

    for message in std::mem::take(messages) {
        if message.get("role").and_then(Value::as_str) == Some("system") {
            if let Some(content) = message.get("content").and_then(content_to_text) {
                if !content.is_empty() {
                    system_parts.push(content);
                }
            }
        } else {
            rest.push(message);
        }
    }

    if !system_parts.is_empty() {
        rest.insert(
            0,
            serde_json::json!({
                "role": "system",
                "content": system_parts.join("\n\n")
            }),
        );
    }

    *messages = rest;
}

fn content_to_text(content: &Value) -> Option<String> {
    match content {
        Value::String(text) => Some(text.trim().to_string()),
        Value::Array(parts) => {
            let text = parts
                .iter()
                .filter_map(|part| {
                    part.get("text")
                        .or_else(|| part.get("input_text"))
                        .and_then(Value::as_str)
                })
                .filter(|text| !text.trim().is_empty())
                .collect::<Vec<_>>()
                .join("\n\n");
            Some(text)
        }
        _ => None,
    }
}

fn clamp_f64_setting(object: &mut serde_json::Map<String, Value>, key: &str, fallback: f64) {
    let Some(value) = object.get(key) else {
        return;
    };
    let next = value
        .as_f64()
        .filter(|value| *value > 0.0 && *value <= 1.0)
        .unwrap_or(fallback);
    if let Some(number) = Number::from_f64(next) {
        object.insert(key.to_string(), Value::Number(number));
    } else {
        object.remove(key);
    }
}

fn clamp_u64_setting(object: &mut serde_json::Map<String, Value>, key: &str, max: u64) {
    let Some(value) = object.get(key) else {
        return;
    };
    let next = value
        .as_u64()
        .filter(|value| *value >= 1)
        .unwrap_or(max)
        .min(max);
    object.insert(key.to_string(), Value::Number(next.into()));
}