2026-08-04 16:47:48 -07:00

Hanzo TypeScript SDK

hanzoai

TypeScript client for the Hanzo API, generated from the API's own OpenAPI document, with budget, policy, audit, search, kb and graph written over it by hand.

Install

npm i hanzoai

Types are included. axios is the only dependency.

Client and the six capabilities below ship from 8.5.158. 8.5.157 and earlier have the generated *Api classes and not them. While npm view hanzoai version still answers 8.5.157, build the package from main:

git clone https://github.com/hanzoai/js-sdk && cd js-sdk
npm ci && npm run build && npm pack    # prints hanzoai-<version>.tgz

and install that file in your project with npm i <path to the .tgz>.

Quickstart

GET /v1/models needs no credential, so this runs before you have one. Save it as quickstart.mjs and run node quickstart.mjs:

import { AiApi, Configuration } from 'hanzoai';

const ai = new AiApi(new Configuration({ basePath: 'https://api.hanzo.ai' }));
const { data } = await ai.getModels();

console.log(`${data.data.length} models`);
for (const m of data.data.slice(0, 5)) console.log(`  ${m.id}`);

From CommonJS, require('hanzoai') returns the same classes.

Calls without a valid credential are limited per address in 8-hour windows. Past the limit the answer is 429, which arrives as an AxiosError with err.response.status set.

Auth

One scheme: a bearer token — an IAM access token or a Cloud API key. The server derives your org from the token's owner claim, so no route takes an org argument.

It goes in accessToken. When it is set, every operation sends Authorization: Bearer <token> from that field.

import { AiApi, Configuration } from 'hanzoai';

const config = new Configuration({
  basePath: 'https://api.hanzo.ai',
  accessToken: process.env.HANZO_API_KEY,
});

type Completion = { choices?: Array<{ message?: { content?: string } }> };

async function main() {
  const { data } = await new AiApi(config).postChatCompletions({
    data: { model: 'zen5', messages: [{ role: 'user', content: 'Say hello in five words.' }] },
  });
  const reply = data as unknown as Completion;
  console.log(reply.choices?.[0]?.message?.content);
}

main();

Four operations need no credential: GET /v1/models, GET /v1/models/providers, GET /v1/commands, GET /v1/openapi.json. For those, construct a Configuration with no accessToken and the client sends no Authorization header.

A generated call that gets a status outside 2xx throws axios's AxiosError, with the status on err.response.status, after one request; nothing is retried. A 200 whose body is an error object resolves like any other 200, and a completion's content comes back as the server sent it, <think> blocks included.

A Configuration reads no environment variable of its own — HANZO_API_KEY above is just where the examples keep theirs. Client, below, does read the environment, and mints its own token rather than being handed one.

GET /v1/iam/oauth/userinfo is how you check a token: it answers the token's identity, or 401 {"error":"invalid_token"}. That is the hello flow below.

The six capabilities

Budget, policy, audit, search, kb and graph — the same six words in the Go, Python and TypeScript SDKs. They hang off Client, which holds an IAM client id and secret and mints its own access token.

import { Client } from 'hanzoai';

// HANZO_CLIENT_ID and HANZO_CLIENT_SECRET, or pass { id, secret }.
const c = new Client();

const left = await c.budget.left();          // 17 of 20 calls left today
const may = await c.policy.check('usr_7', 'write', 'acme/graph');
const a = await c.search.find('incident runbook', { limit: 5 });

A refusal is an answer, not an exception. Anything a gate can stop returns Answer<T>, a union with three arms:

switch (a.status) {
  case 'ok':
    for (const hit of a.value.items) show(hit.title, hit.score);
    if (a.value.partial) warn('a search leg was down');
    break;
  case 'denied':                             // out of credit, or policy said no
    console.log(a.code, a.reason);           // "insufficient_balance", …
    for (const cure of a.cures) offer(cure.kind, cure.url);
    break;
  case 'held':                               // a person was asked
    console.log(`approval ${a.id} on ${a.clause}`);
    break;
}

a.value does not compile until the union is narrowed, so there is no path that reads a value the server never sent. All three arms carry request — the x-request-id that finds the call in the audit trail:

for await (const event of c.audit.all({ request: a.request })) {
  console.log(event.actor, event.action, event.result);
}

Exceptions are for outcomes with no decision in them: no credential, a transport failure, a 401, a 5xx. They arrive as answer.Fault, carrying status, code and request. A 401 re-mints the token and replays the call once before it throws. A 402, or a 403 with a refusal code, is the denied arm. Nothing else is retried, and a 200 is always ok.

Everything else:

capability methods
c.budget left balance plan spent
c.policy check
c.audit list all
c.search find
c.kb put get list drop import reindex connectors connect sync revoke links install
c.graph assert read find resolve walk extract ingest vocabulary

c.as('usr_7') returns a client acting as one of your tenant's subjects — the minted token carries the scope, so no method takes a user id. c.configuration hands the same identity to any generated *Api class.

Types are namespaced by capability: budget.Allowance, search.Hit, audit.Event, graph.Fact, kb.Doc, answer.Answer.

Examples

Eight flows under examples/, one directory each, every one a complete program:

flow what it does routes
models the catalog, no credential needed GET /v1/models
hello identity — prove the key works GET /v1/iam/oauth/userinfo
chat one completion POST /v1/chat/completions
money balance + usage GET /v1/billing/balance, GET /v1/billing/usage
store KV round-trip POST /v1/provisioning/kv, GET/DELETE /v1/provisioning/kv/{name}
agent create + run + read POST /v1/agent, POST /v1/agent/{ref}/run, GET /v1/agent/{ref}/runs
tools tool catalog GET /v1/tool
six budget → policy → search → kb → graph → audit, in one pass the six capabilities

models runs with nothing exported:

npm ci && npm run build && npx tsx examples/models/index.ts

(npm run build first because the examples import hanzoai by name and the package resolves its own name through exports, which points at dist/.)

Six of the others read HANZO_API_KEY and six reads HANZO_CLIENT_ID/HANZO_CLIENT_SECRET; all eight talk to https://api.hanzo.ai unless HANZO_BASE_URL says otherwise:

export HANZO_API_KEY=...
npx tsx examples/hello/index.ts

A token the server refuses answers HTTP 401: {"error":"invalid_token","error_description":"the access token is invalid or revoked"}.

npm run examples type-checks all eight against the client, npm run wire runs the six against a scripted fetch, and hanzo.yml makes both a CI gate.

The API surface

One class per product — the first path segment after /v1/: AiApi, IamApi, BillingApi, GraphApi, KnowledgeApi, SearchApi, AgentApi, ToolApi, CommerceApi, O11yApi, and so on. Each takes a Configuration; each method takes one request object.

import { Configuration, BillingApi } from 'hanzoai';

const billing = new BillingApi(new Configuration({ basePath: 'https://api.hanzo.ai' }));
billing.getBillingBalance().then(({ data }) => console.log(data));

Method names are the document's operation ids in camelCase — get_billing_balance is getBillingBalance, and a path parameter reads as by: GET /v1/provisioning/kv/{name} is getProvisioningKvByName({ name }).

Some operations declare a route but not a response shape, so their data arrives untyped and wants a cast.

Full reference: docs.hanzo.ai.

Regenerating

Most of src/ is generated — never hand-edit a generated file. Fix the spec upstream and rerun:

export OPENAPI=~/work/hanzo/openapi       # the checkout holding the generator
export SPEC=~/work/hanzo/cloud/openapi.yaml   # the document, by value

./scripts/generate.sh            # rewrite src/
./scripts/generate.sh --check    # diff only; non-zero if src/ drifted

Drop SPEC and the driver fetches the ref .spec-lock names, which needs a forge token; passing the document by value is the offline route. Every generator knob lives once, in that repo's generate.py and sdks.yaml.

.generated lists what the driver owns. A file it does not name is this repo's own and a regeneration never touches it — which is where hanzo.ts, client.ts, answer.ts and the six capabilities live.

Requires java 17+ and uv.

License

Apache-2.0

S
Description
Hanzo AI SDK for TypeScript/JavaScript — agents, cloud, inference.
Readme Apache-2.0
19 MiB
Languages
TypeScript 99.7%
JavaScript 0.3%