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:
committed by
GitHub
co-authored by
Hanzo Dev
parent
a8b386ba03
commit
b13d5c7b8a
@@ -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)
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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'
|
||||
|
||||
@@ -245,6 +245,7 @@ components:
|
||||
default: -1
|
||||
ack_wait:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Ack wait timeout in nanoseconds
|
||||
default: 30000000000
|
||||
ConsumerInfo:
|
||||
|
||||
Reference in New Issue
Block a user