The lock sat at 574d1f1d while the product moved on. The team surface stopped at rooms — no messages path, no way to open one — so anything built on this client had to reach past it with its own HTTP calls, which is the duplication a generated client exists to prevent.
Hanzo Cloud — Kotlin SDK
Kotlin client for the Hanzo Cloud API, generated from the
API's own OpenAPI document, under ai.hanzo.cloud.
.spec-lock names the ref and the digest of the bytes this tree
is a projection of.
Install
Nothing is on Maven Central yet — ai.hanzo is not a group there. Build it and
take it from your local Maven repo:
git clone https://github.com/hanzo-kotlin/sdk hanzo-kotlin-sdk
cd hanzo-kotlin-sdk
./gradlew -PpublishLocal :hanzo-kotlin-cloud:publishToMavenLocal
That installs ai.hanzo:hanzo-kotlin-cloud:0.1.0-alpha.5 into ~/.m2. Then, in
the project that wants it:
repositories { mavenLocal() }
dependencies { implementation("ai.hanzo:hanzo-kotlin-cloud:0.1.0-alpha.5") }
-PpublishLocal skips GPG signing; without it the publish stops at
signMavenPublication with no configured signatory. Building wants a JDK 21
toolchain. The jar it produces is Java 8 bytecode.
Quickstart
import ai.hanzo.Hanzo
import ai.hanzo.cloud.api.KeysApi
fun main() {
val keys = Hanzo().api(::KeysApi).getKeys().propertyKeys.orEmpty()
println("${keys.size} keys")
keys.forEach { println("${it.type} ${it.prefix}") }
}
export HANZO_API_KEY=sk-...
./gradlew :examples:hello:run # the same call, in this repo
With a key that prints the caller's own keys. With none, the API answers 403.
examples/hello catches that — see Errors — and
opens with a call that needs no key at all, so it runs either way.
Hanzo() resolves the endpoint and credentials; hanzo.api(::SomeApi) builds
any generated API class against them. The classes follow the document's tags
under ai.hanzo.cloud.api, with their request and response types under
ai.hanzo.cloud.model. propertyKeys rather than keys is the generator
prefixing a name on its reserved list — size, keys, values, entries,
class — and @SerializedName still sends the original. Read method and field
names off the client; do not guess them.
Authenticate
HANZO_API_KEY goes out as Authorization: Bearer … on every request. The
document declares one scheme, bearer, and applies it to every operation but
four.
Two kinds of token fit that header. A Cloud API key is sk- (secret, belongs
on a server) or pk- (publishable, org-identifying, safe in a browser bundle);
POST /v1/keys mints one and the GET above lists what the caller already
holds. An IAM access token is what a service presents, minted from its own
client credentials:
export HANZO_API_KEY=$(curl -s https://hanzo.id/v1/iam/oauth/token \
-d grant_type=client_credentials -d client_id=… -d client_secret=… \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
Four operations need neither: get_models, get_models_providers,
get_commands and get_openapi.json. Every generated request config reports
which side it is on as requiresAuthentication.
| variable | meaning |
|---|---|
HANZO_API_KEY |
bearer credential: a Cloud API key or an IAM access token |
HANZO_BASE_URL |
gateway to talk to; default https://api.hanzo.ai |
HANZO_ORG_ID |
org scope, sent as X-Org-Id; the KV and agents routes refuse without it |
Pass them explicitly when one program serves more than one tenant —
Hanzo(apiKey = "sk-…", orgId = "acme"). Each client keeps the credential it was
handed; a second tenant cannot re-point the first one's calls.
Hanzo is the one
hand-written file under src/main, and it is where the credential lives. The
token rides the transport, per instance, alongside the X-Org-Id no generated
signature accepts.
Errors
A 4xx throws ClientException and a 5xx throws ServerException, both from
ai.hanzo.cloud.infrastructure, both carrying statusCode and the raw
response.
import ai.hanzo.cloud.infrastructure.ClientException
try {
Hanzo().api(::KeysApi).getKeys()
} catch (e: ClientException) {
println("refused ${e.statusCode}") // 403 with no key
}
Examples
The six canonical flows every Hanzo SDK ships, under examples/<flow>/. They are
Gradle subprojects compiled by the build.
| flow | operations | what it does |
|---|---|---|
hello |
get_models, get_keys |
the open call that proves the gateway is reachable, then the call that says no |
chat |
post_chat_completions |
one completion — the route carries no schema in the document, so the flow prints the status it got |
money |
get_billing_balance, get_billing_usage |
the balance and the usage that moved it |
store |
post_kv, get_kv_by_name, delete_kv_by_name |
provision a KV store, read it back, drop it |
agent |
post_agents, post_agents_by_ref_run, get_agents_by_ref_runs |
create an agent, run it, poll until the run is terminal |
tools |
get_tools |
the tools this key can reach, and which are activated |
One command, no credential — hello opens on a route the document declares
open:
./gradlew :examples:hello:run
gateway https://api.hanzo.ai HTTP 200
keys HTTP 403 set HANZO_API_KEY to a key this gateway knows
With a credential the second line answers too, and the other five flows run:
export HANZO_API_KEY=sk-...
export HANZO_ORG_ID=my-org # store and agent only
./gradlew :examples:hello:run
gateway https://api.hanzo.ai HTTP 200
keys the key is good, and it owns no keys of its own
agent runs on zen5 and reads HANZO_MODEL to pick another;
curl https://catalog.hanzo.ai/v1/models lists the rest.
Route reference: docs.hanzo.ai.
Build
./scripts/build # client + examples
./gradlew :hanzo-kotlin-cloud:assemble # the jar
./gradlew :hanzo-kotlin-cloud:test # the flow tests
The build compiles the client and all six flows against it, so a document change
that renames or drops an operation goes red here. FlowsTest pins each flow to
the route hanzoai/openapi flows.yaml names, asserts the client sends the bearer
token and the org header, that two clients in one process keep their own
credentials, and that each route reports the requiresAuthentication the
document declares for it.
Regenerate
Nothing under hanzo-kotlin-cloud/src/main/kotlin/ai/hanzo/cloud is written by
hand. To change the client, change the code that emits the document.
SPEC=/path/to/openapi.yaml OPENAPI=/path/to/hanzoai/openapi ./scripts/generate.sh
SPEC=… OPENAPI=… ./scripts/generate.sh --check # non-zero if the tree drifted
scripts/generate.sh is a call site: the invocation lives once in
hanzoai/openapi/generate.py, and every knob — generator, HTTP library,
coordinates, packages — is data in sdks.yaml beside it.
Sibling repos
The same API in other languages: hanzoai/python-sdk, hanzoai/java-sdk,
hanzo-js/sdk, hanzo-go/sdk, hanzo-cpp/sdk, hanzo-swift/sdk.
License
Apache-2.0. See LICENSE.