capabilities.yaml states the law and nothing enforced it. Measured against
api.hanzo.ai: 103 authored paths outside their owner's prefix, and four
products (kv, s3, search, vector) described by TWO specs at once.
provisioning was the bucket — it claimed /v1/{sql,vector,datastore,kv,search,
s3,docdb}, seven other products' roots, and its own header says why it could:
"these routes are keyed by RESOURCE KIND at top-level /v1/, NOT under
/v1/provisioning/." There is no /v1/provisioning route. Each kind's spec owns
its own quartet now; sql/datastore/docdb became their own specs under `data`;
provisioning is collapsed.
The inference edge (chat/completions, completions, embeddings, rerank, models,
messages, images/generations, audio/speech) moved from ai/ to gateway/, the
binary that answers it, and lost the vendor-as-standard prose on the way — a
tag named for another company's product is not a description of ours. The
/v1/admin/* operations authored in affiliates/, authors/, referrals/ and
plugin/ moved to admin/; /v1/edge/nodes moved from zt/ to edge/. plan/ ->
plans/ and plugin/ -> plugins/, because the routes are /v1/plans and
/v1/plugins and the route is the part that cannot move.
31 paths deleted, each probed live and absent from the route table: the whole
/v1/gateway/* subtree (23 — a phantom second copy of the inference edge, and
the reason three operationIds needed a _2 suffix to stay distinct in codegen),
/v1/load-balancers x2 (served product is /v1/balancers), /v1/bots/launch|{id}|
{id}/{action} (served at /v1/compute/bots/*), /v1/machines/{id}/bind-agent|
agent-binding (served at /v1/machines/{id}/agent), /v1/audio/transcriptions.
hanzo.yaml 1742 -> 1711 paths: 31 removed, 0 added, nothing served lost.
test_placement.py is the gate. Rule 1 — no two authored specs claim one
product — is absolute. Rule 2 — a spec claims its own prefix — carries
OFF_PREFIX, 48 declared exceptions each measured live, each a binary answering
at a noun that is not its service's name; the fix for those is a route move in
the serving repo, and a stated gap beats a silent one.
flows.yaml's chat journey follows the operation it names: ai_createChatCompletion
-> gateway_createChatCompletion. That gate caught the move, which is what it is
for.
Co-authored-by: Hanzo Dev <[email protected]>
Hanzo OpenAPI
hanzo.yaml is THE published document. One OpenAPI 3.1 surface for every
Hanzo service — the one every SDK, every doc site and every tool generates from.
curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" \
https://raw.githubusercontent.com/hanzoai/openapi/main/hanzo.yaml
This repo is private, so that URL needs a token — unauthenticated it is a
404, not a 403, which reads like a missing file rather than a missing header.
Any fetch of it without an Authorization header is broken; curl -f under
set -e turns that into a build failure with nothing that names the cause.
Nothing generates from anything else, and nothing is pushed to an SDK. The SDK repos PULL this file and regenerate themselves; a client is never hand-written and never receives a patch from here.
Forward-only. No backwards compatibility, no deprecated paths, no /api/
prefix, no cross-brand references. A single bearer JWT from Hanzo IAM
(https://hanzo.id) authenticates every route, and every route is
/v1/<service>/<resource> — the same path through the gateway
(api.hanzo.ai) and through the service's own hostname.
One exception — AI inference is top-level. hanzoai/ai serves its OpenAI-
and Claude-compatible surface at the bare /v1/* paths
(/v1/chat/completions, /v1/completions, /v1/embeddings, /v1/models,
/v1/rerank, /v1/messages) because every OpenAI/Anthropic SDK hard-codes
them: there, the path IS the compatibility contract.
curl -H "Authorization: Bearer ${HANZO_TOKEN}" \
https://api.hanzo.ai/v1/chat/completions -d '{"model":"zen5","messages":[...]}'
curl -H "Authorization: Bearer ${HANZO_TOKEN}" \
https://api.hanzo.ai/v1/billing/balance
OIDC discovery: https://hanzo.id/.well-known/openid-configuration.
hanzo.yaml is a file in this repo, not a served endpoint — /v1/discovery
was probed, it 404s, and no handler for it exists.
The files
| File | What it is |
|---|---|
hanzo.yaml |
the published document. Generated by merge.py; never edited. |
<service>/openapi.yaml |
one self-contained authored spec per service. |
cloud/openapi.yaml |
not authored here — hanzoai/cloud's own woven document, copied verbatim by sync.py. |
capabilities.yaml |
the ONE registry: every capability, in exactly one domain. |
flows.yaml |
the canonical example set — the same six journeys in every SDK. |
CAPABILITIES.md |
generated index. Never hand-edit. |
merge.py sync.py generate.py skills.py audit.py |
build · resync · clients · skills · measurement. |
The capability index is CAPABILITIES.md, generated from
capabilities.yaml, so it cannot drift from what is here. There is no second
index in this file to fall out of date.
Two commands
python3 sync.py # pull hanzoai/cloud's woven document, then merge
python3 merge.py # rebuild hanzo.yaml + CAPABILITIES.md from the specs
sync.py is the whole resync: it fetches origin/main in a hanzoai/cloud
checkout (../cloud, or --cloud DIR, or CLOUD_DIR), writes
origin/main:openapi.yaml here byte-for-byte, and re-merges. --check reports
staleness without writing. Nothing is ever written back to hanzoai/cloud.
That document is the one input here that cannot lie about what is served —
cloud weaves it from its own router and gates the weave against source — so
merge.py lets it WIN wherever it and an authored spec describe the same
route. The authored specs cover what other binaries serve, cloud's covers what
cloud serves, and hanzo.yaml is the union with truth on top.
Conventions
info.version: 8.0.0(the V8 generation). The/v1path is the immutable contract — there is nov2, ever. Breaking changes ship as new resources.- operationIds are BARE in each spec (
logs_query);merge.pynamespaces them<svc>_in the published document. Never self-prefix. - No cross-file
$ref. Each spec is self-contained. - No
deprecated: true. No/api/prefix. No dates in filenames. - Security: every operation uses
BearerAuth. No service has its own login. - Errors: 4xx client, 5xx server, JSON body with
statusandmsg. - A product whose own authored routes answer 404 — probed at
api.hanzo.aiAND at its own host — is DELETED, not annotated.skills.pyhas no liveness filter, so an unserved route ships as a live instruction to call a dead endpoint. See LLM.md, "Who reads this repo".
Validate
python3 -c "import yaml, glob; [yaml.safe_load(open(s)) for s in glob.glob('*/openapi.yaml') + ['hanzo.yaml']]; print('OK')"
java -jar ~/.cache/openapi-generator/openapi-generator-cli-7.14.0.jar validate -i hanzo.yaml
npx -y @redocly/cli preview-docs hanzo.yaml
Clients
generate.py runs openapi-generator (pinned 7.14.0) over hanzo.yaml for
every language; sdks.yaml is the per-language data, and each SDK repo carries
only a call site. There is no second generator and no unified SDK monorepo.
python3 generate.py --all # regenerate every client in place
python3 generate.py go # just one
python3 generate.py --all --check # fail if a committed client drifted
| Lang | Canonical repo | Generator | Ships as |
|---|---|---|---|
| Python | hanzoai/python-sdk |
python |
hanzoai on PyPI |
| TypeScript | hanzoai/js-sdk |
typescript-axios |
hanzoai on npm |
| Java · Kotlin | hanzoai/java-sdk · hanzoai/kotlin-sdk |
java · kotlin (okhttp) |
ai.hanzo:hanzo-{java,kotlin}-cloud |
| Rust | hanzo-rs/sdk |
rust (reqwest) |
crates/hanzo-client — own scripts/generate.sh (needs a template override) |
| Go | hanzo-go/sdk |
go |
package hanzoai at the module root — own scripts/generate.sh (no directory to own) |
A language stays in sdks.yaml while its whole invocation is expressible as
data, and leaves when it needs something that is not — a template file, or a
layout take cannot describe. It then owns the whole invocation in its own
repo. A row here AND flags there is two declarations of one contract.
Examples are not a per-SDK decision either: flows.yaml names the six journeys
(hello, chat, money, store, agent, tools) and every SDK renders the
same six from the same operationIds. python3 test_flows.py is the gate — an id
that no longer resolves fails here, once, instead of in six clients separately.
Agent skills
skills.py derives /.well-known/agent-skills/ from the same per-service
specs, clustering GET endpoints by resource — read-only by construction, no
destructive verb ever becomes a skill — and emits a SKILL.md per cluster plus
an index.json carrying a sha256 of every file.
python3 skills.py # dist/agent-skills/<brand>/… (hanzo, lux, zoo)
python3 skills.py --no-services # unified tree only (what the cloud binary embeds)
python3 skills.py --check # drift gate
python3 test_skills.py # golden test
Output is white-labeled per brand: a Lux surface reads Lux / api.lux.network
/ lux.id, a Zoo surface Zoo / api.zoo.ngo / zoo.id — never Hanzo.
dist/ is generated at build time and is not committed.
License
BSD 3-Clause. Copyright Hanzo AI, Inc.