# OpenAI

URL: https://docs.hanzo.ai/docs/guides/migrate/openai

> OpenAI serves models and the platform grown around them. Here that is /v1/ai (272 operations) for the models, their files and their routing, and /v1/agent (37) for the assistant that runs, with the OpenAI-shaped inference routes at the addresses your client already calls.

OpenAI is a model API with a platform grown around it: chat and embeddings at the
centre, then assistants, files, vector stores, fine-tuning, batch and realtime.
Two capabilities answer it. `/v1/ai` (272 operations) holds the models, the files
they read, the stores they search, fine-tuning and the router; `/v1/agent` (37)
holds the assistant, its runs and its live sessions. The OpenAI-shaped routes are
at the addresses your client already calls — `POST /v1/chat/completions`,
`/v1/embeddings`, `/v1/responses`, `/v1/images/generations`, `/v1/audio/speech` —
so for plain inference the base URL is the whole edit. The structural difference
sits above that: an assistant here is one identifier and one call to run, not
three identifiers and a poll.

## Start here

Mint a key, change the host, and the request body you already send works
unchanged.

```bash
# 1. mint a key — sk- belongs on a server, pk- is safe in a browser
curl -sS -X POST https://api.hanzo.ai/v1/account/keys \
  -H "Authorization: Bearer $HANZO_SESSION" \
  -H 'Content-Type: application/json' \
  -d '{"type":"secret"}'

# 2. the call you already make, at a different host
curl -sS -X POST https://api.hanzo.ai/v1/chat/completions \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"model":"zen6","messages":[{"role":"user","content":"summarise Q3"}]}'

# 3. the catalogue the model name in step 2 comes from
curl -sS https://api.hanzo.ai/v1/models
```

Step 2 is your existing request with a new host and a new key — no field in the
body changed, so plain inference is a base URL and a secret. Step 3 is where the
names it accepts are published, and it answers with no `Authorization` header at
all, which is why a 200 there says nothing about your key and step 2's answer
does.

### Core capabilities

| Capability  | What it does                                                            | Operations |
| ----------- | ----------------------------------------------------------------------- | ---------- |
| `/v1/ai`    | Models, files, stores, RAG, fine-tuning, memory and the router          | 272        |
| `/v1/agent` | The assistant — create it, run it, stream it, list every run in the org | 37         |
| `/v1/tool`  | The named tools an agent may call, and the MCP servers behind them      | 19         |

## Nouns

Four tables, because OpenAI is four products stacked on one another.

### Inference

| OpenAI                                                 | Hanzo                                                                                                |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `POST /v1/chat/completions`                            | The same address, `POST /v1/chat/completions`                                                        |
| `POST /v1/responses`                                   | `POST /v1/responses` — converted, then completed by the chat path                                    |
| `POST /v1/completions`                                 | `POST /v1/completions`                                                                               |
| `POST /v1/embeddings`                                  | `POST /v1/embeddings`, or `zen-embedding` at 8K context                                              |
| `GET /v1/models`                                       | `GET /v1/models` — 432 models from 57 providers, 14 of them Zen                                      |
| `POST /v1/images/generations`                          | `POST /v1/images/generations`                                                                        |
| `POST /v1/audio/speech` and `/v1/audio/transcriptions` | The same two addresses, `/v1/audio` (5)                                                              |
| `POST /v1/moderations`                                 | `zen-guard`, a 128K-context safety classifier, called like any other model                           |
| No rerank endpoint                                     | `POST /v1/rerank` — a native provider when the model routes to one, cosine over embeddings otherwise |
| `OpenAI-Organization` and `OpenAI-Project` headers     | Neither. The org is the validated key's, and there is no field for it                                |
| A form to request access to a gated model              | `POST /v1/models/{model}/access`, and `GET` for your own standing                                    |
| The Anthropic-shaped client you also maintain          | `POST /v1/messages`, with `POST /v1/messages/count_tokens` beside it                                 |

### The assistant

| OpenAI                                        | Hanzo                                                                                   |
| --------------------------------------------- | --------------------------------------------------------------------------------------- |
| Assistant                                     | Agent — `POST /v1/agent`, carrying `model`, `instructions` and `tools`                  |
| `assistant_id`                                | The `agent_…` id or the org-unique name you chose. Either resolves the same agent       |
| Thread, and `thread_id`                       | `conversationId` on `POST /v1/agent/chat`, listed at `/v1/agent/chat/conversations`     |
| Run, then poll `GET /v1/threads/{t}/runs/{r}` | `POST /v1/agent/{ref}/run` — answers the finished run                                   |
| Reading the answer back off the thread        | The run's `output`, in that same response                                               |
| A run's per-assistant history                 | `GET /v1/agent/{ref}` — the agent plus its 20 most recent runs                          |
| No feed across assistants                     | `GET /v1/agent/runs` — every run in the org, newest first, filterable by status         |
| Tool of type `function`                       | A name from `GET /v1/tool` (19), dispatched with `POST /v1/tool/call`                   |
| Tool of type `code_interpreter`               | `POST /v1/sandbox/run` (19) — exit code, stdout and stderr returned as data             |
| Tool of type `file_search`                    | `POST /v1/ai/rag/query` over what `POST /v1/ai/rag/ingest` indexed                      |
| A remote MCP server named on a response       | `POST /v1/tool/mcp/servers` registers it; `POST /v1/mcp` is the endpoint                |
| Streaming run events                          | `GET /v1/agent/sessions/stream` — server-sent events, filtered on tenant before fan-out |
| Scheduling a run yourself                     | `executionMode: long-running` with a 5-field cron, parsed at create                     |

### The data the model reads

| OpenAI                                           | Hanzo                                                                                     |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| File                                             | `POST /v1/ai/files`, bytes through `POST /v1/ai/files/upload`                             |
| Listing what you uploaded                        | `GET /v1/ai/files`, scoped to the caller                                                  |
| Vector store                                     | Store — `POST /v1/ai/stores`, vectors at `/v1/ai/stores/{owner}/{name}/vectors`           |
| Chunking and embedding you drive yourself        | `POST /v1/ai/rag/ingest` — parse, chunk, embed, into the vector and keyword index at once |
| A connector you write to pull in Drive or Notion | `/v1/knowledge/connectors` (6), synced                                                    |
| Fine-tuning job                                  | `POST /v1/ai/finetune/jobs`, status refreshed on `GET`                                    |
| Serving the fine-tuned result                    | `POST /v1/ai/finetune/deploy` — registers it as a routable model name                     |
| Prompt object, referenced by id and version      | `POST /v1/prompt` (6) — a repeated name appends a version rather than overwriting         |
| Per-user context you keep in your own database   | `POST /v1/ai/memory/remember`, read with `GET /v1/ai/memory/recall`                       |

### Around the model

| OpenAI                                | Hanzo                                                             |
| ------------------------------------- | ----------------------------------------------------------------- |
| Project, and who is in it             | Org, from `/v1/iam` (159)                                         |
| Service account                       | `POST /v1/iam/service-accounts/{name}/keys`                       |
| API key                               | `/v1/iam/keys`                                                    |
| Usage and Costs in the dashboard      | `GET /v1/usage/summary` and `GET /v1/billing/usage` (45)          |
| Rate-limit tier                       | `GET /v1/billing/tier`                                            |
| Spend limit and its email             | `/v1/billing/alerts`                                              |
| Evals — dataset, grader, run          | `/v1/eval` (16): `datasets`, `evaluators`, `runs`, `traces`       |
| Webhooks                              | `/v1/webhook` (8). The signing secret is returned once, on create |
| Web search folded into a response     | `POST /v1/websearch` (7), a call of its own                       |
| Your own provider key, used by you    | `POST /v1/ai/connections` — sealed into KMS, never echoed back    |
| Model choice hardcoded in your caller | `PUT /v1/ai/router/policy`, or the model name `router:general`    |

## The call

OpenAI's assistant, end to end. Six requests and three identifiers:

```bash
ASSISTANT=$(curl -sS https://api.openai.com/v1/assistants \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H 'OpenAI-Beta: assistants=v2' -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4o","name":"helper","instructions":"be terse"}' | jq -r .id)

THREAD=$(curl -sS -X POST https://api.openai.com/v1/threads \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H 'OpenAI-Beta: assistants=v2' | jq -r .id)

curl -sS https://api.openai.com/v1/threads/$THREAD/messages \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H 'OpenAI-Beta: assistants=v2' -H 'Content-Type: application/json' \
  -d '{"role":"user","content":"summarise Q3"}'

RUN=$(curl -sS https://api.openai.com/v1/threads/$THREAD/runs \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H 'OpenAI-Beta: assistants=v2' -H 'Content-Type: application/json' \
  -d "{\"assistant_id\":\"$ASSISTANT\"}" | jq -r .id)

# poll this until .status stops saying queued or in_progress
curl -sS https://api.openai.com/v1/threads/$THREAD/runs/$RUN \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H 'OpenAI-Beta: assistants=v2'

# then read the answer back off the thread
curl -sS https://api.openai.com/v1/threads/$THREAD/messages \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H 'OpenAI-Beta: assistants=v2'
```

`POST /v1/threads/runs` collapses the middle three into one. The poll and the
read-back stay.

Hanzo. Two requests and one identifier, which is the name you picked:

```bash
curl -sS -X POST https://api.hanzo.ai/v1/agent \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"helper","model":"zen5","instructions":"be terse"}'

curl -sS -X POST https://api.hanzo.ai/v1/agent/helper/run \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"input":"summarise Q3"}'
```

Plain inference is the base URL and nothing else:

```bash
curl -sS -X POST https://api.hanzo.ai/v1/chat/completions \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"model":"zen6","messages":[{"role":"user","content":"summarise Q3"}]}'
```

The run call answers the run — id, status, model, `output`, duration, error —
because the work is finished when the response is written. There is nothing to
poll, so there is no window in which a reader can see a stale one. The name is
the address, so `helper` is reachable the moment create returns and
nothing has to be stored client-side to find it again; the org is the key's, so
an agent in another tenant is a 404 rather than a 403, which is a property of the
lookup rather than of a check that could be skipped. The balance is authorised
before any inference, so an unfunded org gets 402 and no free compute, and only a
successful run is billed — attributed to the model that actually answered, which
after a failover is not the one it started on.

## What does not carry

**No Batch API, and no batch discount.** OpenAI's `/v1/batches` takes a JSONL
file, promises a 24-hour window and prices at half. There is no equivalent
address and no discounted asynchronous tier: a fan-out is a loop you write, and
`/v1/mq` (15) gives that loop a durable queue when the work must survive a
restart. Per-token price is what `GET /v1/models` publishes, the same for ten
thousand calls as for one.

**Realtime is not a socket.** OpenAI's Realtime API is a bidirectional WebSocket
carrying audio both ways with server-side turn detection. Speech here is request
and response — `POST /v1/audio/transcriptions` in, `POST /v1/audio/speech` out —
and the sockets that do exist carry sessions and streams, not audio. For a spoken
agent on a phone line, `POST /v1/tel/calls` dials one and names the agent that
answers it.

**Conversation state and runs are two surfaces, not one.** OpenAI keeps the
messages on a thread and every run against them in the same place.
`POST /v1/agent/{ref}/run` is one shot: it composes the agent's stored
instructions with your `input` and returns. Multi-turn state lives on
`POST /v1/agent/chat` with a `conversationId`, listed at
`/v1/agent/chat/conversations`. Pick by whether the turn needs the last one.

**A tool is a name the org holds, not a schema you send.** OpenAI takes the
function definition in the request body, so the callable set is whatever that
request says. An agent's `tools` is a list of names resolved from `GET /v1/tool`
at run time, and omitting it grants none — that default is the agent's authority
and nothing widens it later. Your own function goes in by publishing an MCP
server and registering it with `POST /v1/tool/mcp/servers`; it then appears in
the same listing.

**Fine-tuning is still a job you poll.** No parity claim here.
`POST /v1/ai/finetune/jobs` submits a training job and `GET /v1/ai/finetune/jobs`
refreshes live status, the same shape you already have. What differs is the end:
`POST /v1/ai/finetune/deploy` serves the checkpoints and registers the result as a
routable model name, so the trained model answers on `/v1/chat/completions` like
any other. A submit that fails is saved with status `failed` and the reason,
never as a job that quietly never starts.

**`GET /v1/models` does not authenticate.** OpenAI's model list sits behind your
key. This one is public and says so out loud: the catalogue is identical for
every caller, and an `Authorization` header is read for exactly one thing —
annotating gated models with your own standing. A dead key gets 200 here, so it
is the wrong probe for "is my auth working". Use a route that acts on your org.
