6.8 KiB
@hanzo/kai
TypeScript client for Kai decisions: POST {base}/v1/decisions and GET {base}/v1/models, base
https://api.hanzo.ai, Bearer auth with a Hanzo sk- key or an IAM access token. Zero runtime dependencies; ESM,
CommonJS and .d.ts; Node ≥ 20 or any runtime with fetch. Repo hanzo-js/kai, npm @hanzo/kai.
Contract
The surface is shared with the Python hanzo-kai (same contract file, built in parallel); a name changes in both or
in neither. Type-level parity with TypeSafe's @typesafe-ai/sdk 0.6.0 under our names; none of its code or text.
new Kai({ apiKey, baseURL, model, timeout, retry, defaultHeaders, fetch, logger, logLevel, dangerouslyAllowBrowser }); environmentHANZO_API_KEY,HANZO_BASE_URL,KAI_MODEL(defaultkai),KAI_LOG_LEVEL(defaultwarn).kai.decide<const Q>({ state, questions, model?, session_id?, user?, trace?, provider?, ...sent as given }, { signal, timeout, retry, headers })returnsAPIPromise<Decision<Q>>: awaiting parses once;.withResponse()gives{ data, response, requestId };.asResponse()the unreadResponse.- Builders
choice(instructions, { label: description | null } | [label, …]),noul(instructions?, { true?, false? }?, labels?)(NoulLabels{ true, false }: the words the two sides go by, /v1/decisions only),score(instructions, [level0, …]); labels and levels become literal types in the answers. Instructions are optional (a builder givennullorundefinedsends none); score levels are non-null in the types. Decision:id,model,provider,answers,usage,routing,state_hash,latency_ms. Answers: noulnoul,confidence= |2p−1|; choicechoice,confidence,probabilities; scorescore,confidence,legendandprobabilitieskeyed"0","1", …; all may carryanswer_confidenceandaction. Unknown answer types are dropped with a warning; unknown fields are kept.kai.models.list(): entries ofGET /v1/modelswhoseoutputsinclude"decision"(todaykai,hanzo/kai).- Client-side checks only: at least one question, score criteria a list, choice criteria a map or a list. Every
other rule is the server's (contract.md, frozen): 1..=100 questions, a choice ≥ 2 labels, a score ≥ 1 level and
no null level;
/v1/systemonecaps labels at 255 and levels at 10, native caps neither; schema errors are 422. - Reach refusals are 422 with
codea name, on both paths (nativeerror.code, compatdetail[0].type):state_too_long(state with a question over what the model reads; wire ceiling 128k tokens),question_too_long(a question with its type line over half the checkpoint'smax_len),option_too_long(an option over 512 tokens),request_too_long(a body over 16 MiB). Other errors carry the status ascode. - Model ids:
kai,hanzo/kai, and the versionedkai-<first 12 hex of the weights' sha256>(fromrouting.sha256), taken on both paths;/v1/systemoneanswersmodelwith the versioned id.typesafe/jev-1.13and~typesafe/jev-latestreach Jev through OpenRouter on both paths, billed at $0.042 per 1M input tokens; bare Jev ids are 400Unknown modelon/v1/systemone. - Errors:
KaiError⊃APIError(status,code,message= the server's sentence,requestIdfromx-request-id,body,retryAfterin seconds,headers) with 400BadRequestError, 401AuthenticationError, 402PaymentRequiredError, 403PermissionDeniedError, 404NotFoundError, 422UnprocessableEntityError, 429RateLimitError, ≥ 500 (529 overloaded)InternalServerError;APIConnectionError⊃APITimeoutError;APIUserAbortError. Bodies read:{"error":{"code","message"}}(native),{"detail": "…" | [{loc, msg, type}]}(compatible path;codeis the firsttype),{"error":{"message","type","code"}}and{"status":"error","msg"}(gateway), text. - Retries: 2 by default; 0.5 s doubling to 8 s less up to 25%; 408, 409, 429, 500–599, connection errors,
timeouts;
retry-after-msthenRetry-After(seconds or date) waited, capped at 60 s;X-Kai-Retry-Counton each retry. Timeout 60 s per attempt, body included. - Headers:
User-Agent: @hanzo/kai/<version>,X-Kai-Runtime. Logs ([kai]prefix):infoone line per attempt,debugheaders withauthorization,proxy-authorization,cookie,set-cookie,x-api-keymasked, and bodies. - Wire facts: with model
hanzo/kaithe gateway re-encodes the body with sorted keys, so everything is read by name. @hanzo/kai/jev(src/jev.ts, its ownexportsentry):Clientwith TypeSafe'ssystemOne({state, questions, model?})onPOST /v1/systemone,defaultModelkai, Jev's answer shapes (NoulResponse,ChoiceResponse,ScoreResponse,Result<Q>),models.list()readingdata(never the gateway'smodels, which stays[]) as Jev's cards: name = id, description empty, release_date = the UTC day ofcreated; no TypeSafe brand name is exported.
Layout
src/client.ts (Kai), http.ts (settings, the request and retry loop both clients share), jev.ts,
promise.ts (APIPromise), types.ts, questions.ts (builders, checks),
errors.ts, retry.ts (defaults, delay, sleep), log.ts, env.ts, runtime.ts, json.ts, version.ts (kept
equal to package.json by test/dist.test.ts). Build: tsc twice (tsconfig.esm.json → dist/esm,
tsconfig.cjs.json → dist/cjs plus a {"type":"commonjs"} package.json); sources import with .js so the
declarations resolve everywhere, and compile without Node types so nothing Node-only slips in.
Test
npm test: build, tsc -p tsconfig.json over src, test and examples (test/types.test-d.ts holds the type-level
assertions and @ts-expect-error lines), then node --test (TAP) over test/*.test.ts through a scripted fetch,
no network. Tests import the built package by name, ESM and CommonJS, so they check what ships. Node's type
stripping runs them: Node ≥ 22.18. Run under nice -n19 ionice -c3 on shared machines.
Conformance: test/conformance/rules.ts holds one check per contract rule, run through the SDK on both paths;
offline.test.ts runs them in npm test against server.ts (the contract as a scripted fetch), and
scripts/conformance.mjs (npm run conformance, needs HANZO_API_KEY) against the live API, pass or fail per rule.
Release
Bump version in package.json and src/version.ts, push to main. .github/workflows/cicd.yml runs hanzo.yml's
gate through hanzoai/ci build.yml@v2; .github/workflows/publish.yml publishes when npm lacks the version, with
NPM_TOKEN read from KMS through KMS_CLIENT_ID/KMS_CLIENT_SECRET, on label linux-amd64. Never publish by
hand. Check with npm view @hanzo/kai version. The lane needs, in the hanzo-js org: those two secrets (it holds only
a GitHub NPM_TOKEN), and a linux-amd64 runner group that admits public repositories.