> ## Documentation Index
> Fetch the complete documentation index at: https://vetta.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Model proxy

> Point an Anthropic- or OpenAI-compatible SDK at Vetta: your API key, your organization's balance, one rate card.

The model proxy is the [model router](/docs/concepts/model-router#calling-the-router-directly) with no agent in front of it: one model call, billed to the organization. It has two doors. `POST /v1/proxy/anthropic/v1/messages` speaks the **Anthropic Messages request and response format**; [`POST /v1/proxy/openai/v1/chat/completions`](#post-v1proxyopenaiv1chatcompletions) speaks **Chat Completions**. Point an existing client at `https://api.vetta.sh/v1/proxy/anthropic` (or `…/v1/proxy/openai/v1`), give it a Vetta API key, and it works — no SDK to swap, no code to change. From this SDK the same two calls are [`client.proxy.messages`](/docs/sdk/proxy#messages) and [`client.proxy.completions`](/docs/sdk/proxy#completions).

Behind either envelope it is the same machinery a [session](/docs/concepts/sessions) runs on: the same [model catalogue](/docs/concepts/model-router) — `vetta/auto` included — the same [completion-window router](/docs/concepts/completion-window), the same [five-tier metering](/docs/platform/billing) against the same prepaid balance. Nothing about the proxy is a separate lane.

```bash theme={"system"}
export ANTHROPIC_BASE_URL=https://api.vetta.sh/v1/proxy/anthropic
export ANTHROPIC_AUTH_TOKEN=sk_live_...
```

<Note>
  This is a **passthrough for model calls only**. It runs no tools for you and keeps no state between calls — the conversation is whatever you send in `messages`, and nothing the proxy stores is ever fed back into a later call. If you want a durable agent that keeps working across turns, that is [`POST /v1/sessions`](/docs/api/sessions).

  It does keep a **record** of each call for your own inspection — see [Call traces](#call-traces) below, including what is stored and for how long.
</Note>

## From the CLI

Four commands. `vetta proxy url` reports the endpoint for the profile you are on — production, staging, or your own deploy — so you configure a client against the right one rather than a URL copied off a page. `vetta proxy message` sends one call, which is how you confirm the key, the plan, the balance, the model and the window all work *before* repointing a production client at it.

```bash theme={"system"}
vetta proxy url --human
vetta proxy message --model openai/gpt-oss-120b --text "Name three sorting algorithms." --window priority
```

`--system` sets a system prompt and `--max-tokens` the output allowance (default `1024`). Streaming has no command: a client that streams already has one, and keeping it working unchanged is the point of the endpoint.

`vetta proxy calls` and `vetta proxy call <request_id>` read back [call traces](#call-traces). They need a key with `audit:read` — deliberately *not* the scope that makes the calls.

```bash theme={"system"}
vetta proxy calls --limit 20
vetta proxy call req_01j9y2p0q4r5s6t7u8v9w0x1y2
```

## Authentication

The same organization-scoped API key as every other endpoint, in the same `Authorization: Bearer` header every other endpoint uses. Clients that default to an `x-api-key` header have to be told to send a bearer token instead — in the official SDKs that is the `auth_token` option, or the `ANTHROPIC_AUTH_TOKEN` environment variable shown above.

The key must hold the `proxy:write` [scope](/docs/api/authentication#scopes), or `sessions:write`, which the proxy routes accept in its place. `proxy:write` is the narrowest scope that reaches these routes: it cannot start, cancel or read a session, and it cannot write an agent. It is **not** a confinement boundary, though — a number of read endpoints declare no scope at all and accept any authenticated key, so a `proxy:write` credential still reaches things like the organization's members and its computer inventory. Treat it as "this key may spend on model calls", not as "this key can do nothing else", and do not hand it to a party you would not trust with those reads. The organization must hold an active plan and enough prepaid balance to cover the request's pre-flight quote.

## Choosing a completion window

The Messages format has no field for a [completion window](/docs/concepts/completion-window), so the proxy reads one from a header and defaults to `immediate`:

```bash theme={"system"}
Vetta-Window: immediate | priority | loose
```

Routing is **by window**, never by guessing from the model name, and a window is never quietly downgraded. If the model you named has no published price in the window you asked for, the call is refused with `window_unavailable` **before anything is spent** — see the [model router](/docs/concepts/model-router) for which windows each model serves.

## POST /v1/proxy/anthropic/v1/messages

<ParamField body="model" type="string" required>
  A Vetta model id, exactly as the [model catalogue](/docs/concepts/model-router) lists it — for example `openai/gpt-oss-120b`. Model ids from other providers' catalogues are not accepted.
</ParamField>

<ParamField body="model_selection" type="object">
  Optional. Let the router pick which of your models answers this call: `{ "options": { "<model id>": "<what it is for>", … }, "min_confidence"?: number }`, two to sixteen concrete catalogue ids (`vetta/auto` is not one). `model` must be one of the keys and is the default when the selector's confidence is below `min_confidence`. The reply's `model` names the pick; the selector's own input tokens are one more `input` line on the same ledger entry. If the selector cannot answer, the call is refused and nothing is spent. See [model auto-selection](/docs/concepts/model-selection#select-during-a-call).
</ParamField>

<ParamField body="messages" type="object[]" required>
  The conversation. Each turn is `{ "role": "user" | "assistant", "content": … }`, and `content` is either a string or an array of content blocks. Three block types are supported: `text`, `tool_use` (on an assistant turn) and `tool_result` (on a user turn).
</ParamField>

<ParamField body="max_tokens" type="integer" required>
  The output allowance for this call. It sets the size of the pre-flight quote held against your balance, so asking for a larger answer requires more headroom.
</ParamField>

<ParamField body="system" type="string | object[]">
  The system prompt, as a string or an array of `text` blocks.
</ParamField>

<ParamField body="stream" type="boolean">
  Stream the answer as server-sent events instead of returning one JSON body. Defaults to `false`.
</ParamField>

<ParamField body="tools" type="object[]">
  Tools the model may call, each `{ "name", "description", "input_schema" }`. The model's calls come back as `tool_use` blocks with `stop_reason: "tool_use"`; run them yourself and send the results back as `tool_result` blocks on the next user turn.
</ParamField>

### Fields that are refused

The proxy carries every field it advertises and **refuses anything else with `400 validation_failed` naming the key** rather than accepting it and ignoring it — a silently dropped setting is a call you paid for and did not ask for.

Not supported today: `temperature`, `top_p`, `top_k`, `stop_sequences`, `thinking`, `tool_choice`, `metadata`, `service_tier`, and image or document content blocks.

<CodeGroup>
  ```bash Request theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/proxy/anthropic/v1/messages \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -d '{
          "model": "openai/gpt-oss-120b",
          "max_tokens": 1024,
          "messages": [{ "role": "user", "content": "Name three sorting algorithms." }]
        }'
  ```

  ```json Response theme={"system"}
  {
    "id": "req_01j9y2p0q4r5s6t7u8v9w0x1y2",
    "type": "message",
    "role": "assistant",
    "model": "openai/gpt-oss-120b",
    "content": [{ "type": "text", "text": "Quicksort, mergesort and heapsort." }],
    "stop_reason": "end_turn",
    "stop_sequence": null,
    "usage": {
      "input_tokens": 14,
      "output_tokens": 9,
      "cache_read_input_tokens": 0,
      "cache_creation_input_tokens": 0
    }
  }
  ```
</CodeGroup>

`id` is the request id, the same value as the `x-request-id` response header — so the message, the log line and the ledger entry all name one thing.

## POST /v1/proxy/openai/v1/chat/completions

The same proxy in the **Chat Completions** dialect, for a client that speaks that envelope and nothing else. Same authentication, same rate card, same meter, same window header — only the request and response shapes differ. Point an OpenAI-compatible SDK at `<baseUrl>/v1/proxy/openai/v1` and set your Vetta key as its API key.

```bash theme={"system"}
curl https://api.vetta.sh/v1/proxy/openai/v1/chat/completions \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{
    "model": "zai-org/GLM-5.2-FP8",
    "messages": [{ "role": "user", "content": "Summarise this invoice." }]
  }'
```

Two differences from the Messages door are worth knowing:

* **The output ceiling is optional.** `max_completion_tokens` (or `max_tokens`) may be omitted, and then the call is bounded by whatever the model may emit — which is also what the pre-flight quote reserves against your balance.
* **Usage always arrives.** On a streamed call the usage chunk is sent before `data: [DONE]` whether or not you asked for it with `stream_options`. A coding agent meters its own context window off those counts, and withholding them makes it guess.

A field that would change the answer is still refused by name — `top_p`, `seed`, `logprobs`, `response_format`, `tool_choice` and the penalties all answer `validation_failed`, because silently dropping one bills you for a call you did not ask for. Fields that cannot change the answer (`session_id`, `stream_options`) are accepted and ignored. `temperature` is the one exception on this door: it is **accepted and not forwarded**, because the clients this envelope exists for send it on every request and cannot be told not to — the model runs at its own default.

## Streaming

With `"stream": true` the response is `text/event-stream` carrying the Messages event sequence: `message_start`, then `content_block_start` / `content_block_delta` / `content_block_stop` per block, then `message_delta` with the stop reason and usage, then `message_stop`. Text arrives as `text_delta`; a tool call arrives as a `tool_use` block whose arguments come through as `input_json_delta`.

Deltas are flushed as the model produces them — a streaming client sees the answer being written, not one buffered body at the end.

```
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Quick"}}
```

The Chat Completions door streams the same way in its own spelling: bare `data:` frames with no `event:` line, a usage chunk, then the literal `data: [DONE]`.

A caller that **hangs up mid-answer is still billed**. The tokens were generated and the provider charged us for them, so the stream is drained and metered whatever the client did — leaving early is not a discount.

`Idempotency-Key` is accepted on this route but a streamed response is never recorded for replay: an event stream is not a value that can be handed back a second time. A retry re-runs the call.

## Call traces

Every proxied call leaves a **trace**: one record of what the call was, beside the ledger entry that says what it cost. It answers the questions the ledger cannot — which key sent this, which model actually served it, what did the vendor charge us for it, and exactly what the prompt was and what came back — whole, never truncated.

`GET /v1/proxy/calls` lists them newest first, keyset-paged like every other list. `GET /v1/proxy/calls/{request_id}` reads one. From this SDK: [`client.proxy.calls`](/docs/sdk/proxy#calls) and [`client.proxy.call`](/docs/sdk/proxy#call).

The id is **the request id** — the same value on `x-request-id`, on the reply's `id`, and as the idempotency key of the call's ledger entry. So a trace, a debit and the answer you are holding all name one call, and you need nothing from us to look one up.

```bash theme={"system"}
curl https://api.vetta.sh/v1/proxy/calls/req_01j9y2p0q4r5s6t7u8v9w0x1y2 \
  -H "Authorization: Bearer sk_live_..."
```

### What is stored

| Field                                                                  | What it is                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`, `created_at`, `dialect`                                          | The call: its request id, when it ran, and which door it came in by.                                                                                                                                                                                                                                                                                                                                                                                                      |
| `actor`                                                                | The principal that made it — for an API key, **which key**.                                                                                                                                                                                                                                                                                                                                                                                                               |
| `model`, `window`, `resolved_model`                                    | What was asked for, the window it ran in, and what actually answered (this is where `vetta/auto` resolves).                                                                                                                                                                                                                                                                                                                                                               |
| `provider`, `generation_id`                                            | The serving provider and the gateway's own id for the generation, where the upstream reports them.                                                                                                                                                                                                                                                                                                                                                                        |
| `usage`, `stop`, `duration_ms`                                         | The five token tiers, how the turn ended, and how long the call took.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `billed_micro_usd`, `vendor_cost_micro_usd`                            | What you were charged, and what the vendor said it cost us.                                                                                                                                                                                                                                                                                                                                                                                                               |
| `request_sha256`, `response_sha256`, `request_bytes`, `response_bytes` | A hash and a size for each side — **always present**.                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `request`, `response`                                                  | The literal prompt (structured messages with tool-call ids, the full tool schemas, the output allowance) and the reply (text, reasoning, tool calls with ids). A call that failed after reaching the model is traced too: `response.is_error` is `true`, `response.error_class` names the failure's class (or `incomplete`), `response.partial` is `true`, and `billed_micro_usd` is null. Inline on every row, on a list page and on `GET /v1/proxy/calls/{request_id}`. |
| `content_scope`                                                        | `full`: the bodies are inline on the row. `metadata` (and `object`, with null bodies) appear only on rows written before the current capture.                                                                                                                                                                                                                                                                                                                             |
| `tool_schema_hash`, `tool_call_ids`                                    | A hash of the tool schemas the model was offered, and the ids of the tool calls it made.                                                                                                                                                                                                                                                                                                                                                                                  |
| `source`, `session_id`, `seq`, `board_card_id`                         | `proxy` for a call through this route. A call made from a hosted agent's sandbox names its session.                                                                                                                                                                                                                                                                                                                                                                       |
| `expires_at`                                                           | Null: traces are kept. Rows written before capture became permanent still carry the deadline they were written with.                                                                                                                                                                                                                                                                                                                                                      |

### Retention, and who can read it

**Prompts are customer content, and a trace is the only place this platform keeps them.** So three things are true of every trace, and are visible on the row itself rather than buried in a setting:

* **It is kept, whole.** Every call is traced and nothing is cut: there is no size cap and no expiry on new rows.
* **It is scoped to your organization.** Traces are tenant rows under the same row-level isolation as everything else: another organization's request id reads as `not_found`, exactly as an id that never existed does.
* **Reading one needs `audit:read`.** Not `proxy:write` — the scope that *makes* proxy calls cannot read back what they said. That is deliberate: `proxy:write` is the narrow scope you hand to a sandbox or a third-party client, and if it could read traces, a leak of it would expose every prompt your organization has ever sent. For the same reason these routes are withheld from the MCP tool table: an agent is not handed the stored conversations of every other call.

Prompts are never written to logs.

## Errors

Failures use **Vetta's error envelope**, not the Messages format's — one shape across the whole API, so your error handling does not fork:

```json theme={"system"}
{
  "error": {
    "type": "invalid_request",
    "code": "window_unavailable",
    "message": "completion window `priority` is unavailable for model `openai/gpt-oss-120b`",
    "request_id": "req_01j9y2p0q4r5s6t7u8v9w0x1y2",
    "param": "window"
  }
}
```

The codes you are most likely to see here are `window_unavailable` (400), `validation_failed` (400), `insufficient_credits` (402), `subscription_required` (402) and `rate_limited` (429). The full taxonomy is in [Errors](/docs/api/errors).

## Billing

Every proxied call books one debit against the organization's prepaid balance, split across the same five token tiers a session's spend uses — `input`, `cache_write`, `cache_read`, `output`, `reasoning`. There is no session id and no agent id on the entry, because the call belongs to the organization itself; read it back on [`GET /v1/credits/ledger`](/docs/api/credits).

You are billed the price on the [rate card](/docs/platform/pricing) for the model and window you actually ran in. A call that is refused before it reaches a model — a bad window, an empty balance — costs nothing. A stream you hang up on part-way through **is** billed for the tokens the model produced: the stream is drained and metered whatever the client did (see [Streaming](#streaming)).
