hanzo 0.1.0

Hanzo AI — Rust SDK and CLI for the Hanzo ecosystem
Documentation
//! # Hanzo — Rust SDK + CLI for the Hanzo ecosystem
//!
//! `hanzo` is both a library (this crate, the SDK) and the `hanzo` command-line tool.
//! It is the single Rust entry point to the Hanzo platform. Today it speaks to the
//! inference engine ([`hanzo-engine`], an OpenAI-compatible server); the rest of the
//! ecosystem — the `dev` coding tool ([hanzoai/dev]), KMS, IAM, … — plugs in behind the
//! same [`Client`] surface, mirroring the Python SDK ([hanzoai/python-sdk]) and using
//! the node's native ZAP transport where applicable.
//!
//! ```no_run
//! let hanzo = hanzo::Client::from_env();
//! let reply = hanzo.engine().chat("default", &[hanzo::msg("user", "hello")])?;
//! println!("{}", reply.text());
//! # Ok::<(), hanzo::Error>(())
//! ```
//!
//! [hanzoai/dev]: https://github.com/hanzoai/dev
//! [hanzoai/python-sdk]: https://github.com/hanzoai/python-sdk

use serde::{Deserialize, Serialize};

/// Errors returned by the SDK.
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("transport error: {0}")]
    Transport(#[from] reqwest::Error),
    #[error("api error {status}: {message}")]
    Api { status: u16, message: String },
    #[error("config error: {0}")]
    Config(String),
}

pub type Result<T> = std::result::Result<T, Error>;

/// Default engine endpoint (the local `hanzo-engine` OpenAI-compatible server).
pub const DEFAULT_BASE_URL: &str = "http://localhost:36900";

/// The Hanzo client — the root of the SDK. Holds connection config and exposes one
/// accessor per ecosystem service (`engine()` today; `dev()`, `kms()`, `iam()` as they
/// are wired — never as non-working stubs).
#[derive(Clone)]
pub struct Client {
    base_url: String,
    api_key: Option<String>,
    http: reqwest::blocking::Client,
}

impl Client {
    /// Create a client pointed at `base_url`.
    pub fn new(base_url: impl Into<String>) -> Self {
        Client {
            base_url: base_url.into(),
            api_key: None,
            http: reqwest::blocking::Client::new(),
        }
    }

    /// Attach a bearer API key.
    pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
        self.api_key = Some(key.into());
        self
    }

    /// Build from the environment: `HANZO_BASE_URL` (default [`DEFAULT_BASE_URL`]) and
    /// `HANZO_API_KEY`.
    pub fn from_env() -> Self {
        let base = std::env::var("HANZO_BASE_URL").unwrap_or_else(|_| DEFAULT_BASE_URL.to_string());
        let mut c = Client::new(base);
        if let Ok(k) = std::env::var("HANZO_API_KEY") {
            c = c.with_api_key(k);
        }
        c
    }

    /// The inference engine service (`hanzo-engine`): chat, embeddings, models.
    pub fn engine(&self) -> Engine<'_> {
        Engine { client: self }
    }

    fn post<B: Serialize, R: for<'de> Deserialize<'de>>(&self, path: &str, body: &B) -> Result<R> {
        let mut req = self.http.post(format!("{}{}", self.base_url, path)).json(body);
        if let Some(k) = &self.api_key {
            req = req.bearer_auth(k);
        }
        self.send(req)
    }

    fn get<R: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<R> {
        let mut req = self.http.get(format!("{}{}", self.base_url, path));
        if let Some(k) = &self.api_key {
            req = req.bearer_auth(k);
        }
        self.send(req)
    }

    fn send<R: for<'de> Deserialize<'de>>(&self, req: reqwest::blocking::RequestBuilder) -> Result<R> {
        let resp = req.send()?;
        let status = resp.status();
        if !status.is_success() {
            return Err(Error::Api {
                status: status.as_u16(),
                message: resp.text().unwrap_or_default(),
            });
        }
        Ok(resp.json()?)
    }
}

/// Inference engine service — mirrors the OpenAI-compatible surface of `hanzo-engine`.
pub struct Engine<'a> {
    client: &'a Client,
}

impl Engine<'_> {
    /// Chat completion.
    pub fn chat(&self, model: &str, messages: &[Message]) -> Result<ChatResponse> {
        self.client.post("/v1/chat/completions", &ChatRequest { model, messages })
    }

    /// Embeddings for one or more inputs.
    pub fn embeddings(&self, model: &str, input: Vec<String>) -> Result<EmbeddingsResponse> {
        self.client.post("/v1/embeddings", &EmbeddingsRequest { model, input })
    }

    /// List available models.
    pub fn models(&self) -> Result<ModelList> {
        self.client.get("/v1/models")
    }
}

/// A chat message.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
    pub role: String,
    pub content: String,
}

/// Convenience constructor for a [`Message`].
pub fn msg(role: &str, content: impl Into<String>) -> Message {
    Message { role: role.to_string(), content: content.into() }
}

#[derive(Serialize)]
struct ChatRequest<'a> {
    model: &'a str,
    messages: &'a [Message],
}

#[derive(Debug, Deserialize)]
pub struct ChatResponse {
    pub choices: Vec<ChatChoice>,
}

impl ChatResponse {
    /// The assistant text from the first choice (empty if none).
    pub fn text(&self) -> &str {
        self.choices.first().map(|c| c.message.content.as_str()).unwrap_or("")
    }
}

#[derive(Debug, Deserialize)]
pub struct ChatChoice {
    pub message: Message,
}

#[derive(Serialize)]
struct EmbeddingsRequest<'a> {
    model: &'a str,
    input: Vec<String>,
}

#[derive(Debug, Deserialize)]
pub struct EmbeddingsResponse {
    pub data: Vec<Embedding>,
}

#[derive(Debug, Deserialize)]
pub struct Embedding {
    pub embedding: Vec<f32>,
}

#[derive(Debug, Deserialize)]
pub struct ModelList {
    pub data: Vec<Model>,
}

#[derive(Debug, Deserialize)]
pub struct Model {
    pub id: String,
}