spec: codegen-clean hanzo.yaml + retire Stainless (openapi-generator for all langs) (#24)

* fix(spec): add SPDX license identifier (openapi-generator 3.1 requires it)

openapi-generator 7.14.0 errors 'info.license.identifier is missing' for 3.1
specs. Add the SPDX custom identifier LicenseRef-Hanzo-Proprietary alongside the
Proprietary name. Last spec-validation blocker for the openapi-generator path.

* fix(spec): make hanzo.yaml fully codegen-clean for python/go/ts

Retire Stainless: python/go/js SDKs now generate from hanzo.yaml via
openapi-generator (like cpp/dart). These spec fixes make the merged
surface compile clean across the go, python (pydantic v2) and
typescript-axios generators — all verified locally at 0 errors.

- pubsub ack_wait: type integer default 30000000000 (ns) overflows int32
  -> format: int64 (Go build fix).
- platform DeployJob/CancelDeploymentJob: oneOf of inline anonymous
  objects + `const` emitted an empty TS union type. Extract each variant
  into a named component schema referenced by $ref, and use enum:[x]
  instead of const:x for cross-generator support.
- merge.py: namespace operation tags per-service (svc_Tag), same
  discipline as schemas/operationIds. Cross-service tag collisions
  (even case-only, MCP/mcp, API Keys/api-keys) were fusing unrelated
  ops into one cross-contaminated API class and colliding on
  case-insensitive filesystems. Collapse to a single primary tag so one
  op maps to exactly one API class (no duplicate request models). Also
  namespace discriminator.mapping pointers, which prefix() missed.

* ci: retire Stainless — dispatch spec-update to python/go/js SDK repos

python/go/js now generate from hanzo.yaml with openapi-generator (like
cpp/dart), each repo owning its scripts/generate.sh + generate.yml. The
regenerate workflow replaces the Stainless build job with a
repository_dispatch(spec-update) fan-out to the three repos. LLM.md SDK
section rewritten: Stainless retired, per-language generators + publish,
remaining /v1/memory + /v1/videos spec-coverage gaps recorded.

---------

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-04 20:07:24 -07:00
committed by GitHub
co-authored by Hanzo Dev
parent a8b386ba03
commit b13d5c7b8a
6 changed files with 2514 additions and 2462 deletions
+18 -18
View File
@@ -1,7 +1,9 @@
name: Regenerate SDKs
# The ONE way: hanzo.yaml (merge.py) is the single source of truth. On every
# change, validate it is codegen-clean, then regenerate every SDK from it —
# Stainless for python/go/js, openapi-generator on ARC for cpp/dart.
# openapi-generator for ALL languages, no Stainless. python/go/js own their
# generation (each repo has scripts/generate.sh + a generate.yml that runs on
# the spec-update dispatch below); cpp/dart generate here as artifacts.
on:
push:
branches: [main]
@@ -51,26 +53,24 @@ jobs:
print("GATE PASS")
PY
stainless:
name: Regenerate python/go/js (Stainless)
dispatch:
name: Trigger regeneration (python/go/js)
needs: validate
runs-on: hanzo-build-linux-amd64
strategy:
matrix:
repo: [python-sdk, go-sdk, js-sdk]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- name: Install Stainless CLI
run: npm i -g @stainless-api/cli
- name: Build (repoints hanzo-ai project -> hanzo.yaml, opens PRs on the 3 SDK repos)
# STAINLESS_API_KEY: source of truth is KMS (kms.hanzo.ai secret
# sdk/STAINLESS_API_KEY). Set it as a repo/org secret synced from KMS.
# The Stainless project's spec SOURCE must also be repointed from the
# legacy LLM spec to this hanzo.yaml in Stainless Studio (one-time).
env:
STAINLESS_API_KEY: ${{ secrets.STAINLESS_API_KEY }}
run: |
[ -n "$STAINLESS_API_KEY" ] || { echo "::error::STAINLESS_API_KEY not set — provision it in KMS then sync to this repo's secrets"; exit 1; }
stl builds create --project hanzo-ai --branch main hanzo.yaml
- name: Fire spec-update to hanzoai/${{ matrix.repo }}
# Each SDK repo's generate.yml regenerates from this hanzo.yaml with
# openapi-generator and opens a PR. No Stainless, no API key.
# SDK_DISPATCH_TOKEN: a PAT (or GitHub App token) with `repo` scope on
# the three SDK repos, synced from KMS.
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SDK_DISPATCH_TOKEN }}
repository: hanzoai/${{ matrix.repo }}
event-type: spec-update
openapi-generator:
name: Regenerate ${{ matrix.lang }} (openapi-generator)
+33 -44
View File
@@ -38,53 +38,42 @@ python3 -c "import yaml, glob; [yaml.safe_load(open(s)) for s in glob.glob('*/op
5. Update `CHANGELOG.md` with a dated entry under the v1.0.0 heading.
6. Run `python3 merge.py` (regenerates `hanzo.yaml`) and commit both.
## SDK generation — the ONE way, and its current drift (READ THIS)
## SDK generation — the ONE way (Stainless RETIRED, 2026-07)
The one interface is `hanzo.yaml`; the generator backend is an orthogonal
per-language concern (one interface, orthogonal backends):
The one interface is `hanzo.yaml`; the generator backend is
**openapi-generator** for EVERY language — no Stainless, no API key. Each
language repo owns its generation via a `scripts/generate.sh` that runs
openapi-generator against this `hanzo.yaml`, plus a `generate.yml` workflow
that regenerates + opens a PR on the `spec-update` repository_dispatch fired
by this repo's `regenerate-sdks.yml`.
| Lang | Repo | Backend | Why |
|------|------|---------|-----|
| Python/Go/JS | `hanzoai/{python,go,js}-sdk` | Stainless project `hanzo-ai` | best DX, already published |
| Rust | `hanzoai/rust-sdk` | hand-written crates | Stainless emits no Rust |
| C++ | `hanzoai/cpp-sdk` | openapi-generator `cpp-restsdk` on ARC | Stainless emits no C++ |
| Dart (if wanted) | `hanzoai/dart-sdk` | openapi-generator `dart-dio` on ARC | Stainless emits no Dart |
| Lang | Repo | Generator | Package | Publish |
|------|------|-----------|---------|---------|
| Python | `hanzoai/python-sdk` (`pkg/hanzoai`) | `python` (urllib3, pydantic v2) | `hanzoai` on PyPI | tag `v*` → twine |
| Go | `hanzoai/go-sdk` | `go` (package `hanzoai`) | `github.com/hanzoai/go-sdk` | tag `v*` → pkg.go.dev |
| TypeScript | `hanzoai/js-sdk` | `typescript-axios` | `hanzoai` on npm | tag `v*` → npm publish |
| C++ | `hanzoai/cpp-sdk` | `cpp-restsdk` | — | artifact (this repo's matrix) |
| Dart | `hanzoai/dart-sdk` | `dart-dio` | — | artifact (this repo's matrix) |
| Rust | `hanzoai/rust-sdk` | hand-written | `hanzo` crate | reconcile (openapi-generator emits no Rust) |
One repo per language, each derives from `hanzo.yaml`. There is NO unified
`hanzoai/sdk` multi-lang monorepo generator — that repo's `gen/` (openapi-
generator over all 10 langs, "replaces Stainless") is the retired SECOND way;
`hanzoai/sdk` is CLI-only now.
Generator version pinned to **7.14.0** everywhere. The merged surface is
verified codegen-clean AND compile-clean for go / python / typescript-axios
(spec fixes that made it so: pubsub `ack_wait` int64; platform DeployJob /
CancelDeploymentJob oneOf → named subschemas; merge.py namespaces operation
tags + discriminator mappings per-service and collapses to one primary tag).
### Known drift (2026-07 audit — must be fixed for the SDKs to be truthful)
There is NO unified `hanzoai/sdk` multi-lang monorepo generator — that repo's
`gen/` is the retired SECOND way; `hanzoai/sdk` is CLI-only now.
1. **Stainless points at the LEGACY LLM-gateway spec, not `hanzo.yaml`.** The
published python/go/js SDKs expose only the LiteLLM inference + gateway-admin
surface (188 endpoints / 49 resources: chat, models, embeddings, Key, User,
Team, Spend, Budget, Guardrails, provider passthrough). They cover NONE of
the cloud product surface. Fix = repoint the Stainless `hanzo-ai` project's
spec source to `hanzo.yaml` and expand its resource tree to the full surface.
`python-sdk/.stats.yml` (spec `87bc62c…`) is also diverged from go/js
(`9b3181f…`) — the repoint resyncs all three.
2. **~28 LIVE `/v1` product prefixes have no usable spec** (so even after the
repoint they'd be uncovered). The cloud binary (`hanzoai/cloud`
`subsystems.go`) mounts these flat; they need per-service specs authored from
the Go routes in `cloud/clients/<x>/*.go`:
`agents, tracker, crm, framework(+cms/erp/help/kb), kb, prompts, tasks,
functions, git, templates, security, integrations, notify, automations,
billing, plans, graph(indexers/oracles), exec, websearch, admin, do
(vpcs/load-balancers), provisioning(sql/datastore/docdb), memory, vfs,
licensing, authz, ml(train)`.
3. **Prefix-mismatch specs — re-base onto the live flat prefix:** `visor` →
`/v1/machines,/v1/gpus,/v1/clusters`; `zt` → `/v1/networks,/v1/mesh,/v1/edge`;
`flow` → `/v1/automations`; `guard` → `/v1/security`.
4. **Stale specs (legacy monolith, NOT fused into the cloud binary):**
`cloud/` (157) and `nexus/` (151) are the retired RPC monolith — only the
`/v1/search-docs` + `/v1/vector` slices survive (via `clients/product`).
`mq, pubsub, stream, registry, db, did, engine` are not live. Separate
deployments that legitimately keep their spec: `iam, chat, gateway,
operative, dns, flow`.
### Remaining spec-coverage gaps (SDKs cover the spec faithfully; the spec
lacks these live prefixes — author from the Go routes, then the SDKs pick
them up automatically on the next regeneration):
The SDK-generation surface is the FUSED `api.hanzo.ai/v1` binary, not the union
of every deployment. `merge.py` currently unions all per-service specs; the
correct target is the live cloud surface (add the 28, re-base the 4, drop the
stale monolith).
- **`/v1/memory`** and **`/v1/videos`** are LIVE (cloud binary) but have no
paths in `hanzo.yaml` yet.
- The broader "author the remaining `/v1` product specs from
`cloud/clients/<x>/*.go`" backlog still applies for any prefix not yet
present in the per-service specs.
The SDK-generation surface is the FUSED `api.hanzo.ai/v1` binary. `merge.py`
unions the per-service specs into it.
+2315 -2300
View File
File diff suppressed because it is too large Load Diff
+28 -3
View File
@@ -29,6 +29,16 @@ def prefix(node, svc):
name = "/".join(v.split("/")[3:])
out[k] = f"#/components/{kind}/{svc}_{name}"
continue
if k == "mapping" and isinstance(v, dict):
# discriminator.mapping values are component pointers too, but not
# under a `$ref` key — namespace them the same way.
out[k] = {
mk: (f"#/components/schemas/{svc}_{'/'.join(mv.split('/')[3:])}"
if isinstance(mv, str) and mv.startswith("#/components/schemas/")
else mv)
for mk, mv in v.items()
}
continue
out[k] = prefix(v, svc)
return out
if isinstance(node, list):
@@ -58,9 +68,24 @@ def main():
# master is a single, globally-valid, generatable OpenAPI document.
if isinstance(item, dict):
for method, op in item.items():
if method in HTTP_METHODS and isinstance(op, dict) \
and isinstance(op.get("operationId"), str):
op["operationId"] = f"{svc}_{op['operationId']}"
if method in HTTP_METHODS and isinstance(op, dict):
if isinstance(op.get("operationId"), str):
op["operationId"] = f"{svc}_{op['operationId']}"
# Namespace tags by service too. Codegen groups operations
# into one API class per tag, so two services sharing a tag
# name (even case-only variants like `MCP`/`mcp`) would fuse
# into a single, cross-contaminated class — and collide on
# case-insensitive filesystems. Prefixing keeps each
# service's operations in their own class (same discipline
# as schemas/operationIds).
# Collapse to a single primary tag: codegen puts one
# operation in exactly one API class. A multi-tagged op
# would otherwise be emitted (with its request model) in
# every class, colliding on the shared model name.
if isinstance(op.get("tags"), list) and op["tags"]:
op["tags"] = [f"{svc}_{op['tags'][0]}"]
else:
op["tags"] = [svc]
paths[p] = item
for n, x in (comps.get("schemas", {}) or {}).items():
schemas[f"{svc}_{n}"] = prefix(x, svc)
+119 -97
View File
@@ -3172,108 +3172,130 @@ components:
type: array
items:
type: string
DeployJobApplication:
type: object
required:
- applicationId
- applicationType
- type
- serverId
properties:
applicationId:
type: string
titleLog:
type: string
descriptionLog:
type: string
server:
type: boolean
type:
type: string
enum:
- deploy
- redeploy
applicationType:
type: string
enum:
- application
serverId:
type: string
DeployJobCompose:
type: object
required:
- composeId
- applicationType
- type
- serverId
properties:
composeId:
type: string
titleLog:
type: string
descriptionLog:
type: string
server:
type: boolean
type:
type: string
enum:
- deploy
- redeploy
applicationType:
type: string
enum:
- compose
serverId:
type: string
DeployJobApplicationPreview:
type: object
required:
- applicationId
- previewDeploymentId
- applicationType
- type
- serverId
properties:
applicationId:
type: string
previewDeploymentId:
type: string
titleLog:
type: string
descriptionLog:
type: string
server:
type: boolean
type:
type: string
enum:
- deploy
applicationType:
type: string
enum:
- application-preview
serverId:
type: string
DeployJob:
oneOf:
- type: object
required:
- applicationId
- applicationType
- type
- serverId
properties:
applicationId:
type: string
titleLog:
type: string
descriptionLog:
type: string
server:
type: boolean
type:
type: string
enum:
- deploy
- redeploy
applicationType:
type: string
const: application
serverId:
type: string
- type: object
required:
- composeId
- applicationType
- type
- serverId
properties:
composeId:
type: string
titleLog:
type: string
descriptionLog:
type: string
server:
type: boolean
type:
type: string
enum:
- deploy
- redeploy
applicationType:
type: string
const: compose
serverId:
type: string
- type: object
required:
- applicationId
- previewDeploymentId
- applicationType
- type
- serverId
properties:
applicationId:
type: string
previewDeploymentId:
type: string
titleLog:
type: string
descriptionLog:
type: string
server:
type: boolean
type:
type: string
enum:
- deploy
applicationType:
type: string
const: application-preview
serverId:
type: string
- $ref: '#/components/schemas/DeployJobApplication'
- $ref: '#/components/schemas/DeployJobCompose'
- $ref: '#/components/schemas/DeployJobApplicationPreview'
discriminator:
propertyName: applicationType
mapping:
application: '#/components/schemas/DeployJobApplication'
compose: '#/components/schemas/DeployJobCompose'
application-preview: '#/components/schemas/DeployJobApplicationPreview'
CancelDeploymentJobApplication:
type: object
required:
- applicationId
- applicationType
properties:
applicationId:
type: string
applicationType:
type: string
enum:
- application
CancelDeploymentJobCompose:
type: object
required:
- composeId
- applicationType
properties:
composeId:
type: string
applicationType:
type: string
enum:
- compose
CancelDeploymentJob:
oneOf:
- type: object
required:
- applicationId
- applicationType
properties:
applicationId:
type: string
applicationType:
type: string
const: application
- type: object
required:
- composeId
- applicationType
properties:
composeId:
type: string
applicationType:
type: string
const: compose
- $ref: '#/components/schemas/CancelDeploymentJobApplication'
- $ref: '#/components/schemas/CancelDeploymentJobCompose'
discriminator:
propertyName: applicationType
mapping:
application: '#/components/schemas/CancelDeploymentJobApplication'
compose: '#/components/schemas/CancelDeploymentJobCompose'
+1
View File
@@ -245,6 +245,7 @@ components:
default: -1
ack_wait:
type: integer
format: int64
description: Ack wait timeout in nanoseconds
default: 30000000000
ConsumerInfo: