Skip to main content
The model proxy is the model router 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 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 and client.proxy.completions. Behind either envelope it is the same machinery a session runs on: the same model catalogue — vetta/auto included — the same completion-window router, the same five-tier metering against the same prepaid balance. Nothing about the proxy is a separate lane.
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.It does keep a record of each call for your own inspection — see Call traces below, including what is stored and for how long.

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.
--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. They need a key with audit:read — deliberately not the scope that makes the calls.

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, 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, so the proxy reads one from a header and defaults to immediate:
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 for which windows each model serves.

POST /v1/proxy/anthropic/v1/messages

string
required
A Vetta model id, exactly as the model catalogue lists it — for example openai/gpt-oss-120b. Model ids from other providers’ catalogues are not accepted.
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.
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).
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.
string | object[]
The system prompt, as a string or an array of text blocks.
boolean
Stream the answer as server-sent events instead of returning one JSON body. Defaults to false.
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.

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.
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.
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.
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 and client.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.

What is stored

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

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. You are billed the price on the rate card 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).