mirror of
https://github.com/hanzoai/enso.git
synced 2026-10-17 02:19:39 +00:00
feat: add Go, Rust client examples and modular routing recipes
This commit is contained in:
@@ -48,9 +48,27 @@ Runnable client examples in this repository:
|
||||
npm start
|
||||
```
|
||||
|
||||
- **Go**: [`examples/go/main.go`](examples/go/main.go)
|
||||
```bash
|
||||
export HANZO_API_KEY="your-api-key"
|
||||
go run examples/go/main.go
|
||||
```
|
||||
|
||||
- **Rust**: [`examples/rust/`](examples/rust/)
|
||||
```bash
|
||||
export HANZO_API_KEY="your-api-key"
|
||||
cargo run --manifest-path examples/rust/Cargo.toml
|
||||
```
|
||||
|
||||
- **cURL**: [`examples/curl/chat.sh`](examples/curl/chat.sh)
|
||||
```bash
|
||||
export HANZO_API_KEY="your-api-key"
|
||||
./examples/curl/chat.sh
|
||||
```
|
||||
|
||||
## Production Rust Recipes
|
||||
|
||||
[`recipes/`](recipes/): Modular, typed Rust patterns for model routing:
|
||||
- **`ModelSuitabilityRecipe`**: Single-shot classification predicting optimal model capability tier given prompt tokens, domain, and latency/cost bounds.
|
||||
- **`ConfidenceGate`**: Evaluates Kai decision margins ($\Delta = P_{(1)} - P_{(2)}$) against threshold $\tau$ for low-latency fast decisions vs. LLM escalation.
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Hanzo Enso Go Quickstart.
|
||||
// Connects to Enso Router at /v1/chat/completions.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ChatResponse struct {
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
apiHost := os.Getenv("HANZO_API_HOST")
|
||||
if apiHost == "" {
|
||||
apiHost = "https://api.hanzo.ai"
|
||||
}
|
||||
apiKey := os.Getenv("HANZO_API_KEY")
|
||||
|
||||
fmt.Printf("Connecting to Enso Router at %s...\n", apiHost)
|
||||
|
||||
reqBody := ChatRequest{
|
||||
Model: "enso-free",
|
||||
Messages: []Message{
|
||||
{Role: "user", Content: "What is model routing?"},
|
||||
},
|
||||
MaxTokens: 128,
|
||||
Temperature: 0.1,
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/chat/completions", strings.TrimRight(apiHost, "/"))
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if apiKey != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Error: HTTP %d: %s\n", resp.StatusCode, string(bodyBytes))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var chatResp ChatResponse
|
||||
if err := json.Unmarshal(bodyBytes, &chatResp); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Model selected: %s\n", chatResp.Model)
|
||||
if len(chatResp.Choices) > 0 {
|
||||
fmt.Printf("Response: %s\n", chatResp.Choices[0].Message.Content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "enso-quickstart"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
reqwest = { version = "0.11", features = ["json", "blocking"] }
|
||||
@@ -0,0 +1,45 @@
|
||||
//! Hanzo Enso Rust SDK Quickstart.
|
||||
//!
|
||||
//! Submits a query to the Enso router (POST /v1/chat/completions).
|
||||
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api_host = env::var("HANZO_API_HOST").unwrap_or_else(|_| "https://api.hanzo.ai".into());
|
||||
let api_key = env::var("HANZO_API_KEY").unwrap_or_default();
|
||||
|
||||
println!("Connecting to Enso Router at {api_host}...");
|
||||
|
||||
let payload = json!({
|
||||
"model": "enso-free",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Explain single-shot routing in one sentence."}
|
||||
],
|
||||
"temperature": 0.1,
|
||||
"max_tokens": 256
|
||||
});
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let mut req = client.post(format!("{}/v1/chat/completions", api_host.trim_end_matches('/')))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload);
|
||||
|
||||
if !api_key.is_empty() {
|
||||
req = req.header("Authorization", format!("Bearer {api_key}"));
|
||||
}
|
||||
|
||||
let resp = req.send()?;
|
||||
let status = resp.status();
|
||||
let body: serde_json::Value = resp.json()?;
|
||||
|
||||
println!("HTTP Status: {status}");
|
||||
println!("Model Selected: {}", body["model"].as_str().unwrap_or("unknown"));
|
||||
if let Some(content) = body["choices"][0]["message"]["content"].as_str() {
|
||||
println!("Response:\n{}", content.trim());
|
||||
} else {
|
||||
println!("Raw Response:\n{}", serde_json::to_string_pretty(&body)?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Generated
+107
@@ -0,0 +1,107 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "enso-recipes"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "enso-recipes"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Hanzo Enso Model Suitability and Routing Recipes in Rust."
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
@@ -0,0 +1,109 @@
|
||||
//! Model Routing & Inference Optimization Recipes.
|
||||
//!
|
||||
//! Enso/Kai routing patterns: single-shot model suitability prediction,
|
||||
//! adaptive thinking depth budgeting, and calibrated LLM escalation.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// 1. Single-Shot Model Suitability Routing Recipe.
|
||||
pub struct ModelSuitabilityRecipe;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TaskComplexityState {
|
||||
pub prompt_token_count: usize,
|
||||
pub task_domain: String,
|
||||
pub requires_formal_reasoning: bool,
|
||||
pub requires_code_execution: bool,
|
||||
pub latency_deadline_ms: u32,
|
||||
pub max_cost_nano_usd: u64,
|
||||
}
|
||||
|
||||
impl ModelSuitabilityRecipe {
|
||||
pub fn build_request(task: &TaskComplexityState) -> Value {
|
||||
json!({
|
||||
"model": "kai",
|
||||
"state": task,
|
||||
"questions": {
|
||||
"selected_tier": {
|
||||
"type": "choice",
|
||||
"instructions": "Predict optimal model tier achieving >= 95% success probability at lowest cost",
|
||||
"criteria": {
|
||||
"edge_fast_3b": "trivial classification or pattern extraction; sub-50ms local inference",
|
||||
"cloud_flash_8b": "standard conversational synthesis or lightweight summarization",
|
||||
"frontier_pro_deep": "complex scientific reasoning, full codebase refactoring, or multi-step logic"
|
||||
}
|
||||
},
|
||||
"thinking_depth": {
|
||||
"type": "choice",
|
||||
"instructions": "Calibrate reasoning budget depth for the chosen tier",
|
||||
"criteria": {
|
||||
"off": "no chain-of-thought needed; direct token output",
|
||||
"low": "lightweight verification check (< 1,000 tokens)",
|
||||
"medium": "standard step-by-step reasoning",
|
||||
"high": "deep exhaustive exploration with self-reflection (> 8,000 tokens)"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 2. Confidence-Gated LLM Escalation.
|
||||
pub struct ConfidenceGate;
|
||||
|
||||
impl ConfidenceGate {
|
||||
/// Decides whether a fast Kai decision stands or requires escalation to an LLM.
|
||||
///
|
||||
/// If margin (P_top - P_second) >= tau, returns None (Kai decision stands).
|
||||
/// If margin < tau, returns Some(target_llm) for fallback escalation.
|
||||
pub fn evaluate_escalation(
|
||||
probabilities: &[f64],
|
||||
tau: f64,
|
||||
target_llm: &str,
|
||||
) -> Option<&'static str> {
|
||||
if probabilities.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let mut sorted = probabilities.to_vec();
|
||||
sorted.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let margin = sorted[0] - sorted[1];
|
||||
if margin < tau {
|
||||
Some(Box::leak(target_llm.to_string().into_boxed_str()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_model_suitability_request() {
|
||||
let task = TaskComplexityState {
|
||||
prompt_token_count: 520,
|
||||
task_domain: "customer_support".into(),
|
||||
requires_formal_reasoning: false,
|
||||
requires_code_execution: false,
|
||||
latency_deadline_ms: 500,
|
||||
max_cost_nano_usd: 10000,
|
||||
};
|
||||
let req = ModelSuitabilityRecipe::build_request(&task);
|
||||
assert_eq!(req["model"], "kai");
|
||||
assert!(req["questions"]["selected_tier"].is_object());
|
||||
assert!(req["questions"]["thinking_depth"].is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_confidence_escalation() {
|
||||
// High confidence: margin = 0.85 - 0.10 = 0.75 >= 0.40 -> Stands
|
||||
let stand = ConfidenceGate::evaluate_escalation(&[0.85, 0.10, 0.05], 0.40, "zen-6");
|
||||
assert!(stand.is_none());
|
||||
|
||||
// Low confidence: margin = 0.48 - 0.45 = 0.03 < 0.40 -> Escalates to Zen
|
||||
let escalate = ConfidenceGate::evaluate_escalation(&[0.48, 0.45, 0.07], 0.40, "zen-6");
|
||||
assert_eq!(escalate, Some("zen-6"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user