basal-client 0.3.0

Async Rust client for the native Basal HTTP API
Documentation

basal-client (Rust)

Async Rust SDK for the native Basal API (POST /v1/basal), using Hyper HTTP/1 with Rustls and the platform certificate verifier. No dependency on the inference engine or GPU libraries. Rust 1.95+; a Tokio runtime is required.

This package is versioned independently. Release tag: sdk-rust-vX.Y.Z. Changelog.

cargo add basal-client
cargo add tokio --features macros,rt-multi-thread
use basal_client::{choice, noul, questions, Client};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new("http://127.0.0.1:8000")?;
    let department = choice(
        "Do którego działu skierować zgłoszenie?",
        &[("returns", "Zwroty"), ("support", "Wsparcie techniczne")],
    )?;
    let urgent = noul("Czy sprawa jest pilna?")?;
    let questions = questions! {
        department: &department,
        second_department: &department,
        urgent: &urgent,
    }?;
    let response = client.decide("Klient prosi o zwrot pieniędzy za uszkodzony telefon.", &questions).await?;
    let answers = response.answers()?;
    println!("Dział: {}, P(tak): {}", answers.department.choice, answers.urgent.noul);
    println!("Drugie pytanie: {}", answers.second_department.choice);
    Ok(())
}

For choice/score, set question.criteria to an ordered serde_json::Value object/array. Question and request extra maps accept native extension fields (evidence, costs, facts etc.). Do not duplicate explicit fields in extra. JSON object order is retained through serde_json's preserve_order and indexmap. The float_roundtrip feature preserves binary64 values when parsing wire probabilities, without extra dependencies or arbitrary_precision feature unification. Add serde_json directly (cargo add serde_json --features preserve_order) to construct structured JSON with its json! macro.

models() reads /v1/models. Use Client::with_options(ClientOptions { ... }) for a bearer token or a different timeout. The timeout (default 120 seconds) includes response-body reading. Dropping the request future cancels client-side I/O. Clone/reuse the client to share its connection pool. Use the server root (possibly with a proxy prefix), without /v1. HTTPS is required except for loopback HTTP, suitable for a local server or SSH tunnel. URL credentials/query/fragment are rejected. DNS names must be ASCII; use punycode for IDN. The client ignores environment proxies, uses a per-client ring crypto provider, and verifies TLS with platform roots. It does not enable HTTP/2 or install a process-global crypto provider. ring includes a native build step; this SDK does not depend on AWS-LC or CMake. max_response_bytes defaults to 8 MiB and limits the actual stream, including error bodies; oversized responses return Error::ResponseTooLarge, deadlines return Error::Timeout.

Error::Http provides status, body and raw retry_after. Error::Transport, Error::Encode and Error::Protocol preserve their causes. No automatic retries or redirects. Responses deserialize into native DTOs; extra JSON fields are retained. HTTP 422 and overload 529 remain distinct. Display and Debug omit HTTP bodies and nested transport details; access them explicitly when needed.

Native choice/score accepts 2–255 options for format-2 models (older model limits are checked by the server); multi uses independent probabilities and set_confidence. Evidence offsets count Unicode characters, not UTF-8 bytes. The SDK does not execute selected actions.

Factories return distinct QuestionSpec<...> types. Default factories take only their required arguments; choice_with_options, noul_with_options, score_with_options, multi_with_options, act_with_options accept type-specific options.

Starting with 0.2.0, questions! infers the answer kind for each named field. Field names become request keys. A definition or its clone can appear under several keys; each field reads its own answer. Expressions are evaluated once. response.answers()? returns borrowed views with required fields, so answers.department.choice is &str and answers.urgent.noul is f64, without optional fields or casts. Choice keys remain strings in Rust; the SDK validates them against the requested criteria at runtime. ? propagates a validation error, rather than unwrapping a nullable field.

For keys loaded dynamically, use the ordered map builder:

let questions = Questions::new()
    .with("department", &department)?
    .with("second_department", &department)?;
let response = client.decide(state, &questions).await?;
println!("{}", response.choice("department")?.choice);
println!("{}", response.choice("second_department")?.choice);

Import basal_client::Questions for this variant. response.answer("department")? returns the AnswerResult enum; match its variants for heterogeneous dynamic keys. The choice, noul, score, multi and act accessors return checked views by key and reject a different answer kind. response.answers()? on a dynamic map returns an ordered map of these enum variants. Unknown keys are errors, including extra answers that were not requested. Duplicate request keys are rejected.

Migrating from 0.1.0: replace response.answer(&question)? with the named fields from questions!, or a keyed accessor such as response.choice("department")?. Question object identity no longer determines which answer is returned. This is a breaking API change; other SDK package versions are independent.

Views borrow the response without copying probability maps. Use decide_with_options for model/facts, response.raw() for native DTOs, and decide_request(&request) for the full raw request (Rust has no method overloading).

Full documentation and publication setup.

Basal 2.0 and Omni

Typed choice/score and act outcomes allow 2–255 options. The server remains responsible for limits of older models and for calibration/model capabilities. tool accepts OpenAI, MCP and Anthropic definition objects, optional non-tool choices (answer_directly, ask_user, none), ranking, multi selection and certified deferral. The SDK returns the decision; it never executes tools.

Multi selection supports threshold, exact, iou and f1, count limits, exclusive, fallback and fallback_threshold. threshold is optional in results because objective-based responses instead contain objective and expected_score; constraint diagnostics remain available. Single tool selection can be null on certified deferral, while multi mode returns a tool list.

Typed request options include lang (pl/en), ordered inline media and detail (default/high). Media helpers accept encoded bytes, base64 or a data URL; they never fetch URLs. Read files explicitly in your application. Decoding, model support and resource limits are checked by the server. Omni requires a CUDA server started with media enabled. calibration: "text" means text calibration applied to media, not validated calibration for images/audio.

Usage exposes longest_prompt_tokens, second_order_branches and media counts, soft tokens and detail. Discovery exposes prompt_format, orders, enabled media and calibration temperatures. Model names remain strings: use exact names from model discovery, not a hardcoded model enumeration. Keep old models and raw JSON access available. These SDKs cover server-supported format-2 decisions; upstream 2.1 task types are not implemented by this server.

use basal_client::{tool_with_options, ToolOptions, ToolExtra, DecisionOptions, Language, MediaInput, questions};
let definitions = [serde_json::json!({"name":"inspect","description":"Inspect material","inputSchema":{"type":"object"}})];
let routing = tool_with_options("Choose a tool.", &definitions, ToolOptions { extra: Some(&[ToolExtra::None]), rank: Some(2), ..Default::default() })?;
let questions = questions! { route: &routing }?;
let media = [MediaInput::image(&std::fs::read("photo.jpg")?)];
let response = client.decide_with_options("Inspect the supplied material.", &questions, DecisionOptions {
    model: Some("basal-omni-1.0-mini"), lang: Some(Language::En), media: Some(&media), ..Default::default()
}).await?;
println!("{:?}", response.answers()?.route.tool);

Rust 0.3 adds a Tool question/answer variant and fields to request options and metadata. When constructing option structs use ..Default::default(); multi threshold is now Option<f64>. MultiOptions borrows constraint keys. Existing request factories and raw response access remain available.