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

# OpenRouter compatibility

> Point your OpenAI or OpenRouter client at https://api.plungeai.com/v1, swap the key for your ozk_… platform key, and POST /v1/chat/completions works with the same provider/model slugs and the same models[] fallback array — Ocean is an OpenAI-compatible router with OpenRouter-style …


Point your OpenAI or OpenRouter client at `https://api.plungeai.com/v1`, swap the key for your `ozk_…` platform key, and `POST /v1/chat/completions` works with the same `provider/model` slugs and the same `models[]` fallback array — Ocean is an OpenAI-compatible router with OpenRouter-style multi-provider addressing. What it is **not** is a drop-in for OpenRouter's cost-accounting fields, its error shape, or its provider-routing request object. This chapter is the developer-facing matrix; the exhaustive live/static diff (every field probed, request ids logged) is the full comparison log at `orchestration/api-gateway/live-test-reports/2026-09-18-openrouter-compat/OPENROUTER-COMPAT-LOG.md`.

## 5.0 Drop-in mode

Point an OpenRouter client at `https://api.plungeai.com/api/v1` and use your `ozk_` account key: `/api/v1/*` is the same models plane as `/v1/*` — same routing, guardrails, response cache, billing and metering — in OpenRouter's wire shape. Only the shape differs:

| | `/api/v1` (OpenRouter shape) | `/v1` (OpenAI shape) |
|---|---|---|
| Error envelope | `{"error":{"code":401,"message":"…","metadata":{"error_type":"authentication","ocean_code":"invalid_api_key","ocean_type":"authentication_error","request_id":"…"}}}` — numeric `code`, provider detail in `metadata` | `{"error":{"message":"…","type":"authentication_error","code":"invalid_api_key","request_id":"…"}}` |
| Completion `id` | `gen-…`, the generation id `GET /generation?id=` takes; also the `x-generation-id` header | `chatcmpl-…` |
| `system_fingerprint` | always present (`null` when the provider sent none) | as the provider sent it |
| Stream | `: OPENROUTER PROCESSING` keepalive until the first byte; the `usage` chunk is always sent and repeats `finish_reason` | `usage` chunk only with `stream_options.include_usage` |
| `GET /models` | public; OpenRouter's item keys, `pricing` = USD-per-token strings, plus `links` and `total_count` | needs a key; Ocean's item shape ([3.8](/one-api/models#38-get-v1models)) |

```bash
curl -s -X POST "$API_BASE/api/v1/chat/completions" -H "content-type: application/json" \
  -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
```

The drop-in base lists models without a key (live-verified 2026-10-04):

```bash
curl -s -o /dev/null -w '%{http_code}\n' "$API_BASE/api/v1/models"
```

## 5.1 Migration from OpenRouter

| Concern | OpenRouter | Ocean | Change needed |
|---|---|---|---|
| Base URL | `https://openrouter.ai/api/v1` | `https://api.plungeai.com/v1` | change base URL |
| Key | `sk-or-…` | `ozk_…` | new key ([Authentication & keys](/one-api/authentication)) |
| Auth header | `Authorization: Bearer` | `Authorization: Bearer` | none |
| Model slug | `provider/model` | `provider/model` | none (bare aliases also resolve, [3.7](/one-api/models#37-providers--slugs)) |
| Fallback list | `models: [...]` | `models: [...]` | none — same field, same semantics ([3.3](/one-api/models#33-routing-models-sort-failover)) |
| App attribution | `HTTP-Referer`, `X-Title` headers | ignored (no effect, no error) | drop them or leave them — they do nothing |
| Cost in response | `usage.cost`, `cost_details` | `usage.cost`, `cost_details`, `is_byok` with `usage:{"include":true}` ([3.5](/one-api/models#35-response-object)) | none |
| Provider routing | `provider:{...}` request object | not interpreted (see below) | use `sort` / `models` / [presets](/one-api/models#34-presets) |

## 5.2 Endpoints

| OpenRouter path | Ocean | Notes |
|---|---|---|
| `POST /chat/completions` | **same** | field caveats in [5.3](#53-request-fields) |
| `GET /models` | **same on `/api/v1`** · differs on `/v1` | public, OpenRouter's item shape under `/api/v1`; key-gated Ocean shape under `/v1` ([5.6](#56-models-list)) |
| `POST /embeddings` | **differs** | openai, mistral, google and cohere models; no `/embeddings/models` list route ([3.9](/one-api/models#39-embeddings)) |
| `POST /completions` (legacy text) | **same** | `prompt` must be one string; served on `/v1` and `/api/v1` ([3.1](/one-api/models#31-endpoints)) |
| `GET /generation` (cost lookup) | **same** | `GET /generation?id=<gen-… id>` ([3.15](/one-api/models#315-account--generation-routes)); traces stay at `GET /v1/traces/:id` ([Traces](/one-api/traces-observability)) |
| `GET /credits`, `/key` | **same** | ([3.15](/one-api/models#315-account--generation-routes)); `/auth/key` answers too. Key management (`/keys`, `POST /auth/keys`) stays in the dashboard |
| `GET /models/{author}/{slug}/endpoints` | **same** | public under `/api/v1` ([3.15](/one-api/models#315-account--generation-routes)) |
| `/images`, `/audio/*` | **missing** | not offered |
| `/v1/agents`, `/v1/workflows`, `/v1/tools`, `/v1/mcp`, `/v1/discovery`, `/v1/traces` | **Ocean-only** | the execution planes ([5.9](#59-ocean-only-capabilities-you-gain)) |

## 5.3 Request fields

Full per-provider-shape table is in [3.2](/one-api/models#32-request-fields). Relative to OpenRouter:

| Field | OpenRouter | Ocean |
|---|---|---|
| `model`, `models`, `messages`, `stream`, `stream_options`, `temperature`, `top_p`, `max_tokens`, `stop`, `tools`, `tool_choice`, `response_format`, `seed`, penalties, `logit_bias`, `logprobs`, `top_k`, `min_p`, `top_a`, `reasoning`, `reasoning_effort` | supported | **openai-shape: forwarded** after one field split (OpenAI itself never receives `top_k`, `min_p`, `top_a`; `reasoning.effort` is sent as `reasoning_effort`). On anthropic/google/cohere some are dropped in translation and named in `x-ocean-dropped-params` — see [3.2](/one-api/models#32-request-fields). |
| `sort` | not a request field | **Ocean-only** — `price`/`latency`/`throughput` |
| `provider` (routing object) | supported | **ignored** — accepted and removed before the provider; use `models` / `sort` |
| `route:"fallback"` | supported | **ignored** — accepted and removed; `models[]` always fails over in order |
| `transforms`, `plugins` | supported | **ignored** — accepted and removed before the provider |
| `prompt_cache_key`, `prompt_cache_options`, `cache_control` | supported | openai-shape passthrough; anthropic honours a `cache_control` marker on a system text part ([3.5](/one-api/models#35-response-object)), otherwise ignored |
| `usage:{include:true}` | opt-in cost accounting | **supported** — adds `usage.cost`, `cost_details`, `is_byok` ([3.5](/one-api/models#35-response-object)); Ocean meters either way |

## 5.4 Response fields

| Field | OpenRouter | Ocean |
|---|---|---|
| `id` | `gen-…` | `gen-…` under `/api/v1`; `chatcmpl-…` under `/v1` |
| `object`, `created`, `model`, `choices`, `usage` | present | present (`model` is the honest served slug, [3.3](/one-api/models#33-routing-models-sort-failover)) |
| `provider` (top-level) | present | present on every response and stream chunk |
| `openrouter_metadata`, `system_fingerprint`, `service_tier` | present | `openrouter_metadata`, `service_tier` **absent**; `system_fingerprint` always under `/api/v1` (`null` when the provider sent none), as the provider sent it under `/v1` |
| `choices[].native_finish_reason` | present | present on every choice |
| `usage.cost`, `usage.cost_details`, `usage.is_byok` | present | present with `usage:{"include":true}` ([3.5](/one-api/models#35-response-object)); the non-stream price is also the `x-ocean-cost-usd` header |
| `usage.prompt_tokens_details.cached_tokens` | present | present; Ocean also adds `cache_write_tokens` (Ocean-only) |
| error-in-choice (mid-stream) | present | matched on the agent plane (error frame, no `[DONE]`) |

## 5.5 Streaming

| Aspect | OpenRouter | Ocean |
|---|---|---|
| Wire | `chat.completion.chunk` + `[DONE]` | same ([4](/one-api/streaming)) |
| Keepalive comment | `: OPENROUTER PROCESSING` | **same on `/api/v1`** (until the first byte); none on `/v1`; the [agents plane](/one-api/streaming#43-agents-plane-openai-shaped-stream) sends `: OCEAN PROCESSING` |
| Final `usage` chunk | included (with `cost`) | **`/api/v1`: always**, repeating `finish_reason`; `/v1`: anthropic/google/cohere always, openai-shape only with `stream_options.include_usage` |
| Mid-stream error | error object, close without `[DONE]` | matched on the agents plane; money-plane openai passthrough relays the upstream's own bytes |

## 5.6 Models list

OpenRouter `GET /models` returns `data[].{id, canonical_slug, name, created, description, context_length, architecture, pricing:{prompt,completion}, top_provider, supported_parameters, …}` where `pricing` is **USD-per-token strings**. Ocean returns `data[].{id, object, created, owned_by, context_length, max_output, pricing:{in,out}, capabilities:{streaming,tools,vision,embedding,fim}}` where `pricing.in`/`.out` are **USD-per-1M numbers** ([3.8](/one-api/models#38-get-v1models)). `/v1/models` items also carry `name`, `canonical_slug`, `architecture`, `top_provider`, `supported_parameters`, `per_request_limits` and the per-token price as `pricing_per_token`; only `pricing` itself keeps the per-1M numbers.

Under `/api/v1/models` the list is OpenRouter's: public (no key), `data[]` items carry `id, canonical_slug, name, created, description, context_length, architecture, pricing` (USD-per-token strings), `top_provider, per_request_limits, supported_parameters, default_parameters, links, supported_voices`, and the envelope adds `links` and `total_count`.

## 5.7 Errors

OpenRouter's envelope is `{"error":{"message":"…","code":401,"metadata":{…}}}` — a **numeric** `code`, an optional `metadata`, no `type`. Ocean's money-plane envelope is `{"error":{"message":"…","type":"authentication_error","code":"invalid_api_key","request_id":"…"}}` — a **string** `code`, a `type`, a `request_id`, no `metadata`. The execution planes use a third shape (`{"error":{"code":"…","message":"…"}}` + `X-Error-Code` header). Full tables: [Errors, limits & headers](/one-api/errors-limits).

```bash
curl -s "$API_BASE/v1/chat/completions" -H "content-type: application/json" \
  -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'
```

The `"code"` above is the string `invalid_api_key`, not the integer `401` an OpenRouter SDK expects. Under `/api/v1` the same request answers `"code":401` with `metadata.error_type` in OpenRouter's vocabulary (`authentication`, `payment_required`, `rate_limit_exceeded`, …) and the Ocean code in `metadata.ocean_code` ([5.0](#50-drop-in-mode)).

## 5.8 What breaks an OpenRouter SDK user today

Every item below describes `/v1`. Under `/api/v1` ([5.0](#50-drop-in-mode)) the error envelope, `gen-` ids, keepalive, usage chunk and `/models` behave as OpenRouter's.

- **`error.code` is a string**, not a number — code that branches on `err.error.code === 401` never matches (compare `err.status` / the HTTP status instead).
- **Cost fields are opt-in** — `usage.cost`, `usage.cost_details`, `usage.is_byok` appear with `usage:{"include":true}` ([3.5](/one-api/models#35-response-object)); without it, read `x-ocean-cost-usd` or `GET /v1/generation` ([3.15](/one-api/models#315-account--generation-routes)).
- **No `openrouter_metadata`** in the response; `provider` and `native_finish_reason` are present.
- **OpenRouter-only request fields are not honoured** — `provider`, `route`, `transforms`, `plugins` are accepted and removed before the provider, with no effect. Replace with `models` + `sort` + [presets](/one-api/models#34-presets).
- **No `: OPENROUTER PROCESSING` keepalive** on the money-plane stream; openai-shape streams carry a final `usage` chunk only if you pass `stream_options.include_usage`.
- **`/models` shape differs** — needs a key, and `pricing` is per-1M numbers (the per-token strings are in `pricing_per_token`).
- **`HTTP-Referer` / `X-Title` are ignored** — no app rankings / attribution.
- **No `/images`, `/audio`, `/responses`, `/messages`, `/providers`** endpoints; `/generation`, `/credits`, `/key` ([3.15](/one-api/models#315-account--generation-routes)) and `/completions` ([3.1](/one-api/models#31-endpoints)) are served.
- **BYOK is dashboard-configured**, not a per-request `provider` object ([3.12](/one-api/models#312-byok-or-billed)).

## 5.9 Ocean-only capabilities you gain

- **Execution planes** an OpenRouter key cannot reach: [Tools](/one-api/tools), [Agents](/one-api/agents), [Workflows](/one-api/workflows), [MCP](/one-api/mcp), [Discovery](/one-api/discovery).
- **`sort`** — route candidates by `price`, `latency`, or `throughput` ([3.3](/one-api/models#33-routing-models-sort-failover)).
- **`@preset/<slug>`** — server-stored routing + param bundles ([3.4](/one-api/models#34-presets)).
- **Guardrails** — org/key spend caps, model/provider allow-lists, content filter, `require_byok` ([3.10](/one-api/models#310-guardrails)).
- **Exact-match response cache** — `$0` hits with an `x-cache` header ([3.11](/one-api/models#311-response-cache)).
- **Traces & honest billing** — `GET /v1/traces/:id`, honest served-model reporting after failover ([Traces](/one-api/traces-observability)).
