Skip to content

Supported API surface

Map of the OpenAI-compatible surface against what llmproxy implements. Wire shapes follow OpenAI's own API reference; requests are forwarded as-is except for the model rewrite and stream_options.include_usage injection on streams. The full machine-readable spec is in openapi.yaml.

Caller-facing (/v1, OpenAI dialect)

Endpoint Status Notes
POST /v1/chat/completions Supported Unary JSON and SSE streaming. Vision content parts, tool/function calls, response_format, logprobs etc. pass through untouched. Capability chat (+ chat_stream for streaming).
POST /v1/completions Supported Legacy text completions, unary and streamed. Capability completions.
GET /v1/models Supported Aliases on enabled providers, minus the hidden ones; ?endpoint=chat\|embeddings\|... filters by capability. ?include_hidden=1 adds hidden models and needs an API key or session (401 without one). ?include_pricing=1 adds pricing (per million units, the prices in force) and pricing_inherited, and needs an API key or session too. Each entry carries the OpenAI fields plus capabilities (resolved), alias_of (the model this name points at, or null) and hidden. Public, no API key required.
GET /v1/models/{id} Not yet Trivial to add; nothing has needed it.
POST /v1/embeddings Supported Unary passthrough. Capability embeddings. Array input is capped at LLMPROXY_MAX_EMBEDDING_BATCH items (default 2048); larger batches get 400 embedding_batch_too_large.
POST /v1/audio/transcriptions Planned Streamed multipart, no disk spill; audio_seconds unit reserved.
POST /v1/audio/translations Out of scope Add on demand once transcription exists.
POST /v1/responses Out of scope for 1.0 OpenAI Responses API; revisit with evidence.
/v1/batches, /v1/files Out of scope Deliberate non-goal; see architecture.
/v1/images/*, /v1/audio/speech, /v1/moderations, fine-tuning, assistants Out of scope Non-goals.
POST /v1/messages (Anthropic dialect) Planned Second ingress adapter over the same catalog. Until then, the transparent relay (below) covers Anthropic-native tooling that brings its own credentials.

Auth: Authorization: Bearer lp_... or x-api-key. Unknown (including deleted) keys get 401 invalid_api_key; key management is database-authoritative, so deletion revokes a key immediately. GET /v1/models is the exception: it is served without authentication, since it only exposes curated aliases and provider names. Its include_hidden=1 form is not, since it names models an admin took off the list.

Model names callers see are curated aliases, globally unique across all providers; there is no bare-upstream-name fallback and no multi-provider resolution. Calling an endpoint outside a model's capability set fails at the proxy with a 400 naming the supported capabilities, never with a confusing upstream 404.

Every response carries x-llmproxy-provider and x-llmproxy-model. Errors are OpenAI-shaped with an added llmproxy.source field plus x-llmproxy-error-source: proxy|upstream; upstream error bodies and status codes pass through intact.

Requests may carry x-llmproxy-tags, a comma-separated list of key:value pairs (app:dataindex,context:search) naming the calling application. The proxy stores the normalised list on the usage event and the Usage tab's Apps subtab breaks spend down by it; see usage. Malformed pairs are dropped rather than rejected, and the header never reaches an upstream.

No request or response content is ever logged or persisted, and there is no flag to turn that on; see architecture.

Proxy-generated error codes: missing_api_key, invalid_api_key, admin_required, model_not_found (404), unknown_endpoint (404, with a hint when the path suggests a missing /v1 prefix or an unsupported OpenAI endpoint), endpoint_not_supported (400, names the supported capability set), invalid_json, model_required, request_too_large (413), provider_unreachable (502).

Self-service (/my)

Endpoint Purpose
POST /my/keys Mint a key for your own principal (plaintext shown once)
GET /my/keys List own keys (metadata only)
DELETE /my/keys/{id} Delete own key (deletion is the revocation mechanism)
POST /my/relay-tokens Mint a transparent-relay token (lpt_, plaintext shown once; not an API key)
GET /my/relay-tokens List own relay tokens (metadata only)
DELETE /my/relay-tokens/{id} Delete own relay token (revocation)
GET /my/usage?since&until Own usage by model/endpoint with per-unit quantities and cost
GET /my/usage/series?bucket&since&until Own usage bucketed by hour/day/week/month, gap-filled, UTC

Team statistics (/stats, any authenticated user)

The proxy's usage is team-visible by design; the admin role gates configuration, not visibility. Every endpoint accepts the same filters: principal, key (an API key id), provider, model, client, tag, outcome and the since/until window; see usage. tag takes one exact key:value pair, is repeatable up to four times, and several pairs narrow together; a pair nothing carries simply matches nothing. outcome takes ok, upstream_error, unreachable, cancelled or the meta value failed (everything not ok); an unknown value is a 400.

Endpoint Purpose
GET /stats/series?bucket&since&until Bucketed usage across everyone
GET /stats/summary?since&until Usage aggregated per (principal, provider, model, endpoint, client, tags)
GET /stats/requests?limit&offset One page of the filtered request metadata log (never content), newest first; returns {requests, limit, offset, total}
GET /stats/requests/facets?since&until Distinct principals, keys, providers, models, clients and tags in the window, for the explorer's filter options
GET /stats/errors?bucket&since&until The errors dashboard in one call: a gap-filled series of counts per outcome, plus a breakdown per (provider, model, endpoint, client, tags, outcome, error_kind, status_code) with request count, average duration, last-seen and time-to-outcome bands (<1s, 1-5s, 5-15s, 15-30s, 30-60s, 60-120s, >=120s). Rows with outcome ok are included so error rates have their denominator.

Failures carry an error_kind classification token: the transport class on unreachable (timeout, connection_error, ...) and the upstream's error.type/error.code on upstream_error (e.g. rate_limit_error), sanitised to a short identifier. Provider error messages are never stored; they can echo request content.

Transparent Anthropic relay (/transparent/anthropic)

{ANY} /transparent/anthropic/{token}/{path...} forwards verbatim to the configured Anthropic base URL: method, path suffix, query, headers (minus hop-by-hop, Cookie, Accept-Encoding and x-llmproxy-tags) and body untouched, the caller's own x-api-key or OAuth bearer passed through. The relay token attributes usage to a principal and authenticates nothing else; API keys are rejected here and relay tokens are rejected everywhere else. Usage is read off the response (message_start/message_delta on streams, the usage object on unary bodies) and recorded under the sentinel provider transparent:anthropic; endpoints without usage still get a metadata-only event, except HEAD/OPTIONS probes, which relay unrecorded. Proxy-generated errors: 404 unknown_relay_token, 404 transparent_relay_disabled, 502 provider_unreachable. Docs: claude-code.

Admin (/admin/v1, admin role required, lists paginated)

Endpoint Purpose
POST/GET/PATCH/DELETE /providers[/{name}] Register, inspect, edit, unregister providers (upstream key encrypted at rest, never returned)
GET /providers/{name}/discover Upstream model listing; read-only, never auto-binds
POST/GET/PATCH/DELETE /models[/{alias}] Bind upstream models to globally unique aliases with capability sets and per-unit prices; a name can instead target another model and inherit its provider, capabilities and prices (one hop). Everything, including the name, is editable in place; a binding serves as soon as it exists (disable the provider to take it offline)
GET /resolve?model&endpoint&stream Dry-run alias resolution
POST/GET /principals Users and service principals
POST /principals/{id}/revoke-sessions Delete every browser session of a principal (API keys untouched)
POST/GET/DELETE /keys[/{id}] Keys for any principal
POST/GET /pricing Load/inspect the versioned pricing feed (bulk; per-model prices go through /models)
GET /usage/summary?since&until&principal Usage and cost by principal/model/endpoint/unit
GET /usage/series?bucket&since&until&principal Usage bucketed by hour/day/week/month across everyone
GET /requests?limit&offset The request metadata log with per-unit quantities (who, key, model, outcome, tokens; never content); same filters as /stats/requests
GET /events Metadata-only admin audit trail

Pagination

Every admin list endpoint (providers, models, keys, principals, events) accepts limit (1 to 500, default 100) and offset (default 0) and echoes both back:

curl -s "$P/admin/v1/models?provider=vllm-1&limit=50&offset=50" \
  -H "authorization: Bearer $ADMIN"

Out-of-range values fall back to the defaults. Ordering is by name or alias (keys by creation time, events newest first), so pagination is stable between requests.

Audit trail

Every admin mutation writes a metadata-only event in the same transaction as the change: provider.create|update|delete, model.create|update|delete, principal.create, key.create, key.delete, pricing.load.

curl -s "$P/admin/v1/events?limit=3" -H "authorization: Bearer $ADMIN"
{
  "events": [
    {
      "ts": "2026-07-29T09:20:01.334019Z",
      "actor_principal_id": "b1f4c2d8e96a4f0b8d3a5c7e9f012345",
      "action": "model.update",
      "target_kind": "model",
      "target_ref": "qwen-72b"
    }
  ],
  "limit": 3,
  "offset": 0
}

Events are newest first and carry identifiers only, never payloads. Direct CLI operations (llmproxy key create and friends) bypass the HTTP layer and are not audited.

Browser auth (/auth)

Endpoint Purpose
GET /auth/me Who am I: {authenticated, name, role, sso_enabled, password_enabled}; 200 even when anonymous so the login screen can render
POST /auth/password JSON {password} login for the local admin (from LLMPROXY_ADMIN_PASSWORD or the generated password file); works with or without SSO
GET /auth/login SSO only: set the signed state cookie, redirect to the IdP authorization endpoint
GET /auth/callback SSO only: verify state, exchange the code, fetch userinfo, apply group gate/role mapping, upsert the principal (keyed on sub), issue the session cookie
GET /auth/logout Delete the caller's server-side session row and clear the cookie; even a stolen copy of the cookie dies with it

Sessions carry the principal's role and authenticate /v1, /my, /stats and (admin role) /admin/v1. Non-GET session requests are origin-checked; API keys need no Origin.

Operational

Endpoint Purpose
GET / Built-in UI: a React app (source in ui/, Vite build committed to internal/server/uidist/ and embedded via go:embed; fonts and all assets served from the binary). It is a plain client of /auth, /my, /stats and /admin/v1. Sign-in via SSO and/or the admin password; self-service keys and a usage dashboard (requests, tokens and cost per hour/day/week/month) for everyone; providers (with upstream discovery and quick-bind), models with their prices and the request log for admins.
GET /healthz Liveness
GET /metrics Prometheus text: requests, duration, per-unit usage, unpriced volume. Aggregate labels only (provider, model, endpoint, unit, outcome).

Model-management compatibility endpoints

A compatibility surface for existing proxy management tooling (admin role required). The JSON field names below, litellm_params included, are the wire format such tooling sends and expects.

Endpoint Behaviour
POST /model/new Accepts a deployment payload (model_name, litellm_params.{model,api_base,api_key}, extra fields ignored). Creates or reuses a provider for api_base (name derived from the host; a supplied api_key becomes the provider credential), then binds model_name as a live alias. model_info.mode embedding/completion maps to capabilities; default is chat, chat_stream. Re-registering the same deployment is an idempotent 200. Because aliases are globally unique and there is no load-balancing pool, a second, different deployment under an existing model_name is rejected with 409.
GET /model/info Deployments as {data: [{model_name, litellm_params: {model, api_base, custom_llm_provider}, model_info: {id}}]}.
POST /model/delete {id} Deletes the binding by the model_info.id from /model/info.
POST /chat/completions, POST /completions, POST /embeddings, GET /models (root, no /v1) Aliases of the /v1 handlers, for clients configured without the prefix. /models is also a page of the built-in UI: a request whose Accept header lists text/html (a browser navigation) gets the app instead of JSON. API clients are unaffected, and /v1/models always returns JSON.