llama-runner 2.3.2

A straightforward Rust library for running llama.cpp models locally on device
Documentation
/// Following is generated by Claude
use std::collections::BTreeMap;

use serde_json::Value;

use crate::mcp::{
    Gemma4ToolFunctionParamsProp, Gemma4ToolFunctionParamsPropArrayItems,
    Gemma4ToolFunctionParamsPropType, error::ParseToolError,
};

/// Resolve a `$ref` like `"#/$defs/MyEnum"` against the root schema object.
/// Returns the referenced sub-schema, or `None` if the reference cannot be
/// followed.
pub fn resolve_ref<'a>(root: &'a Value, ref_str: &str) -> Option<&'a Value> {
    // Only handle internal references that start with "#/"
    let path = ref_str.strip_prefix("#/")?;
    let mut current: &Value = root;
    for segment in path.split('/') {
        // JSON Pointer escape sequences
        let key = segment.replace("~1", "/").replace("~0", "~");
        current = current.get(&key)?;
    }
    Some(current)
}

/// Map a JSON Schema primitive type string to our enum.
pub fn map_type(type_str: &str) -> Gemma4ToolFunctionParamsPropType {
    match type_str {
        "string" => Gemma4ToolFunctionParamsPropType::String,
        "array" => Gemma4ToolFunctionParamsPropType::Array,
        "number" | "integer" => Gemma4ToolFunctionParamsPropType::Number,
        "boolean" => Gemma4ToolFunctionParamsPropType::Boolean,
        _ => Gemma4ToolFunctionParamsPropType::Object,
    }
}

/// Walk a `anyOf` / `oneOf` array and decide:
///   - Is the whole thing nullable? (one variant is `{"type":"null"}`)
///   - What is the "real" schema we should convert? (the non-null variant, or a
///     synthetic object merging all variants for proper enums / unions)
///
/// Returns `(is_nullable, effective_schema)`.
pub fn unwrap_any_of<'a>(variants: &'a [Value], root: &'a Value) -> (bool, Option<&'a Value>) {
    let mut nullable = false;
    let mut non_null: Vec<&Value> = Vec::new();

    for v in variants {
        // Resolve $ref first
        let resolved: &Value = if let Some(r) = v.get("$ref").and_then(|r| r.as_str()) {
            match resolve_ref(root, r) {
                Some(rv) => rv,
                None => v,
            }
        } else {
            v
        };

        if resolved.get("type").and_then(|t| t.as_str()) == Some("null") {
            nullable = true;
        } else {
            non_null.push(resolved);
        }
    }

    let effective = if non_null.len() == 1 {
        Some(non_null[0])
    } else {
        // Multiple non-null variants: return the first; callers will treat the
        // whole thing as a generic Object.
        non_null.first().copied()
    };

    (nullable, effective)
}

/// Convert a single JSON-Schema node (already dereferenced) into a
/// `Gemma4ToolFunctionParamsProp`.
///
/// `root` – the top-level schema object, needed for `$ref` resolution.
/// `field_path` – dotted path used in error messages.
pub fn convert_schema_node(
    node: &Value,
    root: &Value,
    field_path: &str,
) -> Result<Gemma4ToolFunctionParamsProp, ParseToolError> {
    // ── Resolve $ref ──────────────────────────────────────────────────────────
    if let Some(ref_str) = node.get("$ref").and_then(|r| r.as_str()) {
        let resolved = resolve_ref(root, ref_str)
            .ok_or_else(|| ParseToolError(format!("{field_path}.$ref({ref_str})").into()))?;
        return convert_schema_node(resolved, root, field_path);
    }

    let description = node
        .get("description")
        .and_then(|d| d.as_str())
        .unwrap_or_default()
        .to_string();

    // ── anyOf / oneOf ─────────────────────────────────────────────────────────
    if let Some(variants) = node
        .get("anyOf")
        .or_else(|| node.get("oneOf"))
        .and_then(|v| v.as_array())
    {
        let (nullable, effective) = unwrap_any_of(variants, root);

        return if let Some(eff) = effective {
            let mut prop = convert_schema_node(eff, root, field_path)?;
            if nullable {
                prop.nullable = true;
            }
            // Preserve description from the wrapping node if the inner one
            // has none.
            if prop.description.is_empty() && !description.is_empty() {
                prop.description = description;
            }
            Ok(prop)
        } else {
            // Completely empty anyOf – treat as opaque Object
            Ok(Gemma4ToolFunctionParamsProp {
                description,
                type_: Gemma4ToolFunctionParamsPropType::Object,
                nullable,
                ..Default::default()
            })
        };
    }

    // ── Concrete type ─────────────────────────────────────────────────────────
    let type_str = node
        .get("type")
        .and_then(|t| t.as_str())
        .unwrap_or("object");

    match type_str {
        // ── String (with optional enum) ───────────────────────────────────────
        "string" => {
            let enum_vals: Vec<String> = node
                .get("enum")
                .and_then(|e| e.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|v| v.as_str().map(str::to_string))
                        .collect()
                })
                .unwrap_or_default();

            Ok(Gemma4ToolFunctionParamsProp {
                description,
                type_: Gemma4ToolFunctionParamsPropType::String,
                enum_: enum_vals,
                ..Default::default()
            })
        }

        // ── Number / Integer ──────────────────────────────────────────────────
        "number" | "integer" => Ok(Gemma4ToolFunctionParamsProp {
            description,
            type_: Gemma4ToolFunctionParamsPropType::Number,
            ..Default::default()
        }),

        // ── Boolean ───────────────────────────────────────────────────────────
        "boolean" => Ok(Gemma4ToolFunctionParamsProp {
            description,
            type_: Gemma4ToolFunctionParamsPropType::Boolean,
            ..Default::default()
        }),

        // ── Array ─────────────────────────────────────────────────────────────
        "array" => {
            let items_node = node.get("items");

            let items = if let Some(items_schema) = items_node {
                // Resolve $ref inside items
                let items_resolved: &Value =
                    if let Some(r) = items_schema.get("$ref").and_then(|r| r.as_str()) {
                        resolve_ref(root, r).unwrap_or(items_schema)
                    } else {
                        items_schema
                    };

                let item_type_str = items_resolved
                    .get("type")
                    .and_then(|t| t.as_str())
                    .unwrap_or("object");

                let item_type = map_type(item_type_str);

                // If items is an object, recursively gather its properties.
                let (item_props, item_required) = if item_type_str == "object" {
                    (
                        convert_properties(items_resolved, root, &format!("{field_path}.items"))?,
                        extract_required(items_resolved, &format!("{field_path}.items"))?,
                    )
                } else {
                    (BTreeMap::new(), vec![])
                };

                Some(Gemma4ToolFunctionParamsPropArrayItems {
                    properties: item_props,
                    required: item_required,
                    type_: item_type,
                })
            } else {
                None
            };

            Ok(Gemma4ToolFunctionParamsProp {
                description,
                type_: Gemma4ToolFunctionParamsPropType::Array,
                items,
                ..Default::default()
            })
        }

        // ── Object (default / fallthrough) ────────────────────────────────────
        _ => {
            let properties = convert_properties(node, root, field_path)?;
            let required = extract_required(node, field_path)?;

            Ok(Gemma4ToolFunctionParamsProp {
                description,
                type_: Gemma4ToolFunctionParamsPropType::Object,
                properties,
                required,
                ..Default::default()
            })
        }
    }
}

/// Convert the `"properties"` map of a schema object node into a
/// `BTreeMap<String, Gemma4ToolFunctionParamsProp>`.
pub fn convert_properties(
    node: &Value,
    root: &Value,
    parent_path: &str,
) -> Result<BTreeMap<String, Gemma4ToolFunctionParamsProp>, ParseToolError> {
    let mut out = BTreeMap::new();

    let Some(props_map) = node.get("properties").and_then(|p| p.as_object()) else {
        return Ok(out);
    };

    for (key, prop_schema) in props_map {
        let field_path = format!("{parent_path}.properties.{key}");
        let prop = convert_schema_node(prop_schema, root, &field_path)?;
        out.insert(key.clone(), prop);
    }

    Ok(out)
}

/// Pull the `"required"` array out of a schema node.
pub fn extract_required(node: &Value, parent_path: &str) -> Result<Vec<String>, ParseToolError> {
    let Some(arr) = node.get("required").and_then(|r| r.as_array()) else {
        return Ok(vec![]);
    };

    arr.iter()
        .enumerate()
        .map(|(i, v)| {
            v.as_str()
                .map(str::to_string)
                .ok_or_else(|| ParseToolError(format!("{parent_path}.required[{i}]").into()))
        })
        .collect()
}