> ## 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.

`POST /v1/proxy/anthropic/v1/messages` speaks the **Anthropic Messages request and response format** and nothing else. Point an existing client at `https://api.vetta.sh/v1/proxy/anthropic`, give it a Vetta API key, and it works — no SDK to swap, no code to change.

Behind the envelope it is the same machinery a [session](/docs/concepts/sessions) runs on: the same [model catalogue](/docs/concepts/model-router), 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, keeps no state between calls, and has no event log — the conversation is whatever you send in `messages`. If you want a durable agent that keeps working across turns, that is [`POST /v1/sessions`](/docs/api/sessions).
</Note>

## From the CLI

Two commands, for the two things a terminal is good for here. `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.

## 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 `sessions:write` [scope](/docs/api/authentication) — a proxied call is metered model spend, exactly like a session's. 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="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` 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.

## 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.

## 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, and a stream you hang up on part-way through is not billed.
