One API
Models plane
The models plane is the OpenAI-compatible surface: POST /v1/chat/completions, POST /v1/embeddings, GET /v1/models. Base URL https://api.plungeai.com. Auth is your ozk_… account key in Authorization: Bearer (only: X-API-Key is a 401 on this plane, except on /v1/messages, 3.16), billed to your account's wallet — see Authentication & keys. api.plungeai.com/v1/* is a transparent proxy (orchestration/api-gateway/routes/models-proxy.ts) to the inference gateway (inference/gateway/src/); the proxy adds no auth of its own — the inference gateway owns the key, wallet, and metering.
Examples in this chapter that carry
# verifyare run against the deployed API with anozk_key byverify-examples.mjs; the others are verified against the gateway source (inference/gateway/src).
3.1 Endpoints
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | /v1/chat/completions | JSON | Sync or stream:true SSE. Full field reference in 3.2. Handler routes/chat-completions.ts. |
| POST | /v1/messages | JSON | Anthropic Messages wire on the same money path; x-api-key or Bearer. Sync or stream:true SSE (3.16). Handler routes/messages.ts. |
| POST | /v1/completions | JSON | Legacy text completion: prompt is one string (arrays answer 400 unsupported_parameter); answered as text_completion, streamed or not. A chat request with one user message underneath — same routing, guardrails, cache and billing. Handler routes/completions.ts. |
| POST | /v1/embeddings | JSON | OpenAI, Mistral, Google and Cohere models (3.9). Handler routes/embeddings.ts. |
| GET | /v1/models | — | Aggregate catalog, OpenAI shape (3.8). Requires a key. Handler routes/models.ts. |
| GET | /v1/generation | — | Usage and cost of one completion (3.15). Handler routes/account.ts. |
| GET | /v1/credits | — | Lifetime credits bought and used (3.15). |
| GET | /v1/key · /v1/auth/key | — | The calling key: label, limit, usage, rate limit (3.15). |
| GET | /v1/models/{author}/{slug}/endpoints | — | The endpoint serving one model (3.15). |
3.2 Request fields
The support words below are: supported (mapped and sent), partial (only part works — the cell says which), ignored (accepted, no effect), rejected (errors), Ocean-only (a field OpenAI does not define).
Every request routes to exactly one provider shape. openai-shape providers: openai, groq, mistral, deepseek, xai, together, fireworks, cerebras, perplexity, openrouter, moonshot (routing/model-router.ts PROVIDERS). For openai-shape providers the gateway forwards your body after one field split (splitRequestFields, routing/params.ts): Ocean directives (models, sort, compress) and OpenRouter-only fields (provider, route, transforms, plugins, usage, reasoning, include_reasoning, debug, session_id) are removed before the provider sees them (reasoning.effort is sent as reasoning_effort). The sampling knobs top_k, min_p, top_a and repetition_penalty are forwarded to the OpenAI-compatible hosts only: OpenAI itself never receives them, nor any other field it does not define, while the other openai-shape providers keep provider-specific extras and top_k. Those names are returned in the x-ocean-dropped-params response header (3.6) — except min_p, top_a and repetition_penalty, which OpenRouter also defines and which are removed without a mention, like the other OpenRouter-only fields. For anthropic, google and cohere the gateway builds a fresh provider body from the same split; a field the shape cannot take is dropped (ignored). Those names are returned in the x-ocean-dropped-params response header (3.6) — OpenRouter-only fields (min_p, top_a, repetition_penalty among them) are again removed without a mention.
Core fields
| Field | openai-shape | anthropic | cohere | Source / note | |
|---|---|---|---|---|---|
model | supported | supported | supported | supported | provider/model slug or bare alias (3.7). Required unless models. |
models (array) | Ocean-only | Ocean-only | Ocean-only | Ocean-only | Ordered fallback list; stripped before an openai-shape upstream. routing/select.ts resolveCandidates (3.3). |
sort | Ocean-only | Ocean-only | Ocean-only | Ocean-only | price | latency | throughput; stripped before upstream. routing/select.ts sortCandidates. |
messages | supported | supported | supported | supported | Required; empty → 400 invalid_request_error "messages required". |
stream | supported | supported | supported | supported | SSE (Streaming). |
max_tokens | supported | supported | supported | supported | Defaults to 4096 on every shape when both token caps are omitted (DEFAULT_MAX_OUTPUT_TOKENS_ESTIMATE). For reasoning models (o1..o9, gpt-5*) it is folded into max_completion_tokens. |
max_completion_tokens | supported | supported | supported | supported | Preferred for reasoning models; on translated shapes it feeds the same output cap. |
temperature | supported | supported | supported | supported | Mapped on every shape. |
top_p | supported | supported (top_p) | supported (topP) | supported (p) | |
stop | supported | supported (stop_sequences) | supported (stopSequences) | supported (stop_sequences) | String or array. |
n | supported (passthrough) | ignored | ignored | ignored | Reserve is sized for n completions; only openai-shape returns multiple choices. |
stream_options | supported (passthrough) | ignored | ignored | ignored | Controls include_usage for openai-shape; translated shapes always emit a final usage (4). |
user | supported (passthrough) | supported | ignored | ignored | anthropic → metadata.user_id. |
Messages & content parts
| Field | openai-shape | anthropic | cohere | Source / note | |
|---|---|---|---|---|---|
content part text | supported | supported | supported | supported | |
content part image_url | supported (passthrough) | partial | supported | ignored | anthropic forwards the raw OpenAI part unconverted (toAnthropicBody passes non-text parts as-is) — real Anthropic expects {type:"image",source:…} and may 400. google: a data URI → inlineData, an http(s) URL → fileData. cohere extracts text parts only. |
content part file / input_audio | supported (passthrough) | partial | ignored | ignored | Forwarded raw to anthropic, dropped for google/cohere. |
name (on a message) | supported (passthrough) | ignored | ignored | ignored | Dropped by every translator. |
role:"tool" message | supported (passthrough) | supported (mapped) | supported (mapped) | ignored | anthropic: tool_result / tool_use blocks; the results of one call share one user turn. google: functionResponse / functionCall parts. cohere: text-only extraction. |
tool_call_id | supported (passthrough) | supported (mapped) | supported (mapped) | ignored | anthropic: tool_result / tool_use blocks; the results of one call share one user turn. google: functionResponse / functionCall parts. cohere: text-only extraction. |
tool_calls (assistant) | supported (passthrough) | supported (mapped) | supported (mapped) | ignored | anthropic: tool_result / tool_use blocks; the results of one call share one user turn. google: functionResponse / functionCall parts. cohere: text-only extraction. |
Tools & structured output
| Field | openai-shape | anthropic | cohere | Source / note | |
|---|---|---|---|---|---|
tools | supported (passthrough) | supported (mapped) | supported (mapped) | ignored | anthropic → {name,description,input_schema}; google → functionDeclarations; cohere drops tools (toCohereBody emits none). |
tool_choice | supported (passthrough) | supported | supported (mapped) | ignored | anthropic: auto, none, required→any, a named function→tool. google → toolConfig. |
parallel_tool_calls | supported (passthrough) | ignored | ignored | ignored | |
response_format | supported (passthrough) | supported | supported (mapped) | ignored | anthropic: json_object / json_schema through one forced synthetic tool; ignored when the request also sends tools. google → responseMimeType + responseSchema. Not mapped for cohere. |
A forced tool call on the Anthropic shape comes back as tool_calls with finish_reason:"tool_calls" and arguments {"city":"Berlin"} (live-verified 2026-10-04). The call needs about 33 completion tokens, above the 16-token cap of # verify fences, so it carries no marker:
curl -s "$API_BASE/v1/chat/completions" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"anthropic/claude-haiku-4-5","max_tokens":64,"messages":[{"role":"user","content":"Weather in Berlin?"}],"tools":[{"type":"function","function":{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}'A json_schema response on the Google shape. Gemini 2.5 counts its thinking tokens against max_tokens, so the call needs 512 and carries no # verify marker; message.content is the JSON string {"a":1}:
curl -s "$API_BASE/v1/chat/completions" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"google/gemini-2.5-flash","max_tokens":512,"messages":[{"role":"user","content":"Return a=1"}],"response_format":{"type":"json_schema","json_schema":{"name":"o","schema":{"type":"object","properties":{"a":{"type":"integer"}},"required":["a"]}}}}'Sampling & OpenRouter extensions
| Field | openai-shape | anthropic | cohere | Source / note | |
|---|---|---|---|---|---|
top_k | supported on openai-compatible hosts, ignored on OpenAI itself | supported | supported (mapped) | ignored | google → topK. |
presence_penalty, frequency_penalty | supported (passthrough) | ignored | supported (mapped) | ignored | google → presencePenalty / frequencyPenalty. |
repetition_penalty | supported on openai-compatible hosts, ignored on OpenAI itself | ignored | ignored | ignored | |
seed | supported (passthrough) | ignored | supported (mapped) | ignored | google → seed. |
logit_bias, logprobs, top_logprobs | supported (passthrough) | ignored | ignored | ignored | Forwarded verbatim on openai-shape; the upstream decides. |
min_p, top_a | supported on openai-compatible hosts, ignored on OpenAI itself | ignored | ignored | ignored | |
prediction, modalities, verbosity, web_search_options | supported (passthrough) | ignored | ignored | ignored | openai-shape passthrough only. |
reasoning, reasoning_effort | supported (passthrough) | ignored | ignored | ignored | Forwarded to reasoning-capable openai-shape providers. On the Agents plane reasoning_effort is a first-class field; here it is passthrough only. |
usage (request-level) | supported | supported | supported | supported | usage:{include:true} adds usage.cost to the response (3.5); Ocean always meters. |
provider | ignored | ignored | ignored | ignored | OpenRouter-only. Accepted and removed before the provider; use Ocean models / sort for routing. |
route | ignored | ignored | ignored | ignored | OpenRouter-only. Accepted and removed before the provider; use Ocean models / sort for routing. |
transforms, plugins | ignored | ignored | ignored | ignored | OpenRouter-only. Accepted and removed before the provider; use Ocean models / sort for routing. |
prompt (legacy) | rejected | rejected | rejected | rejected | Chat endpoint requires messages — a body without them is 400 "messages required". |
3.3 Routing: models, sort, failover
routing/select.ts and the runAttempts loop in routes/chat-completions.ts.
models[]— an ordered candidate list. Each entry is normalized (alias table +provider/modelshape); an unresolvable entry is dropped (recorded asreason:"unknown_model"), not fatal, as long as one survives. If every entry is unknown →404 model_not_found.sort— reorders candidates:price(cheapestinput+outputrate first, from the rate card),latency(lowest rolling p50, unknown providers last),throughput(highest rolling tok/s first). Nosortor<2candidates → input order preserved.- Attempt loop — one candidate at a time: up to 2 attempts per candidate (one retry on
429/5xx/network), then failover to the next. A non-4294xxis a caller error on that candidate — no retry, and the loop moves to the next candidate (a retired model id or a stale key on one provider does not sink the request); when no candidate succeeds, the first such error's status and body are returned, so a single-model request gets its own4xxunchanged. Exhausting every candidate →502"All routing candidates failed". - Outage exclusion — a candidate that exhausts its retries against a real upstream failure is marked out for 30s (KV
outage:<provider>); subsequent requests skip it (reason:"outage_excluded") until the window elapses. - Retry backoff — honors a small upstream
Retry-After(capped at 2s), else a flat 250ms. - Honest
model— the response'smodelis the slug that actually served the request (matters after failover), not the one you asked for, for anthropic/google/cohere and for the non-stream openai path. On the openai-shape stream the per-chunkmodelis left as the upstream reports it (not rewritten). The full ordered trail is persisted toinference_requests.route_attempts.
3.4 Presets
Set model to @preset/<slug> (policy/presets.ts). The gateway resolves a stored preset (org-specific row wins over a platform-global one) and merges it before candidate resolution. Precedence: your explicit request fields always win — preset params_json fills defaults, preset routing_json.models/sort apply only when you did not send models/sort; the decision log records @preset/<slug> as the requested model. Unknown or inactive slug → 404 preset_not_found. Presets are provisioned by an operator (signed /admin/presets); there is no public create route.
3.5 Response object
Non-stream body is OpenAI chat.completion; stream chunks are chat.completion.chunk (4).
| Field | Value |
|---|---|
id | chatcmpl-… on every shape (openai passthrough keeps the upstream id; translators mint one). |
object | chat.completion | chat.completion.chunk. |
created | Unix seconds (upstream's for openai passthrough; Date.now() for translated shapes). |
model | The served slug (see honest-model note in 3.3). |
provider | canonical provider id that served the request — openai, anthropic, …; every response and every stream chunk |
choices[].message / .delta | Standard OpenAI shape. |
choices[].finish_reason | See vocabulary below. |
choices[].native_finish_reason | the provider's own finish reason — end_turn, STOP, TOOL_CALL, …; on openai-shape providers it equals finish_reason |
usage | prompt_tokens, completion_tokens, total_tokens, plus prompt_tokens_details.cached_tokens and Ocean's extension prompt_tokens_details.cache_write_tokens when a cache read/write occurred. |
usage.cost, usage.is_byok, usage.cost_details | only with usage:{"include":true}: cost is the price charged in USD, cost_details.upstream_inference_cost the list cost at markup 1, split into upstream_inference_prompt_cost and upstream_inference_completions_cost |
finish_reason per shape (mapAnthropicStop / mapGoogleStop, streaming/sse-translate.ts):
| Upstream | openai | anthropic | cohere | |
|---|---|---|---|---|
| normal end | stop | end_turn,stop_sequence→stop | STOP→stop | always stop |
| length cap | length | max_tokens→length | MAX_TOKENS→length | stop |
| tool call | tool_calls | tool_use→tool_calls | a functionCall part → tool_calls | stop |
| content filter | content_filter | (mapped to stop) | SAFETY,RECITATION→content_filter | stop |
Usage & cache tokens — Anthropic's input_tokens (which excludes cache) is normalized to OpenAI semantics: prompt_tokens includes the cached portion, and prompt_tokens_details breaks it out. Cached tokens bill at cache rates and are subtracted from full-rate input, never double-counted (metering/pricing.ts computeCost).
3.6 Response headers
| Header | When | Meaning |
|---|---|---|
x-request-id | always | The inference gateway's request id — the same value as error.request_id and the id of your row in the request log. The client's own x-request-id is never trusted as an id. |
x-ocean-router-id | via api.plungeai.com | The router's own id for the call (its request-log row); quote it to support next to x-request-id. |
x-correlation-id | when you send x-request-id | Echoes your value. |
x-cache | cache-eligible + org cache on | hit (served from cache, $0) or miss (stored for next time) (3.11). |
x-ocean-cost-usd | non-stream chat completions | The settled price in USD, 6 decimals (0.000000 on a cache hit). Not sent on a stream: its price exists only after the last chunk. |
x-ocean-dropped-params | when the request carried fields the routed provider shape cannot take | comma-separated names that were removed (see 3.2); absent otherwise |
content-type | always | application/json or text/event-stream. |
The money-plane error body carries request_id inside the JSON envelope; note it does not set X-Error-Code (that header is execution-plane only — see Errors, limits & headers).
3.7 Providers & slugs
Address a model as provider/model (e.g. anthropic/claude-sonnet-4-6). Bare aliases in routing/model-router.ts ALIASES auto-prefix: gpt-4o, gpt-4o-mini, gpt-4.1, gpt-4.1-mini, o1, o1-mini, o3-mini, claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5, gemini-2.5-pro, gemini-2.5-flash. Any other bare name (no /) → 404 model_not_found.
| Provider id | Shape | Upstream |
|---|---|---|
openai | openai | api.openai.com |
anthropic | anthropic | api.anthropic.com/v1/messages |
google | generativelanguage.googleapis.com | |
groq, mistral, deepseek, xai, together, fireworks, cerebras, perplexity, moonshot | openai | each provider's OpenAI-compatible endpoint |
openrouter | openai | openrouter.ai/api/v1 |
cohere | cohere | api.cohere.com/v2/chat |
Discover the live catalog with GET /v1/models (3.8) or the discovery plane GET /v1/discovery/search?kind=models (Discovery). Not every provider slug in the map is priced/active — the catalog is the source of truth.
3.8 GET /v1/models
Requires a key. Returns { "object":"list", "data":[ … ] } built from the rate-card snapshot (routes/models.ts, metering/pricing.ts listCatalog). Each entry:
{
"id": "anthropic/claude-sonnet-4-6",
"object": "model",
"created": 0,
"owned_by": "anthropic",
"context_length": 200000,
"max_output": 8192,
"pricing": { "in": 3.0, "out": 15.0 },
"capabilities": { "streaming": true, "tools": true, "vision": true, "embedding": false, "fim": false },
"name": "Anthropic: claude-sonnet-4-6",
"canonical_slug": "anthropic/claude-sonnet-4-6",
"architecture": { "modality": "text+image->text", "input_modalities": ["text","image"], "output_modalities": ["text"], "tokenizer": null, "instruct_type": null },
"pricing_per_token": { "prompt": "0.000003", "completion": "0.000015", "input_cache_read": "0.0000003", "input_cache_write": "0.00000375" },
"top_provider": { "context_length": 200000, "max_completion_tokens": 8192, "is_moderated": false },
"supported_parameters": ["temperature", "top_p", "max_tokens", "max_completion_tokens", "stop", "tools", "tool_choice", "response_format", "top_k"],
"per_request_limits": null
}pricing.in/.out are USD per 1M tokens. pricing_per_token is the same price in OpenRouter's format (USD per token, decimal string); supported_parameters lists what the routed provider shape honours (tool parameters only for tool-capable models). vision:false means "not verified", not "verified absent"; max_output:null means unknown; fim is a name heuristic (codestral). This shape differs from OpenRouter's /models — see OpenRouter compatibility.
3.9 Embeddings
POST /v1/embeddings serves openai, mistral, google and cohere models (routes/embeddings.ts, streaming/embeddings-translate.ts); any other provider answers 400 unsupported_provider. Body is standard OpenAI (model, input string or array, optional encoding_format: float or base64); the answer is always OpenAI's {object:"list", data:[{embedding, index}], model, usage}, whatever the provider speaks. When a provider reports no token count (google) the usage is estimated as characters/4. An upstream failure passes through with its own status. BYOK, guardrails, reserve/settle metering, and trial exemption apply exactly as for chat. The price is the rate card's per-token input price; google/gemini-embedding-001 is served (live-verified 2026-10-04) but has no rate-card row yet, so GET /v1/models does not list it and no price is published for it.
3.10 Guardrails
Org- and key-scoped policies (policy/guardrails.ts), merged strictest-wins and enforced before any spend or upstream call. What a developer sees:
| Situation | Status | Code |
|---|---|---|
Model/provider not on the allow-list (or key allowed_models) | 403 | model_not_allowed |
| Prompt matches a blocked-content regex | 403 | content_blocked |
| Provider requires your own key on this plan and none is connected | 402 | byok_required |
| Org day/week/month spend cap reached | 429 | spend_cap_exceeded |
Spend caps count inference and connector spend plus outstanding wallet holds. On a models[] request the allow-list/require_byok filters narrow the candidate list per entry; only if none survive do you get the error above.
3.11 Response cache
Exact-match, per-org opt-in, default off (metering/response-cache.ts). Eligibility: stream:false and an explicit temperature:0 (an absent temperature defaults to 1 and is not eligible). The key is a SHA-256 of the entire request body scoped by org, so any differing field (tools, response_format, seed, …) is a different entry. A hit returns the stored body with x-cache: hit, bills $0 (no wallet touch, no upstream call); a miss on an eligible request sets x-cache: miss and stores the response (TTL clamped 60s–24h). A hit can never cross an org boundary.
3.12 BYOK-or-billed
Connect your own provider key (BYOK) in the dashboard; it is stored AES-256-GCM per org (routing/byok.ts, table inference_byok_keys on ocean-billing). When a BYOK key exists for the winning candidate's provider, the gateway uses it and bills only the org's flat byok_routing_fee (cost=0, price=byok_fee). A single-candidate BYOK request with a zero routing fee skips the wallet entirely (no reserve/settle/ledger row); a multi-candidate request always reserves (the winner may not be the zero-fee one). Guardrail require_byok can force BYOK for named providers → 402 byok_required if unmet.
3.13 Billing & metering
From inference/CHARGING-README.md. Every request is metered; only active accounts ever pay — an unknown/missing/stale status resolves to trial (metered, never debited).
| Status | Calls | Wallet | Monthly cap |
|---|---|---|---|
trial | yes | never debited | exempt; a per-org $10/mo platform-cost allowance applies (TRIAL_MONTHLY_CAP_USD), then 429 insufficient_quota |
active | yes | reserve→settle at true price | enforced |
suspended / closed | 401 at key resolution | — | — |
- Price = provider
cost × org markup(inference_org_settings), computed from the rate-card components (metering/pricing.ts); component-based, per-1M normalized. - Reserve-then-settle — a conservative max estimate is held before the upstream call and settled at the true price after (
metering/billing.ts). A failure releases the hold (no charge). A near-empty wallet →402 insufficient_balance. Anactivekey over itsmonthly_limit_usd→429 insufficient_quota. - RPM limit applies to everyone (abuse protection, not billing):
429 rate_limit_exceededatRATE_LIMIT_RPM_DEFAULT(600/min default), atomic per key (Errors, limits & headers).
Posted prices. A model with a posted Ocean price bills at that price whichever host serves it; x-ocean-pricing on every completion says posted, passthrough (cost × markup, above) or byok, and x-ocean-provider names the host. A posted price never exceeds the model maker's own list price; a published change reaches billing within 120 s; an aborted stream settles what streamed at the posted price. The public list: GET /v1/pricing (no key), live at https://plungeai.com/pricing/ (opens in a new tab).
3.14 Not supported today
- No
/v1/responses(Responses API),/v1/messages/count_tokensor Message Batches (the Anthropic Messages wire itself is 3.16),/v1/providers,/v1/images,/v1/audio,/v1/moderationsor/v1/rerank, and no/v1/embeddings/modelslist route. /v1/embeddingsserves openai, mistral, google and cohere models only (3.9); any other provider is400 unsupported_provider./v1/completionstakespromptas one string; an array is400 unsupported_parameter.usage.completion_tokens_details.reasoning_tokensis passed through only when an openai-shape provider sends it; the anthropic, google and cohere translations do not report it.- A request that times out is not answered with
408: a network failure fails over to the next candidate and ends as502when every candidate failed. - A
service_tierother thanautoordefaultis400 unsupported_service_tier(live-verified 2026-10-04): the rate card prices the default tier only. - The OpenRouter-only request fields
provider,route,transforms,plugins,include_reasoning,debugandsession_idare accepted and ignored: they are removed before the provider sees them, without a mention inx-ocean-dropped-params. Use Oceanmodels/sort/ presets for routing.min_p,top_aandrepetition_penaltyreach the OpenAI-compatible hosts only (never OpenAI itself, anthropic, google or cohere). - On anthropic,
n,seed,presence_penalty,frequency_penalty,logit_bias,logprobs/top_logprobs,parallel_tool_callsandreasoning_effortare dropped (the Messages API has no such fields) and named inx-ocean-dropped-params;response_formatis not enforced when the request also sendstools; images are forwarded without conversion (may400). - On google,
n,logit_bias,logprobs/top_logprobs,parallel_tool_callsandreasoning_effortare dropped and named inx-ocean-dropped-params, and so arefile/input_audiocontent parts (images are mapped). - On cohere, the translation is text-only:
tools,tool_choice,response_format,top_k,seed,presence_penalty/frequency_penaltyand images are dropped without a mention inx-ocean-dropped-params(Cohere tool calling is not built); the fields listed for google are dropped and named.
curl -s "$API_BASE/v1/chat/completions" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'The models plane takes the same ozk_ key as every other plane; a missing or invalid key is 401 invalid_api_key (a legacy sk-ocean- key is also accepted here). GET /v1/models enforces the same:
curl -s "$API_BASE/v1/models" -H "Authorization: Bearer $PLUNGE_API_KEY"3.15 Account & generation routes
Four read-only routes answer for the calling key's own account. They live on /v1/* (OpenAI shape, key required) and /api/v1/* (OpenRouter shape; /models/{author}/{slug}/endpoints is public there, like OpenRouter's).
| Route | Answers | Read from |
|---|---|---|
GET /generation?id=<id> | one completion's usage and cost: tokens_prompt, tokens_completion, native_tokens_cached, total_cost (what you were charged, USD), upstream_inference_cost, is_byok, generation_time, latency. id is the gen-… id of an /api/v1 response (also its x-generation-id header) or the x-request-id of any call. Another account's id answers 404 not_found. | inference_requests |
GET /credits | {"data":{"total_credits","total_usage"}} — lifetime USD bought and spent; zeros for an account with no wallet yet | inference_org_balance |
GET /key (alias /auth/key) | {"data":{"label","limit","usage","limit_remaining","is_free_tier","rate_limit":{"requests","interval"}}} — usage is this UTC month's spend on this key | inference_api_keys (legacy sk-ocean-), the key record (ozk_) |
GET /models/{author}/{slug}/endpoints | the one endpoint that serves a model, with per-token pricing | rate card |
A generation row is written after the response completes, so a lookup in the same second can answer 404 — retry after a moment.
curl -s "$API_BASE/api/v1/credits" -H "Authorization: Bearer $PLUNGE_API_KEY"The gen- id of an /api/v1 completion resolves through GET /v1/generation (live-verified 2026-10-04):
RID=$(curl -s "$API_BASE/api/v1/chat/completions" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":4}' \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
sleep 3
curl -s "$API_BASE/v1/generation?id=$RID" \
-H "Authorization: Bearer $PLUNGE_API_KEY"3.16 Anthropic Messages (POST /v1/messages)
The Anthropic Messages wire on the same money path as /v1/chat/completions (routing, models/sort failover, guardrails, reserve and settle, the x-ocean-* headers of 3.6). An Anthropic SDK or Claude Code points its base URL at the gateway and keeps its own request and response shapes. Handler routes/messages.ts; translation ingress/anthropic-in.ts / ingress/anthropic-out.ts. Also mounted at /api/v1/messages (OpenRouter's Anthropic skin, fallbacks up to 3).
- Auth:
x-api-key: ozk_…(with anyanthropic-version) orAuthorization: Bearer ozk_…. - Request:
model(any slug, not onlyanthropic/*),messages(string or blocks:text,image,tool_use,tool_result,thinking, …),system(string or blocks),max_tokens,stop_sequences,temperature,top_p,top_k,tools,tool_choice,metadata.user_id,thinking,stream; plus Ocean'smodels,sort,max_cost. anthropic/*models: your body is forwarded as sent, with onlymodel(the upstream id) andmax_tokens(the capped value, 3.2) set by the gateway —cache_control, thinking and server tools reach Anthropic unchanged.- Any other model: translated to the chat-completions shape —
system→ a system message,tool_use→tool_calls,tool_result→toolmessages,tools[].input_schema→function.parameters,tool_choiceany→required,stop_sequences→stop,metadata.user_id→user. Blocks without a neutral form (thinking, server tools) are not sent. - Response: an Anthropic
message(contenttext/tool_useblocks,stop_reason,usagewithinput_tokensexcluding cache reads/writes). Withstream:truethe SSE sequence ismessage_start,content_block_start/content_block_delta(text_delta,input_json_delta) /content_block_stop,message_delta(stop reason + usage),message_stop; a failure after the first byte ends in anerrorevent. - Errors:
{"type":"error","error":{"type":"<anthropic type>","message":"…"},"request_id":"…"}with the HTTP status of Errors —401authentication_error,402billing_error,429rate_limit_error,5xxapi_error,504timeout_error.
curl -s https://gateway.plungeai.com/v1/messages \
-H "x-api-key: $PLUNGE_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"openai/gpt-4o-mini","max_tokens":64,
"system":"Answer in one sentence.",
"messages":[{"role":"user","content":"What is a Durable Object?"}]}'Not yet live-verified (not deployed at time of writing; drill tests/live/inference/anthropic-ingress.sh). api.plungeai.com/v1/messages answers once the One API models proxy forwards the route.