mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
regenerate from cloud@5126939 — the client leaves the hand-merged lineage
pkg/hanzoai/cloud goes +2186 -2126 ~175: 1700 paths / 2354 operations / 2186 schemas, 182 api modules and 2172 model modules, against the 239/1116 it carried from hanzo.yaml. The document is hanzoai/cloud's own emission now, pinned in a new .spec-lock by commit and sha256, and generate.py reaches it by value — --skip-validate-spec means the 1012 missing-`responses` errors no longer write zero files, so a client can be cut from the authority instead of a projection of it. Two renamings come with that, and both are the fix rather than the damage. IAM's types are namespace-qualified (iam.Role, iam.Application, 95 of them) because a bare `Role` was two unrelated shapes wearing one name. And the <svc>_ prefix is gone from every operationId, so every generated method lost it. The prefix is what broke the examples, and the gate saw only two thirds of it. Four flows failed on their imports; `money` and `tools` PASSED while every call in them named a method that no longer existed — an import resolves the names in the `from … import` line and a method is looked up at call time. All five flows now name operations that exist, checked by resolving each one as an attribute, and hanzo.yml records that ceiling so the next reader does not trust the gate for more than it says. `chat` is removed, which is a measurement and not a preference: cloud declares POST /v1/chat/completions with no requestBody and no responses at all, so the generated method takes no body and returns None — the one call a chat example exists to make cannot be expressed. Inventing the type, or hand-rolling the HTTP inside a generated client, is the drift these SDKs exist to prevent; js-sdk dropped its own chat flow at 2.0.7 for exactly this. It returns the release cloud gives that route a body. Two smoke assertions were already red before this regeneration and are now true again: they pinned AIApi/APIKeysApi/MCPApi and AdminApi.plugin_admin_*, names from the retired lineage. The surface is all still there under AiApi/KeysApi/ McpApi and AdminApi.admin_plugins. tests/test_zap_transport.py still errors, exactly as it did before: it imports `Hanzo` from hanzoai, which the hand-written package does not define. That is not this document's business and is untouched. Minor rather than patch: every generated method changed name. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""agent — define one, run it, read the run back.
|
||||
|
||||
POST /v1/agents cloud_post_v1_agents
|
||||
POST /v1/agents/{ref}/run cloud_post_v1_agents_by_ref_run
|
||||
GET /v1/agents/{ref}/runs cloud_get_v1_agents_ref_runs
|
||||
POST /v1/agents post_v1_agents
|
||||
POST /v1/agents/{ref}/run post_v1_agents_by_ref_run
|
||||
GET /v1/agents/{ref}/runs get_v1_agents_ref_runs
|
||||
|
||||
``ref`` is the agent's public id (``agent_...``) OR its org-unique name, which
|
||||
is why the run and the read below can both use the name we just created without
|
||||
@@ -17,7 +17,7 @@ server actually produced.
|
||||
|
||||
import time
|
||||
|
||||
from hanzoai.cloud import AgentsApi, CloudCreateAgentIn
|
||||
from hanzoai.cloud import AgentsApi, CreateAgentIn
|
||||
|
||||
from examples.client import MODEL, client, run
|
||||
|
||||
@@ -29,8 +29,8 @@ def main() -> None:
|
||||
with client() as api:
|
||||
agents = AgentsApi(api)
|
||||
|
||||
created = agents.cloud_post_v1_agents(
|
||||
CloudCreateAgentIn(
|
||||
created = agents.post_v1_agents(
|
||||
CreateAgentIn(
|
||||
name=NAME,
|
||||
model=MODEL,
|
||||
description="Created by the hanzoai SDK agent example.",
|
||||
@@ -39,10 +39,10 @@ def main() -> None:
|
||||
)
|
||||
print(f"created {created.name} ({created.id}) on {created.model}")
|
||||
|
||||
agents.cloud_post_v1_agents_by_ref_run(NAME)
|
||||
agents.post_v1_agents_by_ref_run(NAME)
|
||||
print("run started")
|
||||
|
||||
runs = agents.cloud_get_v1_agents_ref_runs(NAME, limit=5)
|
||||
runs = agents.get_v1_agents_ref_runs(NAME, limit=5)
|
||||
entries = runs.runs or []
|
||||
print(f"{len(entries)} run(s):")
|
||||
for entry in entries:
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"""chat — one completion.
|
||||
|
||||
``POST /v1/chat/completions`` (operationId ``ai_createChatCompletion``). The
|
||||
route is OpenAI-compatible, so the request and response shapes are the ones you
|
||||
already know; only the base URL and the key change.
|
||||
|
||||
``stream`` is left False on purpose. Streaming is a different transport (SSE)
|
||||
that the generated client returns as an opaque body, so a streaming example here
|
||||
would teach the wrong thing.
|
||||
|
||||
python -m examples.chat
|
||||
"""
|
||||
|
||||
from hanzoai.cloud import AiChatCompletionRequest, AiChatMessage, OpenAICompatibleApi
|
||||
|
||||
from examples.client import MODEL, client, run
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with client() as api:
|
||||
completion = OpenAICompatibleApi(api).ai_create_chat_completion(
|
||||
AiChatCompletionRequest(
|
||||
model=MODEL,
|
||||
messages=[AiChatMessage(role="user", content="Say hello in exactly five words.")],
|
||||
)
|
||||
)
|
||||
|
||||
# `choices[0].message.content` is reachable without a cast only because the
|
||||
# spec models a ChatChoice. It used to be `items: {type: object}`, which every
|
||||
# generator turns into its untyped bag — on the most-called route in the API.
|
||||
choices = completion.choices or []
|
||||
if not choices:
|
||||
raise SystemExit("no choices returned")
|
||||
print(choices[0].message.content if choices[0].message else "(empty message)")
|
||||
|
||||
if completion.usage:
|
||||
print(f"tokens: {completion.usage.prompt_tokens} in / {completion.usage.completion_tokens} out")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(main)
|
||||
+31
-14
@@ -1,32 +1,49 @@
|
||||
"""hello — who am I?
|
||||
"""hello — prove the key works.
|
||||
|
||||
The smallest complete call: prove the key works and print the identity behind
|
||||
it. ``GET /v1/bot/auth/me`` (operationId ``bot_authMe``).
|
||||
``GET /v1/keys`` (operationId ``get_v1_keys``): the caller's own API keys.
|
||||
|
||||
This flow's whole job is to FAIL when the key is bad, so the route has to be one
|
||||
that actually checks. Not every identity-shaped route does: ``/v1/ai/account``
|
||||
answers 200 with ``type="anonymous-user"`` to a request carrying no
|
||||
Authorization header at all, so a ``hello`` built on it prints a cheerful
|
||||
identity for a key that would 401 everywhere else — worse than no check, because
|
||||
it reads as proof. ``/v1/bot/auth/me`` answers 403
|
||||
``{"error":"no validated principal"}``. Verified against api.hanzo.ai, not
|
||||
assumed from the spec.
|
||||
that actually checks, and that is decided by probing api.hanzo.ai rather than by
|
||||
reading the document. Three identity-shaped routes are disqualified for
|
||||
answering 200 to a caller carrying no credential at all — ``/v1/ai/account``
|
||||
(``type="anonymous-user"``), ``/v1/iam/whoami`` and ``/v1/iam/account`` (both
|
||||
200 ``{"status":"error"}``) — and a ``hello`` built on any of them prints a
|
||||
cheerful identity for a key that would be refused everywhere else, which is
|
||||
worse than no check because it reads as proof.
|
||||
|
||||
``/v1/keys`` answers 403 ``{"code":"forbidden","error":"sign in to manage API
|
||||
keys"}`` with no key and with a bogus one, while the nonsense sibling
|
||||
``/v1/keys-zzq9`` answers 404 — so the 403 is this route refusing, not a
|
||||
wildcard door swallowing the address.
|
||||
|
||||
This replaces ``bot_authMe`` (``GET /v1/bot/auth/me``), which no longer
|
||||
resolves: cloud relays all of ``/v1/bot`` through one ``app.All("/v1/bot/*")``,
|
||||
so the document carries ``/v1/bot/{wildcard1}`` and no operation at that
|
||||
address. The old id existed only in a hand-authored spec and vanished with it —
|
||||
which is the reason an example's operationId has to come from the served
|
||||
document.
|
||||
|
||||
python -m examples.hello
|
||||
"""
|
||||
|
||||
from hanzoai.cloud import AuthApi
|
||||
from hanzoai.cloud import KeysApi
|
||||
|
||||
from examples.client import BASE_URL, client, run
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with client() as api:
|
||||
me = AuthApi(api).bot_auth_me()
|
||||
listing = KeysApi(api).get_v1_keys()
|
||||
|
||||
keys = listing.keys or []
|
||||
print(f"hello from {BASE_URL}")
|
||||
print(f" {me.display_name or me.handle or '(unnamed)'} <{me.email or 'no email'}>")
|
||||
print(f" id {me.id} · role {me.role}")
|
||||
print(f" the key is accepted; this org has {len(keys)} API key(s)")
|
||||
for key in keys[:5]:
|
||||
# `key.key` is the secret itself and is never printed. The prefix is the
|
||||
# part that names one without disclosing it.
|
||||
print(f" {key.prefix or '(no prefix)'} · {key.type or 'untyped'} · created {key.created_at or 'unknown'}")
|
||||
if len(keys) > 5:
|
||||
print(f" … and {len(keys) - 5} more")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""money — what is in the wallet, and what spent it.
|
||||
|
||||
GET /v1/billing/balance cloud_get_v1_billing_balance
|
||||
GET /v1/billing/usage cloud_get_v1_billing_usage
|
||||
GET /v1/billing/balance get_v1_billing_balance
|
||||
GET /v1/billing/usage get_v1_billing_usage
|
||||
|
||||
Neither call takes an org: both derive the tenant SERVER-side from the token's
|
||||
``owner`` claim, so a key can only ever read its own money. There is no org
|
||||
@@ -28,8 +28,8 @@ def main() -> None:
|
||||
with client() as api:
|
||||
billing = BillingApi(api)
|
||||
for label, call in (
|
||||
("balance", billing.cloud_get_v1_billing_balance_without_preload_content),
|
||||
("usage", billing.cloud_get_v1_billing_usage_without_preload_content),
|
||||
("balance", billing.get_v1_billing_balance_without_preload_content),
|
||||
("usage", billing.get_v1_billing_usage_without_preload_content),
|
||||
):
|
||||
response = call()
|
||||
body = response.read()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""store — a KV round-trip: provision a store, read it back, delete it.
|
||||
|
||||
POST /v1/kv cloud_post_v1_kv provision
|
||||
GET /v1/kv/{name} cloud_get_v1_kv_name read back
|
||||
DELETE /v1/kv/{name} cloud_delete_v1_kv_name tear down
|
||||
POST /v1/kv post_v1_kv provision
|
||||
GET /v1/kv/{name} get_v1_kv_name read back
|
||||
DELETE /v1/kv/{name} delete_v1_kv_name tear down
|
||||
|
||||
This is the PROVISIONING plane. The per-key data plane the spec also describes
|
||||
(``/v1/kv/keys/{key}``) is not mounted anywhere — GET 404s and PUT/DELETE 405 at
|
||||
@@ -18,7 +18,7 @@ rather than leaving it billable for the next run to collide with.
|
||||
|
||||
import time
|
||||
|
||||
from hanzoai.cloud import CloudProvisionRequest, KvApi
|
||||
from hanzoai.cloud import ProvisionRequest, KvApi
|
||||
|
||||
from examples.client import client, run
|
||||
|
||||
@@ -29,15 +29,15 @@ def main() -> None:
|
||||
with client() as api:
|
||||
kv = KvApi(api)
|
||||
|
||||
kv.cloud_post_v1_kv(CloudProvisionRequest(name=NAME))
|
||||
kv.post_v1_kv(ProvisionRequest(name=NAME))
|
||||
print(f"provisioned {NAME}")
|
||||
|
||||
try:
|
||||
store = kv.cloud_get_v1_kv_name(NAME)
|
||||
store = kv.get_v1_kv_name(NAME)
|
||||
print(f"read back: {store.name} · {store.kind} · status {store.status}")
|
||||
print(f" host {store.host}:{store.port}")
|
||||
finally:
|
||||
kv.cloud_delete_v1_kv_name(NAME)
|
||||
kv.delete_v1_kv_name(NAME)
|
||||
print(f"deleted {NAME}")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""tools — list the tools this key can reach.
|
||||
|
||||
``GET /v1/tools`` (operationId ``cloud_get_v1_tools``), the catalog behind the
|
||||
``GET /v1/tools`` (operationId ``get_v1_tools``), the catalog behind the
|
||||
MCP surface: each entry is a tool name, its description and its input schema.
|
||||
|
||||
A note on the MCP door, because it is easy to pick the wrong one. There is a
|
||||
@@ -22,7 +22,7 @@ from examples.client import client, run
|
||||
|
||||
def main() -> None:
|
||||
with client() as api:
|
||||
catalog = ToolsApi(api).cloud_get_v1_tools()
|
||||
catalog = ToolsApi(api).get_v1_tools()
|
||||
|
||||
tools = catalog.tools or []
|
||||
print(f"{len(tools)} tools")
|
||||
|
||||
Reference in New Issue
Block a user