One API — Developer Guide (v3.0)
Contents
0. Overview 1. Quickstart 2. Authentication & keys 3. Models plane 4. Streaming 5. OpenRouter compatibility 6. Tools plane 7. Connectors & connected accounts 8. Discovery 9. Agents plane 10. Workflows plane 11. MCP plane 12. Traces & observability 13. Errors, limits & headers 14. Skills, plugins & capability injection 15. SDKs & clientsThe single front door to the Ocean Platform:
https://api.plungeai.com. One endpoint for models, tools, agents, workflows, MCP, connectors, discovery, and traces. Machine-readable contract:GET /v1/openapi.json. Every# verify-marked example in this guide was run against the live platform on the build date (2026-09-18). Previous version:ONE-API-DEVELOPER-GUIDE-2.0.md(v2.0, kept intact).
Contents
- 0. Overview
- 1. Quickstart
- 2. Authentication & keys
- 3. Models plane
- 4. Streaming
- 5. OpenRouter compatibility
- 6. Tools plane
- 7. Connectors & connected accounts
- 8. Discovery
- 9. Agents plane
- 10. Workflows plane
- 11. MCP plane
- 12. Traces & observability
- 13. Errors, limits & headers
- 14. Skills, plugins & capability injection
- 15. SDKs & clients
0. Overview
The Ocean One API is a single HTTPS front door to the Ocean platform. One base URL, one router, and every capability the platform exposes reachable from it:
https://api.plungeai.com
Everything in this guide is a real route on that host. The router is a thin Hono worker
(orchestration/api-gateway/api-gateway.ts) that fronts the registry, the CNL engine, the
MCP gateway, the MCP executor, and the inference gateway over internal service bindings —
you never address those services directly.
The seven planes
The router groups routes into planes. Six are the execution planes and take an ozk_
platform key; the models plane is OpenAI-compatible and takes an sk-ocean- key.
| Plane | What it does | Key routes | Key |
|---|---|---|---|
| Models | OpenAI-compatible chat, embeddings, model catalog — a transparent proxy to the inference gateway with ordered fallback, sort, @preset/<slug>, guardrails, and an opt-in response cache |
POST /v1/chat/completions · POST /v1/embeddings · GET /v1/models |
sk-ocean- |
| Tools | List tool agents, read a tool's invocation contract, execute one operation behind a trust fence | GET /v1/tools · GET /v1/tools/{id} · POST /v1/tools/{id}/execute |
ozk_ |
| Agents | Execute a registry agent from a prompt — sync, async pointer, or OpenAI-shaped SSE | GET /v1/agents · POST /v1/agents/{id}/execute · GET /v1/agents/results/{workflowId}/{taskId} |
ozk_ |
| Workflows | Run a multi-agent CNL workflow, inline or saved, with optional SSE | POST /v1/workflows/execute · POST /v1/workflows/execute-stream · POST /v1/workflows/{id}/execute |
ozk_ |
| MCP | The platform as an MCP server (inbound Streamable HTTP), plus outbound MCP runs against catalog servers | POST /v1/mcp · GET /v1/mcp/tools · POST /v1/mcp/runs |
ozk_ |
| Discovery | Search and recommend across the capability registry (agents, skills, connectors, models, MCP servers, …) | GET /v1/discovery/search · POST /v1/discovery/recommend · GET /v1/discovery/cards/{type}/{id} |
ozk_ |
| Traces | Read the persisted execution trace (spans + gateway request log) for one trace id | GET /v1/traces/{id} |
ozk_ |
The full machine-readable route list and schemas are served at GET /v1/openapi.json
(source: orchestration/api-gateway/openapi.ts). There is also a signed, internal-only
capability plane (/v1/capability, OceanCode RemotePacks) that customer keys cannot open —
it is not one of the seven and is not documented here.
Positioning
Think of the One API as an OpenRouter-compatible models plane plus everything a model router cannot do. The models plane speaks the OpenAI wire format, so an existing OpenAI or OpenRouter integration points at it with a base-URL swap (see OpenRouter compatibility). The execution planes are the part a router has no answer for: real tools with contracts and a trust fence, registry agents, multi-agent workflows, discovery, MCP, and a persisted trace for every run.
Two kinds of key
| Prefix | Opens | How you get it |
|---|---|---|
ozk_ |
The six execution planes (tools, agents, workflows, MCP, discovery, traces) | Self-service at Dashboard → API Keys (https://dashboard.plungeai.com); the key is bound to your account and tier |
sk-ocean- |
The models plane only (/v1/chat/completions, /v1/embeddings, /v1/models) |
Self-service at Dashboard → Gateway → Inference → Keys (https://dashboard.plungeai.com) |
A third prefix, sk-conn-, authenticates the connector proxy (a separate host,
gateway.plungeai.com/connect/...) and is covered in Connectors.
Sending the wrong prefix to a plane is a 401 — see Authentication & keys.
Full key mechanics are in that chapter; this guide writes keys as the shell variables
$PLUNGE_API_KEY (ozk_), $PLUNGE_MODEL_KEY (sk-ocean-), and $PLUNGE_CONNECTOR_KEY
(sk-conn-).
How a request flows
An execution-plane call is not a thin proxy to a model. The path is router → CNL engine → agent:
- The router authenticates the
ozk_key, mints a server-sidex-request-id, and either adopts your UUIDx-trace-idor generates the trace id (api-gateway.ts). - It dispatches over an internal service binding to the CNL engine, which loads the agent
card from the registry, refuses anything not
status:activebefore dispatch, recalls memory, and runs the agent. - The agent's result is stored in SharedMemory and returned; the whole run is persisted as trace spans.
The engine hop exists on purpose: it gives every call the same history, outcome vocabulary
(ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error), and trust fence, whether the caller is a workflow, Ocean Studio, or you over HTTP.
The rationale is recorded in orchestration/api-gateway/log/API-SURFACE-DECISION-AND-PLAN.md
(everything routes through the engine so there is one execution path, not two).
What "live-verified" means in this guide
Every ozk_-plane example in this guide was run against the deployed API on
2026-09-18 and its output checked. Those examples carry a hidden # verify /
# expect: marker that the docs build strips but a verifier
(orchestration/api-gateway/docs/guide-3.0/verify-examples.mjs) re-runs. Models-plane
(sk-ocean-) examples are verified against code and the live-verified 2.0 guide, not
re-run live in this pass (a models-plane key was unavailable 2026-09-18) — that is stated
again in the Quickstart.
60-second tour
Set your key once, then hit three planes. These three are live-verified:
curl -s https://api.plungeai.com/health
curl -s "https://api.plungeai.com/v1/discovery/search?q=web+search&kind=agents&limit=3" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
curl -s "https://api.plungeai.com/v1/agents?limit=3" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
For a first tool call, agent call, workflow, and model completion, go straight to the Quickstart.
What's new in 3.0
- Full field-level models reference — every
/v1/chat/completionsfield with its support status (Models plane). - OpenRouter compatibility matrix — endpoint and field parity, same / differs / missing / Ocean-only (OpenRouter compatibility).
- Connectors chapter — OAuth vs API-key connectors, connected accounts, the
sk-conn-connector proxy (Connectors). - Agent tiers — tool agents,
llm-agent,harness-agent,concierge-agent,claude-managed-agent(Agents plane). - Capability injection — skills, experts, persona, backgrounds, plugins, and MCP on harness missions (Skills, plugins & capability injection).
- SDK matrix — OpenAI SDKs,
@plungeai/one-api, LiteLLM, Vercel AI SDK, LangChain, and MCP clients (SDKs & clients). - Website copy — this guide is also published at
https://plungeai.com/docs.
Where to get help
- Docs:
https://api.plungeai.com/docs(this guide) ·GET /llms.txtandGET /llms-full.txtfor LLM tooling ·GET /v1/openapi.jsonfor the contract. - Keys and support:
support@plungeai.com. - Every response carries an
x-request-id— include it when you report a problem (Traces & observability).
1. Quickstart
Five first calls, one per plane you will use most. The ozk_ examples here are
live-verified (2026-09-18). The models-plane (sk-ocean-) examples are verified against
code and the live-verified 2.0 guide, not re-run live in this pass — a models-plane key
was unavailable on 2026-09-18. Get an sk-ocean- key from Dashboard → Gateway → Inference → Keys and an
ozk_ key from Dashboard → API Keys (see Authentication & keys).
Throughout, $PLUNGE_API_KEY is your ozk_ key and $PLUNGE_MODEL_KEY is your
sk-ocean- key.
(a) A model completion with the OpenAI SDK
The models plane is OpenAI-compatible. Point the official OpenAI SDK at
https://api.plungeai.com/v1 and pass a provider/model slug. Valid slugs come from
GET /v1/models and the router's alias table (inference/gateway/src/routing/model-router.ts);
anthropic/claude-sonnet-4-6 is a stable one.
Python:
from openai import OpenAI
client = OpenAI(
api_key="<your sk-ocean- key>",
base_url="https://api.plungeai.com/v1",
)
resp = client.chat.completions.create(
model="anthropic/claude-sonnet-4-6",
messages=[{"role": "user", "content": "Reply with exactly the word: pong"}],
max_tokens=16,
)
print(resp.choices[0].message.content)
TypeScript:
import OpenAI from 'openai'
const client = new OpenAI({
apiKey: process.env.PLUNGE_MODEL_KEY,
baseURL: 'https://api.plungeai.com/v1',
})
const resp = await client.chat.completions.create({
model: 'anthropic/claude-sonnet-4-6',
messages: [{ role: 'user', content: 'Reply with exactly the word: pong' }],
max_tokens: 16,
})
console.log(resp.choices[0].message.content)
curl:
curl -s https://api.plungeai.com/v1/chat/completions \
-H "Authorization: Bearer $PLUNGE_MODEL_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"anthropic/claude-sonnet-4-6","messages":[{"role":"user","content":"Reply with exactly the word: pong"}],"max_tokens":16}'
The response model field is the slug that actually served the request — after a
failover it can differ from what you asked for. Add "stream": true for an SSE response.
Ordered fallback (models[]), sort, @preset/<slug>, guardrails, and the response cache
are covered in Models plane.
(b) A tool call — read the contract first, then execute
A tool agent has an invocation contract: operations, params, gates, examples. Read it before you call. This is live-verified:
curl -s https://api.plungeai.com/v1/tools/brave-agent \
-H "Authorization: Bearer $PLUNGE_API_KEY"
Then execute an operation. brave-agent takes a free-text prompt (a search query, ≤ 400
chars). Live-verified:
curl -s -X POST https://api.plungeai.com/v1/tools/brave-agent/execute \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"cloudflare workers pricing"}'
The result carries ok, content, and an outcome. A 403 refused or 409 approval_required is the trust fence on a gated or money verb; a 422 invalid_params
echoes the contract back with what was missing. See Tools plane.
(c) An agent call — sync, then streamed
POST /v1/agents/{id}/execute runs a registry agent from a prompt. Default is synchronous
(the resolved result comes back on the same response). llm-agent is a general LLM agent.
Live-verified:
curl -s -X POST https://api.plungeai.com/v1/agents/llm-agent/execute \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"Reply with exactly the word: pong","sync":true,"max_tokens":32}'
Add "stream": true for an OpenAI-shaped chat.completion.chunk SSE — a primer chunk, one
delta per token, a final finish_reason chunk, then data: [DONE]. Live-verified:
curl -s -N -X POST https://api.plungeai.com/v1/agents/llm-agent/execute \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"Reply with exactly the word: pong","stream":true,"max_tokens":32}'
Set "sync": false for a 202 pointer you redeem at
GET /v1/agents/results/{workflowId}/{taskId}. Full field list, tiers, and error codes are
in Agents plane; the SSE vocabulary is in Streaming.
(d) A workflow — inline CNL YAML
POST /v1/workflows/execute runs a multi-agent workflow. Send the CNL YAML as a workflow
string (or POST raw YAML with Content-Type: text/yaml). The response is an ack with a
workflow_id; redeem each task's result from SharedMemory at
GET /v1/workflows/results/{workflowId}/{taskId}. This fence executes and then polls the
result — live-verified:
ack=$(curl -s -X POST https://api.plungeai.com/v1/workflows/execute \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"workflow":"workflow:\n name: quickstart\n tasks:\n - id: t1\n agent: llm-agent\n prompt: \"Reply with exactly the word: pong\"\n max_tokens: 32\n"}')
echo "$ack"
wf=$(printf '%s' "$ack" | sed -n 's/.*"workflow_id":"\([^"]*\)".*/\1/p')
out=""
for i in $(seq 1 10); do
out=$(curl -s "https://api.plungeai.com/v1/workflows/results/$wf/t1" \
-H "Authorization: Bearer $PLUNGE_API_KEY")
printf '%s' "$out" | grep -q '"content"' && break
done
echo "$out"
Use POST /v1/workflows/execute-stream for live SSE events instead of polling. See
Workflows plane and the CNL primer there.
(e) Discover a capability
GET /v1/discovery/search is a hybrid semantic + keyword search over the registry. Describe
the capability in natural language; filter by kind. Live-verified:
curl -s "https://api.plungeai.com/v1/discovery/search?q=convert+pdf+to+markdown&kind=agents&limit=3" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
Read a single card's markdown at GET /v1/discovery/cards/{type}/{id}. See
Discovery.
Request ids and traces
Every response echoes a server-minted x-request-id. To correlate several calls under one
trace, send your own x-trace-id — it must be a UUID or it is ignored. Read the persisted
trace at GET /v1/traces/{id} (two sentences here; full detail in
Traces & observability):
curl -s "https://api.plungeai.com/v1/traces/$(uuidgen)" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
2. Authentication & keys
Every route except GET /health and GET /v1/openapi.json needs a key. There are three key
prefixes, and each opens a different set of planes. Sending the wrong prefix to a plane is a
401, not a silent fallback.
The three prefixes
| Prefix | Name | Opens | Auth checked by |
|---|---|---|---|
ozk_ |
Ocean Zero Key | The six execution planes: tools, agents, workflows, MCP, discovery, traces | orchestration/api-gateway/lib/auth.ts (router) |
sk-ocean- |
Inference key | The models plane only: /v1/chat/completions, /v1/embeddings, /v1/models |
inference/gateway/src/auth/api-key.ts (downstream) |
sk-conn- |
Connector key | The connector proxy on gateway.plungeai.com (/connect/:provider/proxy/*) |
inference/gateway/src/routes/connector-proxy.ts |
In this guide: $PLUNGE_API_KEY = ozk_, $PLUNGE_MODEL_KEY = sk-ocean-,
$PLUNGE_CONNECTOR_KEY = sk-conn-.
Which routes accept which key
The router (api-gateway.ts) gates /v1/discovery, /v1/tools, /v1/agents,
/v1/workflows, /v1/mcp, and /v1/traces with requireAuth, which accepts only a
bearer that starts with ozk_ (or a signed internal identity). The money plane is mounted
without requireAuth — the inference gateway owns sk-ocean- auth downstream. So the
two obvious mistakes both return 401:
An ozk_ key on the models plane → 401 (live-verified):
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://api.plungeai.com/v1/chat/completions \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"anthropic/claude-haiku-4-5","messages":[{"role":"user","content":"hi"}],"max_tokens":1}'
A models-plane key shape on an execution plane → 401 (the router rejects any non-ozk_
bearer before dispatch; live-verified):
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://api.plungeai.com/v1/agents/llm-agent/execute \
-H "Authorization: Bearer sk-ocean-EXAMPLE" \
-H "Content-Type: application/json" \
-d '{"prompt":"hi"}'
The 401 body is the standard error envelope with X-Error-Code: unauthorized
(live-verified):
curl -s https://api.plungeai.com/v1/tools
Header forms
Both header forms work on the execution planes; the models plane takes Authorization
only.
| Header | Planes | Example |
|---|---|---|
Authorization: Bearer <key> |
all | -H "Authorization: Bearer $PLUNGE_API_KEY" |
X-API-Key: <key> |
execution planes (ozk_) |
-H "X-API-Key: $PLUNGE_API_KEY" |
X-API-Key with an ozk_ key is live-verified:
curl -s "https://api.plungeai.com/v1/tools?limit=1" \
-H "X-API-Key: $PLUNGE_API_KEY"
How to obtain each key
| Key | Where | Notes |
|---|---|---|
ozk_ |
Dashboard → API Keys (https://dashboard.plungeai.com) |
Self-service; session-authenticated (apps/ocean-dashboard/src/worker/routes/platform-keys.routes.ts, POST /api/platform-keys). Name the key, pick an expiry (never / 7–365 days), copy it once — it is shown exactly once and stored hashed. Bound to your account and its tier; revoke any time from the same list. |
sk-ocean- |
Dashboard → Gateway → Inference → Keys (https://dashboard.plungeai.com) |
Self-service; session-authenticated (apps/ocean-dashboard/src/worker/routes/inference.routes.ts, POST /api/inference/keys) |
sk-conn- |
Dashboard → Gateway → Connector → Keys | Self-service connector key for the connector proxy; see Connectors |
Key fences (scoping)
Keys can be scoped so a leak is bounded.
| Fence | Key kind | Effect | Source |
|---|---|---|---|
allowed_tools |
ozk_ |
Restrict the key to a named set of tools | orchestration/mcp-gateway/auth-middleware.ts |
allowed_ips |
ozk_ |
Accept the key only from listed IPv4 addresses / CIDR blocks or exact IPv6 addresses; a call from elsewhere is a definitive 401 (no fallback) |
orchestration/mcp-gateway/auth-middleware.ts (ipAllowed) |
allowed_models |
sk-ocean- |
Restrict the key to a model allow-list; an off-list model is 403 model_not_allowed |
inference/gateway/src/auth/api-key.ts (assertModelAllowed) |
monthly_limit_usd |
sk-ocean- / sk-conn- |
Per-key monthly spend cap; exceeding it is 429 insufficient_quota (inference) or 402 insufficient_balance (connector) |
inference/gateway/src/auth/rate-limit.ts |
ozk_ keys are stored hashed only (key:{sha256} in KV) and shown once at mint time — the
plaintext is never at rest (orchestration/mcp-gateway/auth-middleware.ts).
Tiers and rate limits
Execution-plane rate limiting is per-key (per user uuid), enforced by a Durable Object on
POST executions only — catalog GETs are free (api-gateway.ts calls checkRateLimit
for POST only). The limits below are the code values in
orchestration/mcp-gateway/rate-limiter.ts:
| Tier | Requests / minute | Requests / day |
|---|---|---|
| free | 30 | 1,000 |
| pro (default) | 100 | 10,000 |
| enterprise | 300 | 100,000 |
An unset tier defaults to pro. A per-key rate_limit override replaces the per-minute cap.
The models plane has its own limiter — a default of 600 req/min per key
(RATE_LIMIT_RPM_DEFAULT, inference/gateway/src/routes/chat-completions.ts), also
overridable per key.
A separate failed-auth limiter meters bad credentials at 20 attempts / minute per
client IP (auth-fail tier in rate-limiter.ts; wired in auth.ts) so key enumeration
is bounded.
When you are limited, the response is 429 with a Retry-After header (seconds) and a
rate_limited (execution planes) or rate_limit_exceeded (models plane) error code.
Request ids and trace correlation
x-request-idis minted server-side on every request and echoed on the response (api-gateway.ts). It is never trusted from the client. Include it when reporting a problem. Live-verified — the header is present:
curl -sD - -o /dev/null https://api.plungeai.com/health | grep -i '^x-request-id'
x-trace-idis yours to set for correlating several calls into one trace. It is honoured only if it is a UUID; otherwise the router falls back to the request id. On an execution route the trace id doubles as the execution/workflow primary key, so replaying a UUID that was already used returns409 duplicate_execution_id— send a fresh UUID or omit the header.
Responses also set X-Content-Type-Options: nosniff. Error responses additionally set
X-Error-Code (equal to error.code) so a client can branch on the header regardless of the
negotiated body format.
CORS
The router enables permissive CORS (cors() in api-gateway.ts), and the models plane sets
access-control-allow-origin: * with authorization, content-type, x-request-id allowed and
x-request-id, x-correlation-id exposed (inference/gateway/src/index.ts). Browser SDKs
work, but do not ship a secret key in client-side code.
Internal HMAC identity (not for customers)
Service-to-service calls inside the platform authenticate with a signed internal identity
(X-Internal-Signature, HMAC over the request) instead of a key, verified before the ozk_
path in auth.ts. This is the mechanism Ocean Studio and the engine use to call each other;
it is not available to external developers and needs no action from you.
3. 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 an sk-ocean-… model key in Authorization: Bearer — 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. gateway.plungeai.com/v1/* reaches the same code directly.
Authenticated models-plane examples in this chapter are verified against the gateway source (
inference/gateway/src), not re-run live on 2026-09-18 (nosk-ocean-key was mintable this session). The negative auth checks below carry# verifyand were run live.
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/embeddings |
JSON | OpenAI provider only in v1 (3.9). Handler routes/embeddings.ts. |
| GET | /v1/models |
— | Aggregate catalog, OpenAI shape (3.8). Requires a key. Handler routes/models.ts. |
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. For openai-shape providers the gateway forwards your body verbatim (callUpstream in routes/chat-completions.ts spreads {...body}, deleting only models and sort) — the upstream provider, not Ocean, interprets each field, so anything the OpenAI/OpenRouter wire accepts passes through. openai-shape providers: openai, groq, mistral, deepseek, xai, together, fireworks, cerebras, perplexity, openrouter, moonshot (routing/model-router.ts PROVIDERS). For anthropic, google, and cohere the gateway builds a fresh provider body (toAnthropicBody / toGoogleBody / toCohereBody in streaming/sse-translate.ts); a field that is not explicitly mapped there is dropped (ignored).
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) | ignored | ignored | ignored |
Messages & content parts
| Field | openai-shape | anthropic | cohere | Source / note | |
|---|---|---|---|---|---|
content part text |
supported | supported | supported | supported | |
content part image_url |
supported (passthrough) | partial | ignored | 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/cohere extract text parts only. |
content part file / input_audio |
supported (passthrough) | partial | ignored | ignored | Same as image_url: 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) | ignored | ignored | ignored | Dropped for anthropic (if (m.role==='tool') continue); text-only extraction elsewhere. |
tool_call_id |
supported (passthrough) | ignored | ignored | ignored | Only meaningful with a passed-through tool loop. |
tool_calls (assistant) |
supported (passthrough) | ignored | ignored | ignored | Translators map assistant text, not the tool_calls array. |
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) | ignored | ignored | ignored | Not mapped by any translator. |
parallel_tool_calls |
supported (passthrough) | ignored | ignored | ignored | |
response_format |
supported (passthrough) | ignored | ignored | ignored | Not mapped for anthropic/google/cohere — JSON-mode / json-schema works on openai-shape only. |
Sampling & OpenRouter extensions
| Field | openai-shape | anthropic | cohere | Source / note | |
|---|---|---|---|---|---|
top_k |
supported (passthrough) | ignored | ignored | ignored | Not mapped by translators (even though anthropic/google support it natively). |
presence_penalty, frequency_penalty, repetition_penalty |
supported (passthrough) | ignored | ignored | ignored | |
seed, logit_bias, logprobs, top_logprobs, min_p, top_a |
supported (passthrough) | ignored | ignored | ignored | Forwarded verbatim on openai-shape; the upstream decides. |
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 (passthrough) | ignored | ignored | ignored | OpenRouter's usage-accounting toggle. Ocean always meters; the field itself just rides through. |
provider |
partial | ignored | ignored | ignored | OpenRouter provider-routing object. Ocean does not strip it — it is forwarded verbatim, so real OpenAI/most providers 400 on the unknown key; only openrouter/* accepts it. Use Ocean models/sort instead. |
route |
partial | ignored | ignored | ignored | OpenRouter route:"fallback". Forwarded verbatim to openai-shape (most providers 400). Ocean's equivalent is the models array. |
transforms, plugins |
partial | ignored | ignored | ignored | OpenRouter-only. Forwarded verbatim to openai-shape; no Ocean handling; most non-OpenRouter upstreams 400. |
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 — it aborts the whole loop immediately (no retry, no failover), and its status/body are returned. 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). |
choices[].message / .delta |
Standard OpenAI shape. |
choices[].finish_reason |
See vocabulary below. |
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. |
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 |
(n/a) | 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 | Server-minted request id (correlation/trace). The client's own x-request-id is never trusted as an 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). |
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 }
}
pricing.in/.out are USD per 1M tokens. 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 is OpenAI-only in v1 (routes/embeddings.ts): a non-openai/* model → 400 invalid_request_error "Only OpenAI embeddings supported in v1". Body is standard OpenAI (model, input string or array, optional encoding_format); the response is passed through verbatim, including the upstream status on failure. BYOK, guardrails, reserve/settle metering, and trial exemption apply exactly as for chat.
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).
3.14 Not supported today
- No
response_format,tool_choice,top_k, penalties,seed, orlogit_biason anthropic/google/cohere (dropped in translation — openai-shape only). - No tool calls on cohere (tools dropped).
- Images/files on google/cohere (text-only); on anthropic forwarded raw without conversion (may 400).
- No
/v1/completions(legacy text-completion), no/v1/embeddingsfor non-OpenAI providers, no/v1/images,/v1/audio,/v1/moderations,/v1/responses. - OpenRouter-only request fields (
provider,route,transforms,plugins) are not interpreted — they ride through to openai-shape upstreams (which usually 400). Use Oceanmodels/sort/presets instead.
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 requires an sk-ocean- key; the ozk_ execution-plane key above is rejected with invalid_api_key. GET /v1/models enforces the same:
curl -s "$API_BASE/v1/models" -H "Authorization: Bearer $PLUNGE_API_KEY"
4. Streaming
Three surfaces stream over Server-Sent Events (SSE): the models plane (/v1/chat/completions with stream:true), the workflows plane (/v1/workflows/execute-stream), and the agents plane (/v1/agents/:id/execute with stream:true). The models plane speaks the OpenAI delta wire; the workflows plane speaks the CNL engine's named-event wire; the agents plane re-shapes an engine run into the OpenAI delta wire. All three send Content-Type: text/event-stream and frame events as …\n\n.
4.1 Models-plane SSE
Code: routes/chat-completions.ts (stream branch) + streaming/sse-translate.ts + streaming/usage-meter.ts.
Wire format. Each event is one data: <json>\n\n line whose payload is a chat.completion.chunk, terminated by a literal data: [DONE]\n\n. The gateway injects no comment lines on this plane.
Role primer. Differs by provider shape:
| Shape | First chunk | How |
|---|---|---|
| openai-shape (openai, groq, mistral, deepseek, xai, together, fireworks, cerebras, perplexity, openrouter, moonshot) | whatever the upstream sends (typically delta:{role:"assistant"}) |
raw upstream SSE bytes are piped verbatim — Ocean does not add or rewrite chunks. |
| anthropic | delta:{role:"assistant"} injected by Ocean |
translateAnthropic emits a primer in start(). |
| google, cohere | delta:{role:"assistant"} injected by Ocean |
emitted on the first upstream frame. |
Final usage chunk.
| Shape | Final usage present? |
|---|---|
| anthropic, google, cohere | always — the translator emits a final chunk carrying finish_reason + usage (anthropic adds prompt_tokens_details when cached). |
| openai-shape | only when you send stream_options:{"include_usage":true} — Ocean forwards it verbatim and relays whatever the upstream emits. |
Ocean meters tokens regardless (a passive usage-meter observes the passing stream for billing); the client-visible usage chunk on openai-shape still depends on stream_options.
model on stream chunks. On the openai-shape passthrough the per-chunk model is left exactly as the upstream reports it — it is not rewritten to the honest served slug (unlike the non-stream body). Translated shapes stamp the served slug on every chunk.
Response headers: content-type: text/event-stream, cache-control: no-cache, no-transform, x-accel-buffering: no, x-request-id.
Failover vs mid-stream failure. Failover between candidates is decided on the response status, before any SSE byte is read (runAttempts). Once bytes are flowing there is no re-failover: an openai-shape upstream error mid-stream is passed through as the upstream sent it; a translated shape simply stops emitting deltas.
Cancellation / abort. The client's AbortSignal is forwarded to the upstream fetch, so hanging up propagates to the provider. On a mid-stream disconnect the billing flush() never runs (TransformStream cancellation aborts the writable side without flushing) — the reserved hold simply stands until the W3 sweeper reclaims it; flush() (and settle) fire only on a fully-drained normal completion. There is no gateway-imposed stream idle timeout on this plane beyond the Workers runtime and the upstream's own.
4.2 Workflows-plane SSE
Code: routes/workflows.ts + lib/stream-relay.ts (workflowsEncoder) + engine orchestration/cnl-engine/context.ts.
Routes: POST /v1/workflows/execute-stream (inline YAML/JSON body), POST /v1/workflows/:id/execute-stream (saved workflow), and the legacy alias POST /v1/cnl/execute-stream. All require an ozk_ key (Authentication & keys). The relay also drains every provider sub-stream the run advertises and closes the execution row under waitUntil, so a client disconnect never aborts the run.
Wire format. Engine frames pass through verbatim as event: <name>\ndata: <json>\n\n. Provider token deltas are injected as event: token\ndata: {"task_id":"…","delta":"…"}\n\n. Token frames for a task are flushed before that run's terminal frame (ordering guarantee).
Engine event vocabulary (CNLEventType, orchestration/cnl-engine/context.ts):
request_received, workflow_loaded, workflow_started, task_dispatched, task_completed, task_error, task_skipped, streaming_started, streaming_completed, output_delivered, parallel_start, parallel_complete, sequential_start, sequential_complete, dynamic_start, dynamic_generated, dynamic_complete, batch_start, batch_item_start, batch_item_complete, batch_progress, batch_complete, debate_start, debate_round_complete, debate_judgment, debate_complete, condition_evaluated, workflow_yielded, workflow_result, workflow_completed, workflow_error, heartbeat.
Each data payload carries workflow_id, execution_id, timestamp, type, a nested data object, and elapsed_ms. The run ends on workflow_completed (carries final_task_id, execution_summary) or workflow_error (carries error).
Keepalive. The engine emits its own heartbeat events; the relay forwards them verbatim and additionally touches the execution row's updated_at every ~60s so a long run is not swept mid-flight. This plane does not inject comment-line keepalives (that is the agents plane, 4.3).
Reading the final result. After workflow_completed, fetch the stored output with GET /v1/workflows/results/:workflowId/:taskId (use the final_task_id) — see Workflows. While a run is in flight the results route returns 404 not_ready.
4.3 Agents-plane OpenAI-shaped stream
Code: routes/agents.ts (stream branch) + lib/stream-relay.ts (openaiEncoder). Requesting the stream: set "stream": true in the JSON body. This runs the same one-task engine envelope as the sync path but with force_streaming, and the openaiEncoder re-shapes the run into an OpenAI chat.completion.chunk stream — so an OpenAI SDK's streaming client works against an Ocean agent. (For the CNL-native event stream of the same agent, use the workflows plane instead.)
What the client sees — only data: lines and comments, never engine event: lines (they are dropped by the encoder):
id:chatcmpl-<executionId hex, 24 chars>— derived from the execution id, stable across the stream.model: the served model — a caller-pinnedprovider/model, else the provider's default from its/health, else<provider>/default. Never the agent id.- A primer chunk (
delta:{role:"assistant"}) is emitted lazily on the first token. - Content chunks:
delta:{content:"…"}. - Final chunk:
delta:{}withfinish_reason(stoporlength) and, when the split is readable, ausageobject; thendata: [DONE]\n\n. - Keepalive: a comment line
: OCEAN PROCESSING\n\nroughly every 15s while idle (so proxies don't time out a slow first token).
Error after the first byte. Once SSE headers are committed, a run error is emitted as data: {"error":{"message":…,"type":"server_error","code":"engine_error"}} and the stream closes without [DONE] (matching OpenRouter's mid-stream-error behaviour). A rejection before the first byte is a normal JSON 502 engine_error instead.
Headers: content-type: text/event-stream; charset=utf-8, cache-control: no-cache, x-accel-buffering: no, x-request-id, and X-Execution-Id (the run id — reuse it to read the result later at GET /v1/agents/results/:workflowId/:taskId, Agents).
4.4 Verified examples
An agent OpenAI-shaped stream — the primer, content deltas, a final usage chunk, and [DONE]:
curl -s -N "$API_BASE/v1/agents/llm-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"say hi in one word","stream":true,"max_tokens":16}'
A workflow event stream — engine frames pass through verbatim as named event: lines:
curl -s -N "$API_BASE/v1/workflows/execute-stream" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"workflow":"workflow:\n name: doc-stream\n tasks:\n - type: task\n id: t1\n agent: llm-agent\n prompt: say hi"}'
5. OpenRouter compatibility
Point your OpenAI or OpenRouter client at https://api.plungeai.com/v1, swap the key for an sk-ocean-… model 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.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-… |
sk-ocean-… |
new key (Authentication & keys) |
| Auth header | Authorization: Bearer |
Authorization: Bearer |
none |
| Model slug | provider/model |
provider/model |
none (bare aliases also resolve, 3.7) |
| Fallback list | models: [...] |
models: [...] |
none — same field, same semantics (3.3) |
| 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 |
absent — read traces | remove reads of usage.cost |
| Provider routing | provider:{...} request object |
not interpreted (see below) | use sort / models / presets |
5.2 Endpoints
| OpenRouter path | Ocean | Notes |
|---|---|---|
POST /chat/completions |
same | field caveats in 5.3 |
GET /models |
differs | different JSON shape (5.6) |
POST /embeddings |
differs | OpenAI models only in v1 (3.9) |
POST /completions (legacy text) |
missing | chat endpoint only |
GET /generation (cost lookup) |
missing | use GET /v1/traces/:id (Traces) |
GET /credits, /auth/keys, /key |
missing | balance/keys live in the dashboard |
/images, /audio/* |
missing | not offered |
/v1/agents, /v1/workflows, /v1/tools, /v1/mcp, /v1/discovery, /v1/traces |
Ocean-only | the execution planes (5.9) |
5.3 Request fields
Full per-provider-shape table is in 3.2. 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 verbatim (same behaviour). On anthropic/google/cohere many are dropped in translation — see 3.2. |
sort |
not a request field | Ocean-only — price/latency/throughput |
provider (routing object) |
supported | partial/rejected — not interpreted; forwarded verbatim, so non-openrouter upstreams 400 on it |
route:"fallback" |
supported | partial — not read; use models[] |
transforms, plugins |
supported | not interpreted — ride through to openai-shape upstreams (usually 400) |
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), otherwise ignored |
usage:{include:true} |
opt-in cost accounting | ignored — Ocean always meters; no cost returned |
5.4 Response fields
| Field | OpenRouter | Ocean |
|---|---|---|
id |
gen-… |
chatcmpl-… |
object, created, model, choices, usage |
present | present (model is the honest served slug, 3.3) |
provider (top-level) |
present | absent |
openrouter_metadata, system_fingerprint, service_tier |
present | absent (system_fingerprint only if the upstream sent it via passthrough) |
choices[].native_finish_reason |
present | absent — only the normalized finish_reason |
usage.cost, usage.cost_details, usage.is_byok |
present | absent — cost lives in traces |
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) |
| Keepalive comment | : OPENROUTER PROCESSING |
none on the money plane (raw passthrough); the agents plane sends : OCEAN PROCESSING |
Final usage chunk |
included (with cost) |
anthropic/google/cohere always; openai-shape only with stream_options.include_usage; never carries cost |
| 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). A client that reads architecture, supported_parameters, or per-token pricing.prompt will find none of them.
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.
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.
5.8 What breaks an OpenRouter SDK user today
error.codeis a string, not a number — code that branches onerr.error.code === 401never matches (compareerr.status/ the HTTP status instead).- No cost fields —
usage.cost,usage.cost_details,usage.is_byokare absent; per-request cost accounting readsundefined. Read cost from traces. - No
provider/openrouter_metadata/native_finish_reasonin the response — provider-attribution logic finds nothing. - OpenRouter-only request fields are not honoured —
provider,route,transforms,pluginsare not interpreted and, on non-openrouterupstreams, are forwarded verbatim and rejected (400). Replace withmodels+sort+ presets. - No
: OPENROUTER PROCESSINGkeepalive on the money-plane stream; openai-shape streams carry a finalusagechunk only if you passstream_options.include_usage. /modelsshape differs — noarchitecture/supported_parameters;pricingis per-1M numbers.HTTP-Referer/X-Titleare ignored — no app rankings / attribution.- No
/generation,/credits,/key,/completions,/images,/audioendpoints. - BYOK is dashboard-configured, not a per-request
providerobject (3.12).
5.9 Ocean-only capabilities you gain
- Execution planes an OpenRouter key cannot reach: Tools, Agents, Workflows, MCP, Discovery.
sort— route candidates byprice,latency, orthroughput(3.3).@preset/<slug>— server-stored routing + param bundles (3.4).- Guardrails — org/key spend caps, model/provider allow-lists, content filter,
require_byok(3.10). - Exact-match response cache —
$0hits with anx-cacheheader (3.11). - Traces & honest billing —
GET /v1/traces/:id, honest served-model reporting after failover (Traces).
6. Tools plane
A tool agent is any active registry agent with a typed invocation contract. The tools plane is the shortest path from "I want Brave search" to a result: three routes, one security check, one execution path. Every call becomes a one-task workflow on the engine, so it lands in your execution history and carries an x-request-id you can trace (Traces).
Discover ──► GET /v1/tools who is callable (the active catalog)
Inspect ──► GET /v1/tools/:id the contract: operations, params, examples
Execute ──► POST /v1/tools/:id/execute contract check → trust fence → engine → result
Auth: Authorization: Bearer $PLUNGE_API_KEY (an ozk_ key — see Authentication & keys).
6.1 List the catalog — GET /v1/tools
Returns the active registry agents as summary cards. Paging: limit (default 50, max 100) and offset. The response count is the size of the page you received, not the catalog total — page until a short page comes back.
curl -s "https://api.plungeai.com/v1/tools?limit=100" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
Response shape: { "tools": [ { "id", "type": "agent", "name", "description", "category", "tags": [...], "status": "active", ... } ], "count" }. Tags worth reading: gated:<operation> marks operations the trust fence will refuse unattended (see 6.4), status:active is the only status this route lists.
On 2026-09-18 the catalog had 92 active tool agents in 14 categories (GET /v1/agents/categories gives the live breakdown; the same query backs GET /v1/agents, so the two lists are identical — the difference is how you call them).
6.2 Inspect the contract — GET /v1/tools/:id
curl -s "https://api.plungeai.com/v1/tools/brave-agent" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
| Field | Meaning |
|---|---|
agent_id, name, description |
the card identity |
operations[] |
{ name, description?, required_params? } — the verbs the agent accepts as operation; an agent with no operations takes a prompt (or its soleRequiredParam) |
inputSchema |
JSON Schema (type: object, properties, required, additionalProperties) for params; per-operation requireds live on operations[].required_params |
examples[] |
{ title, cnl } — copy-paste CNL task snippets from the card's demonstrations |
output_type |
prose description of what comes back |
soleRequiredParam |
present when exactly one string param is required — a bare prompt is mapped onto it |
credentials |
filled by surfaces that know the user's connections; absent here |
Unknown id → 404 unknown_tool (also in the X-Error-Code header):
curl -s -o /dev/null -w '%{http_code} %header{x-error-code}\n' \
"https://api.plungeai.com/v1/tools/no-such-agent" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
6.3 Execute — POST /v1/tools/:id/execute
Body (JSON by default; the same fields as YAML with Content-Type: text/yaml):
| Field | Type | Notes |
|---|---|---|
operation |
string | one of operations[].name; omit for prompt-only agents |
params |
object | validated against the contract; names that collide with task-envelope fields (agent, id, type, …) are reserved and rejected |
prompt |
string | free-text input; satisfies missing requireds on prompt-capable agents |
format |
json · yaml · markdown · text |
response negotiation; Accept and the request Content-Type also count (default JSON, YAML in → YAML out) |
Order of evaluation, from orchestration/api-gateway/routes/tools.ts:
- Contract validation.
reservednames, or missing requireds on a prompt-less call →422 invalid_paramswith{ missing, reserved, warnings, contract }— the contract is echoed so you can fix the call without a second request. - The trust fence (
checkAgentCall, modeunattended) →403 refusedor409 approval_required— see 6.4. Nothing is executed and no execution row is written. - Execution. A
workflow_executionsrow opens under your user (execution_id= the request's trace id), the engine runs the one-task wrapper, the row closes with the result.
Success (200):
{ "ok": true, "content": "…markdown or text…", "outcome": "ok", "execution_id": "…", "request_id": "…" }
curl -s -X POST "https://api.plungeai.com/v1/tools/brave-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \
-d '{"prompt":"cloudflare workers pricing","params":{"count":3}}'
Operation form (the weather agent takes location; here the contract says what is missing):
curl -s -w '\n%{http_code}\n' -X POST "https://api.plungeai.com/v1/tools/weather-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \
-d '{"operation":"current","params":{}}'
6.4 Outcomes and the trust fence
An agent that cannot do the job answers with a structured outcome, not prose (core/core-base/agent-outcome.ts). The tools plane maps outcomes onto HTTP (routes/_util.ts outcomeStatus):
outcome |
HTTP | error.code |
What to do |
|---|---|---|---|
ok |
200 | — | read content |
needs_input |
422 | invalid_params |
fix params — error.detail and the echoed contract say how |
needs_connection |
424 | connection_required |
the user behind the key has not connected this provider — see Connectors |
needs_api_key |
424 | credential_required |
the agent needs a BYO key the user has not stored |
unavailable |
503 | agent_unavailable |
upstream/provider down — retry later or pick another agent |
error |
502 | execution_failed |
the agent failed; error.message carries the agent's own text |
The error body is { "error": { "code", "message", "outcome", "detail"?, "content"?, "contract"? } } plus an X-Error-Code header.
The trust fence (core/core-base/call-agent.ts) runs before every execution and cannot be turned off from the API:
- Always-gated operations —
buy,purchase,fetch_paid,send,transfer,pay,send_payment,withdraw,shop— plus anygated:<op>tag on the card. On this unattended surface they return403 refused(a human-in-the-loop surface such as Studio or a harness mission with an explicit allow-list returns409 approval_requiredinstead). - Guarded categories — agents in the
paymentandblockchaincategories are refused unattended unless a mission'sallowed_agentsnames them. - Not active — a card that is not
status:activeis403 agent_not_active; an unknown id is404 unknown_tool.
curl -s -w '\n%{http_code}\n' -X POST "https://api.plungeai.com/v1/tools/resend/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \
-d '{"operation":"send","params":{"from":"hello@example.com","to":"nobody@example.com","subject":"x","text":"x"}}'
A refusal writes no execution row — a denial is not a run.
6.5 The catalog by category (live 2026-09-18)
Ids are exact; GET /v1/tools/<id> is the source of truth for operations. Account = needs a connected account or BYO key for the user behind the key (see chapter 7); everything else runs on platform keys.
| Category | Agents | Account |
|---|---|---|
| search (14) | brave-agent, exa-agent, firecrawl-agent, tavily-agent, serper-agent, serpapi-agent, crawl4ai-agent, browserbase, arxiv-search, wikipedia-search, newsapi-get-articles, nyt-api, product-hunt-agent, branding-agent |
no |
| tools (23) | calculator-tool-agent, markitdown, weather-agent, wolfram-alpha-full-results, data-analyst, code-reviewer, technical-writer, research-analyst, research-team, learn, pdf-worker, pptx-worker, xlsx-worker, ms-office-worker, report-worker, render-worker, cloud-browser-agent, gemini-file-search, google-docs-export-worker, n8n-converter, concierge, agentic-agent, universal-agent |
no (google-docs-export-worker: Google) |
| ai (3) | llm-agent, vision-agent, skill-agent |
no |
| google-workspace (25) | google-gmail, google-calendar, google-drive, google-docs, google-sheets, google-slides, google-forms, google-tasks, google-contacts, google-chat, google-meet, google-keep, google-classroom, google-admin, google-vault, google-analytics, google-search-console, google-ads, google-merchant-center, google-business-profile, google-books, google-youtube, google-translate, cloud-natural-language, google-workspace-assistant |
Google (OAuth) |
| microsoft-365 (12) | microsoft-outlook-mail, microsoft-outlook-calendar, microsoft-outlook-contacts, microsoft-onedrive, microsoft-sharepoint, microsoft-teams, microsoft-todo, microsoft-planner, microsoft-onenote, microsoft-excel, microsoft-word, microsoft-powerpoint |
Microsoft 365 (OAuth) |
| coding-agents (1) | gitlab-agent |
GitLab (OAuth or PAT) |
| communication (2) | resend, cloudflare-email-agent |
no — send/send_batch gated |
| media (2) | image-gen-agent, video-gen-agent (generate_video gated) |
no |
| financials (1) | sec-agent |
no |
| entertainment (2) | sports-schedule-agent, tv-listings-agent |
no |
| ecommerce-agents (2) | amazon-shopping-agent, shopping-zinc (buy gated) |
Amazon retailer login |
| payment (2) | x402-wallet-agent (fetch_paid gated), crossmint-agent |
wallet — guarded category |
| blockchain (2) | circle-agent, circle-wallet-agent |
Circle wallet — guarded category |
| shopping (1) | x402-broker (execute gated) |
wallet |
6.6 Using tools from your own LLM loop
The tools plane is designed to sit behind any function-calling model — no SDK required:
GET /v1/tools/:idfor the agents you want; turn each contract into a function definition (name= agent id,parameters=inputSchema, one function peroperations[].nameif you want the model to pick operations).- Send the definitions to your model as
tools(any provider — the models plane works too). - When the model returns a tool call,
POST /v1/tools/<id>/executewith{ "operation", "params" }. - Feed
contentback as the tool result. On422feed backerror.message+error.detail— the model fixes its own params. On403 refusedtell the user a human has to approve that action.
Every hop carries x-request-id; keep it in your logs and GET /v1/traces/:id shows the run. For a server-side loop that already does all of this (discover → inspect → execute with an approval gate), call the concierge agent on the agents plane.
7. Connectors & connected accounts
A connector is a third-party integration a user connects once — Gmail, Microsoft 365, GitHub, Adobe, Slack, Stripe, Notion, … A connected account is that user's live credential row. Tool agents then act as that user: google-gmail reads the mailbox of whoever owns the ozk_ key that made the call. Nothing about the credential ever appears in the API.
Three things are easy to confuse:
| Thing | What it is | Where |
|---|---|---|
Tool agent (google-gmail) |
code that calls a provider's API on behalf of a user | tools / agents planes |
Connector (google, github, …) |
the OAuth app or API-key slot a user connects | Studio → Integrations |
Connector proxy provider (brave-search, firecrawl, …) |
a metered pass-through to a provider's REST API with your own or the platform's key | gateway.plungeai.com/connect/… (7.5) |
7.1 Two credential tiers
| Tier | How the user connects | Examples | Stored as |
|---|---|---|---|
| OAuth | clicks Continue with |
Google Workspace, Microsoft 365, Adobe, GitHub, GitLab, Slack, Salesforce, HubSpot, Bitbucket, Shopify | encrypted access + refresh token; refreshed server-side when tokensExpire |
| API key / PAT | pastes a key in the connector dialog | Anthropic, OpenAI, Stripe, Notion (internal token), GitLab PAT, Linear, Jira, Datadog, … | encrypted key; verified live before storage (core/core-secrets/api-key-verify.ts probes the provider — a rejected key is never saved) |
Both land in user_credentials on the compliance database, AES-256-GCM encrypted with per-scope keys; every read is audited (core/core-secrets/CLAUDE.md). Agents never receive raw tokens from callers — the platform injects them at run time, which is why params names such as api_key or token are stripped by contract validation.
7.2 How a connected account flows into an API call
your app ──ozk_ key──► One API ──user_id──► engine ──► google-gmail agent
│ resolveApiKey(google, user)
▼
user_credentials row? yes → call Gmail as the user
no → outcome: needs_connection
Without a connection the agent answers with a structured refusal (success: false, outcome: needs_connection) and the tools plane returns 424 connection_required:
{ "error": { "code": "connection_required", "message": "Connect your Google account in Studio → Integrations to use google-gmail", "outcome": "needs_connection" } }
The same run with a connection returns 200 { ok: true, content } — live example from 2026-09-18 with a Microsoft 365 connection on the calling account (microsoft-todo, operation list_lists, request id 1ee507f1-a5b6-48c8-a963-51b6fbd0f904).
Which agents need what: the Account column in the tool catalog. The rule of thumb: google-* needs the Google connector, microsoft-* needs Microsoft 365, adobe-* Adobe, gitlab-agent GitLab; search/scrape/document tools run on platform keys.
7.3 How your testers connect an account
Connecting is a browser step in Ocean Studio (https://studio.plungeai.com), because OAuth needs a redirect back to a page the user is logged into:
- Studio → Integrations → Browse connectors → pick the provider.
- OAuth connector: Continue with
→ approve → back in Studio as Connected. API-key connector: paste the key → it is probed live → Connected (or an inline "rejected" error). - Verify with the API: run the agent with the same user's
ozk_key — theneeds_connectionoutcome disappears.
The Studio endpoints behind those buttons (session cookie, not API key — listed so you know what happens, not for you to call): GET /api/connections (yours), GET /api/connections/providers (public: every OAuth provider and its scopes), GET /api/auth/oauth/:provider/start, POST /api/connections/api-key (400 when the live probe rejects the key), DELETE /api/connections/:provider. Special flows exist for the Circle agent wallet (/api/connections/circle-agent-wallet/init|verify), retailer logins such as Amazon (/api/connections/retailer/connect) and QR/browser logins such as WhatsApp (/api/connections/browser-session/*).
curl -s "https://studio.plungeai.com/api/connections/providers"
Redirect URIs and OAuth app registration are platform-side — testers never configure anything at the provider.
7.4 The connector catalog
The registry lists 300 connector cards (GET /v1/discovery/search?kind=connectors&limit=100, page with offset; live 2026-09-18). Each card carries connector.auth:
connector.auth |
Meaning | Count |
|---|---|---|
oauth_static |
a platform-registered OAuth app; user clicks Continue | ~45 |
api_key |
user pastes a key/PAT | ~90 |
oauth_dcr |
an MCP server the user connects with Dynamic Client Registration (see MCP plane) | ~110 |
none / absent |
platform-key or public provider — nothing to connect | ~55 |
curl -s "https://api.plungeai.com/v1/discovery/search?kind=connectors&limit=3" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
curl -s "https://api.plungeai.com/v1/discovery/cards/connector/slack" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
OAuth apps registered on the platform today (client id wired in the Studio worker, wrangler-templates/template.wrangler.ocean.jsonc ↔ core/core-secrets/oauth-providers.ts): google (Workspace: Drive, Gmail, Calendar, Contacts, Tasks, Docs, Sheets, Slides, Analytics, Ads, Search Console, YouTube, …), azure (Microsoft 365: OneDrive, Outlook, Calendar, Teams, SharePoint, To Do, Planner, OneNote, Office files), adobe (Creative Cloud, Express, Photoshop, Lightroom, Stock, Experience Platform, Journey Optimizer), github, gitlab, bitbucket, slack, salesforce, hubspot, shopify. Provider entries also exist for sonos, spotify, tesla, facebook, linkedin, twitter, reddit, tiktok, pinterest, snapchat, intuit, square, zoom, sentry, notion — their cards show in Studio, but the OAuth app is not registered yet, so Continue with … bounces with oauth_not_configured; connect those with an API key where the card offers one.
By category (ids as the registry spells them):
| Group | Connectors |
|---|---|
| AI & ML | openai, anthropic, huggingface, ibm |
google (Workspace umbrella), google-drive, google-gmail, google-calendar, google-contacts, google-tasks, google-youtube |
|
| Microsoft | azure (Microsoft 365 umbrella), microsoft-onedrive, microsoft-outlook-mail, microsoft-outlook-calendar, microsoft-teams, microsoft-sharepoint, microsoft-365 |
| Adobe | adobe, adobe-express, adobe-photoshop, adobe-lightroom, adobe-stock, adobe-cc-files, adobe-aep, adobe-ajo, adobe-workfront |
| Developer & cloud | github, gitlab, bitbucket, postman, aws, cloudflare, vercel, netlify, supabase, databricks, snowflake |
| Observability | sentry, datadog, grafana-mcp-server, honeycomb, incident-io, logrocket-mcp, jam |
| Communication | slack, discord, telegram, twilio, sendgrid, mailchimp, resend, whatsapp, whatsapp-personal |
| Productivity & PM | notion, airtable, asana, jira, linear, monday, clickup, trello, basecamp, teamwork, craft, mem, roam-research, ticktick, … (28 productivity + 19 project-management cards) |
| CRM, sales & marketing | salesforce, hubspot, pipedrive, zoho, close, helpscout, intercom, attio, apollo-io, klaviyo, lemlist, outreach, salesloft, zoominfo, … (49 marketing-and-sales cards) |
| Payments & finance | stripe, paypal, square, plaid, xero, circle, circle-agent-wallet, x402-wallet, intuit, intuit-quickbooks, intuit-payments, intuit-payroll, intuit-time-tracking, airwallex |
| Data & analytics | amplitude, mixpanel, segment, looker, alpha-vantage |
| Search & scraping (platform-key providers) | brave-search, exa, firecrawl, crawl4ai, browserbase, scrapingbee, serpapi, serper, perplexity-search, news-api, nyt, arxiv, wikipedia, product-hunt, weather, wolfram-alpha |
| Social | facebook, instagram, threads, linkedin, twitter, reddit, pinterest, snapchat, tiktok, youtube, bereal, clubhouse |
| Commerce & travel | shopify, amazon, target-app, shop, instacart, expedia, booking, tripadvisor, uber, viator, kiwi-com, amadeus, … |
| Legal, security, research, lifestyle | 13 legal, 15 security, 8 research and 11 lifestyle MCP-server connectors (oauth_dcr / none) |
7.5 The connector proxy — gateway.plungeai.com/connect/:provider/proxy/*
For providers you would otherwise call directly, the inference gateway offers a metered pass-through (inference/gateway/src/routes/connector-proxy.ts): one sk-conn- key, one bill, transparent bytes in both directions (streaming included, no body transformation).
| Key | sk-conn-… — Dashboard → Connectors → API keys (https://dashboard.plungeai.com); per-org, optional allowed_providers and monthly_limit_usd |
| Route | ANY https://gateway.plungeai.com/connect/<provider_id>/proxy/<upstream path> — the path after /proxy/ is appended to the provider's base_url |
Mode (from the connector_providers rate card) |
byok — your own upstream token connected for the org is used, you pay byok_routing_fee; platform — the platform's key is used, you pay price_per_call; both — your token if connected, else the platform's |
| No connection, provider is BYOK-only | 402 "connection required" |
| Unknown or inactive provider | 404 model_not_found / 400 provider_unavailable |
| Limits | per-provider rate_limit_rpm; org spend caps and guardrails apply exactly as on the models plane |
| Auth failures | 401 invalid_api_key; a suspended or closed account is 401 Account suspended |
Example (verified against the source, not run live — no sk-conn- key was available on 2026-09-18):
curl "https://gateway.plungeai.com/connect/brave-search/proxy/res/v1/web/search?q=cloudflare+workers" \
-H "Authorization: Bearer $PLUNGE_CONNECTOR_KEY"
Each call is metered as a connector_requests row and appears in the Dashboard next to your inference spend. Prefer the tools plane when you want the agent's judgement (result formatting, pagination, outcome vocabulary); use the proxy when you want the provider's raw API with one key and one bill.
8. Discovery
Discovery is the read-only catalog behind every other plane: agents, skills, plugins, MCP servers, connectors, models, providers and (once you have saved some) workflows. Three routes, all ozk_-authenticated, all backed by the registry service (orchestration/api-gateway/routes/discovery.ts → orchestration/registry/).
8.1 Search — GET /v1/discovery/search
| Param | Values | Notes |
|---|---|---|
q |
text | natural-language or keyword query; with q the registry runs hybrid search (semantic + keyword), without it a plain listing |
kind |
agents · skills · plugins · mcp-servers · connectors · models · providers · workflows · experts · backgrounds · personas (= digital-twins) |
the plural catalog groups (registry.ts KIND_GROUPS) |
type |
singular storage type (agent, skill, mcp_server, …) |
alternative to kind |
category |
e.g. search, google-workspace |
exact card category |
status |
active (default) |
the public catalog shows callable capabilities only; other statuses need an explicit value |
tier |
card tier tag | e.g. verified |
mode |
hybrid (default with q) · keyword · vector |
force one search leg |
fields |
summary · full |
full adds parameters, demonstrations and prompt_format |
limit, offset |
ints | limit is capped by the registry; page with offset |
include |
quality |
attaches a quality object per agent card — { score, success_rate, runs } or null when the evaluation service has no data (it was null for every card on 2026-09-18) |
Response: { "cards": [...], "count": <cards in this page>, "searchMethod": "text" | "hybrid" | "vector", "query": { …the normalised query… } }. count is the page size, not the catalog total.
curl -s "https://api.plungeai.com/v1/discovery/search?q=web+search&kind=agents&limit=3" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
curl -s "https://api.plungeai.com/v1/discovery/search?q=llm&kind=agents&limit=2&include=quality" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
Catalog sizes on 2026-09-18 (limit=100, first page): agents 92, skills 12, plugins 5, providers 11, mcp-servers 100+, models 100+, connectors 300 (3 pages), workflows 0 for a fresh key (saved workflows are per user), experts / backgrounds / personas 0 (capability cards are created by users and missions — see Skills, plugins & capability injection).
8.2 The card object
Summary cards (fields=summary, the default for the agents/tools lists) carry: id, type, name, version, description, category, best_for[], not_for[], tags[], status, icon, color, created_at, updated_at. Full cards add parameters[], demonstrations[], prompt_format, output_type, output. Connector cards add a connector object (auth, provider_key, key_placeholder, help_text); model cards add pricing and capability columns from the rate card.
Tags encode the operational facts the planes act on:
| Tag | Meaning |
|---|---|
status:active |
callable — the only status the tools/agents planes accept |
gated:<op> |
the trust fence refuses <op> unattended (403) — Tools plane |
tier:verified |
reviewed card |
auth:oauth_static · auth:api_key · auth:oauth_dcr |
how a connector is connected — Connectors |
serves:agent |
a connector some tool agent uses |
8.3 Recommend — POST /v1/discovery/recommend
Body { "type": "agent" | "skill" | … (plural kinds accepted as aliases), "task": "<what you want done>" } — the registry ranks cards for the task. Status 2026-09-18: this route returned 502 upstream_error for every request (the registry's /api/cards/recommend failed upstream; the router replaces raw upstream 5xx with a clean envelope). Until it is fixed, use GET /v1/discovery/search?q=<task>&kind=agents — the same hybrid ranking, one card list. The defect is logged in orchestration/api-gateway/live-test-reports/2026-09-18-openrouter-compat/OPENROUTER-COMPAT-LOG.md (follow-ups) and in log/2026-09-18-one-api-docs-3.0.md.
8.4 One card — GET /v1/discovery/cards/:type/:id
Returns the full card as markdown with YAML frontmatter (Content-Type: text/markdown) — the same document the platform's own agents read before calling a tool. :type is the singular storage type (agent, skill, plugin, mcp_server, connector, model, provider, workflow, expert, background, digital_twin); the plural kind spellings (agents, mcp-servers, …) are accepted as aliases.
curl -s "https://api.plungeai.com/v1/discovery/cards/agent/brave-agent" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
Unknown card → 404 not_found. A 4xx from the registry is normalised into the One API error envelope; a registry 5xx never reaches you raw (502 upstream_error).
8.5 From an id to a call
Card type |
Use it as |
|---|---|
agent |
GET /v1/tools/<id> + POST /v1/tools/<id>/execute (tools); POST /v1/agents/<id>/execute (agents); agent: <id> in a CNL task (workflows) |
model |
"model": "<id>" on POST /v1/chat/completions — ids are provider/model slugs (models); also model: + provider: on an agent-plane call |
provider |
the provider/ prefix of model slugs; provider: on agent-plane calls |
skill · plugin · expert · background · digital_twin |
skills: [<id>], plugins: [<id>], experts: [<id>], backgrounds: [<id>], persona: <id> on a type: harness task (capability injection) |
mcp_server |
POST /v1/mcp/runs with the server id (MCP plane); mcp: [<id>] on a harness mission |
connector |
connect it in Studio → Integrations; then the agents that serves:agent it stop returning needs_connection (connectors) |
workflow |
POST /v1/workflows/<id>/execute (workflows) |
Search results are cached briefly on the platform side (KV + LRU, ~200 ms per uncached call); agents that discover at run time — concierge, the harness loop — hit this same route, so what you see is what they see.
9. Agents plane
The agents plane runs one registry agent per request and hands back its result. It is the
execution plane you reach with an ozk_ key (see Authentication) at
POST /v1/agents/:id/execute. Every call wraps exactly one minimal CNL task in the engine
envelope, so the agents plane is the single-task front door; multi-task orchestration lives on the
Workflows plane and bounded autonomous missions on a type: harness task
there (see capability injection).
Base URL is always https://api.plungeai.com. All examples below use $PLUNGE_API_KEY for an
ozk_… key. Source: orchestration/api-gateway/routes/agents.ts.
9.1 Agent tiers
An agent is a registry card whose id equals its Worker service name. There are ~90 active
agents (GET /v1/agents returned count: 92, live-verified 2026-09-18). They fall into these
tiers. Every id below is status:active in the registry unless the row says otherwise
(agents/agents/README.md, agents/agents/CORE-AGENTS-DECISION.md).
| Tier | id(s) | What it is | Use when | Call how |
|---|---|---|---|---|
| Tool agent | ~90 ids: serper-agent, exa-agent, tavily-agent, sec-agent, serpapi-agent, markitdown, weather-agent, … |
a registry agent with a typed invocation contract (parameters/operations) | you need one bounded capability (search, documents, finance, a connector) | POST /v1/agents/:id/execute with a prompt; or the Tools plane POST /v1/tools/:id/execute with typed params; or an agent: task in a workflow |
llm-agent |
llm-agent |
thin single-shot LLM call — YAML in → provider RPC → result | plain generation with a pinned model/provider | agents-plane execute; model/provider/system/messages |
skill-agent |
skill-agent |
single-shot LLM with persona/expert/skill prompt injection | generation shaped by a persona or skill | agents-plane execute with persona; skills via a workflow (chapter 14) |
speed-agent |
speed-agent |
ultra-low-latency stateless pass-through to the provider (the internal perf baseline) | latency-critical simple calls | not status:active on the ozk plane today — live-verified 2026-09-18 it returns 403 agent_not_active; used inside workflows run by the engine |
| Harness loop | universal-agent, agentic-agent |
a bounded ReAct mission runtime: tool fence, iteration cap, long-term memory, capability injection | multi-step autonomous work that calls tools/sub-agents | a type: harness task on the Workflows plane (recommended) — the agents plane runs these single-shot only (no mission fields, see 9.3) |
concierge |
concierge |
interactive Discover→Inspect→Execute front-door over any agent, with a payment/approval gate | "do X" where the platform must find and call the right agent, with a money gate | its own approval contract (9.7) and the MCP plane plungeai_continue; the raw agents plane forwards only generation knobs |
claude-managed-agent |
deployed, not in the active list | Anthropic hosted Managed Agents runtime for autonomous cloud code execution | autonomous cloud code runs (higher infra overhead) | not exposed on the ozk agents plane today — live-verified 2026-09-18 it is absent from GET /v1/agents, so /v1/agents/claude-managed-agent/execute returns 404 unknown_agent |
| Campaign kind | Studio kind: campaign (no campaign-agent Worker) |
a scheduled, ledger-driven workflow that works a task list in short runs | recurring bounded batch work | via the scheduler / MCP plungeai_schedule / Studio, not a callable agent id (agents/agents/campaign-agent/README.md) |
The bare loop-runtime binding
harness-agentis not a callable registry id on the planes (live-verified:404 unknown_agent), andspeed-agentis not status:active there (403 agent_not_active). Call the harness loop throughuniversal-agent/agentic-agent, or through atype: harnesstask on the workflows plane;llm-agent/skill-agentare the directly-callable single-shot ids (both live-verified).
9.2 List agents & categories
GET /v1/agents?limit=<1..100>&offset=<n>→{ agents: [...], count }. Each card carriesid, a summary andtags(includingstatus:active).GET /v1/agents/categories→{ categories: [{name, count}], count }, aggregated from the active-agent summaries (there is no separate categories store).
curl -s "https://api.plungeai.com/v1/agents?limit=5" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
9.3 Execute — body reference
POST /v1/agents/:id/execute. JSON body (default) or an Ocean-format text/yaml body with the
same fields. Every accepted field, from routes/agents.ts:
| Field | Type | Status | Meaning |
|---|---|---|---|
prompt |
string | supported, required | the per-run input. Missing/blank → 400 missing_prompt |
input |
string | supported | alias for prompt |
sync |
boolean | supported (default true) |
false → 202 pointer, poll the result route (9.5) |
stream |
boolean | supported (default false) |
true → an OpenAI-shaped SSE stream (9.6) |
messages |
array | partial | forwarded to the agent as messages; prompt is still required even when present (live-verified: messages alone → 400 missing_prompt). Multi-turn threading depends on the target agent — for reliable conversation use the MCP plane (session_id, plungeai_continue) |
system |
string | supported | system-prompt frame forwarded to the agent |
model |
string | supported | pin a model; validated against the catalog → 422 unknown_model if unknown/inactive |
provider |
string | supported | pin a provider; normalised (google→gemini). Must travel with model — a pinned model without its provider routes to the agent default |
temperature |
number | supported | sampling temperature |
top_p |
number | supported | nucleus sampling |
max_tokens / maxTokens |
number | supported | output-token budget (must be > 0) |
reasoning_effort |
string | supported | reasoning-effort hint forwarded to the model |
thinking_level |
string | supported | thinking-level hint forwarded to the model |
persona |
string | supported | the only capability field forwarded on this plane |
format |
json|yaml|markdown|text |
supported | response format (also ?format= or Accept). Unknown value → 400 invalid_format (lib/format.ts) |
Fields not in this table (e.g. goal, mission, allowed_tools, skills, experts, mcp,
approved, action_token) are ignored on the agents plane — run those through a
type: harness task on the Workflows plane.
Sync execute (default)
curl -s "https://api.plungeai.com/v1/agents/llm-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"prompt":"Reply with only the word: pong","max_tokens":24}'
Success (200): { "content": "...", "workflow_id": "...", "task_id": "t1", "request_id": "..." }.
request_id equals the trace id (see Traces).
Model / provider pinning
Pin a model and provider together; discover models with GET /v1/models
(Models plane) or GET /v1/discovery/search?kind=models (Discovery).
curl -s "https://api.plungeai.com/v1/agents/llm-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"prompt":"Reply with only: pong","provider":"openai","model":"gpt-4o-mini","max_tokens":16}'
9.4 Async execution (202 + poll)
Send "sync": false. The response is 202 with a pointer { workflow_id, task_id, request_id };
read the result from the result route once it is ready.
RESP=$(curl -s "https://api.plungeai.com/v1/agents/llm-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"prompt":"Reply with only the word: pong","max_tokens":24,"sync":false}')
echo "$RESP"
WF=$(printf '%s' "$RESP" | sed -n 's/.*"workflow_id":"\([^"]*\)".*/\1/p')
TASK=$(printf '%s' "$RESP" | sed -n 's/.*"task_id":"\([^"]*\)".*/\1/p')
for i in 1 2 3 4 5 6 7 8; do
R=$(curl -s "https://api.plungeai.com/v1/agents/results/$WF/$TASK" \
-H "Authorization: Bearer $PLUNGE_API_KEY")
case "$R" in *'"content"'*) echo "$R"; break;; *) sleep 2;; esac
done
9.5 Results route
GET /v1/agents/results/:workflowId/:taskId → { content, content_type, workflow_id, task_id }.
It reconciles the run's terminal state (lib/execution-row.ts):
| Situation | Status | Code |
|---|---|---|
| result stored | 200 |
— |
| run still in flight | 404 |
not_ready |
| run failed | 409 |
execution_failed |
| run completed but produced no visible content | 422 |
empty_completion (raise max_tokens ≥ 64) |
9.6 Streaming
Send "stream": true on POST /v1/agents/:id/execute. The agents plane emits an
OpenAI-shaped chunk stream (the only stream shape on this plane) — Content-Type: text/event-stream, data: lines only (no named event: lines), each a chat.completion.chunk,
terminated by data: [DONE]. This mirrors the models plane; see Streaming.
Relay + encoder: lib/stream-relay.ts (relayEngineStream, openaiEncoder).
Exact request shape:
curl -sN "https://api.plungeai.com/v1/agents/llm-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"prompt":"Reply with only the word: pong","max_tokens":24,"stream":true}'
Observed wire (live-verified 2026-09-18):
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":…,"model":"gemini-3.1-flash-lite","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"pong"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":72,"completion_tokens":1,"total_tokens":73}}
data: [DONE]
Notes: model is the pinned model, else the provider's default (never the agent id); a final
usage is included when the provider reported the token split. On an error after the first
byte the stream emits data: {"error":{…}} and closes without [DONE] (OpenRouter behaviour);
a pre-dispatch rejection is a JSON error (see 9.8), not a stream.
9.7 The concierge approval flow
concierge runs a bounded Discover→Inspect→Execute loop with three tools — find_agent
(discover), agent_card (inspect), run_agent (execute) — and a payment gate
(agents/agents/concierge-agent/README.md). Money operations (buy, purchase, send,
transfer, pay, withdraw, shop, …) and any card gated:<op> are held for approval:
- A gated action returns
{ "needs_approval": true, "pending_action": {…}, "action_token": "<10-char>", "messages": [...] }. - Re-invoke with the same
messages, plus"approved": trueand theaction_token, to execute and continue. The token is held server-side for 1 hour.ask_userquestions work the same way (append the user's answer tomessages).
On the One API this handshake is surfaced through the MCP plane: the pause arrives
as a ⏸ continuation, and you approve with plungeai_continue { approve: true } (never approve on
the user's behalf). The raw agents plane forwards only generation knobs + messages + persona
— it does not forward approved/action_token, so drive the multi-turn approval loop over MCP,
Studio, or the concierge agent's own endpoint. (Not live-verified here: an approval run can spend
money.)
9.8 Errors
Pre-dispatch and post-dispatch errors from routes/agents.ts / routes/_util.ts. Full envelope
and header reference: Errors, limits & headers.
| Status | Code | When |
|---|---|---|
400 |
missing_prompt |
no prompt/input string |
400 |
invalid_format |
format is not json/yaml/markdown/text |
404 |
unknown_agent |
no such agent id (fenced before dispatch) |
403 |
agent_not_active |
the card is not status:active |
422 |
unknown_model |
pinned model absent/inactive in the catalog |
422 |
empty_completion |
run completed but produced no visible content |
422 |
invalid_params |
agent outcome needs_input |
424 |
connection_required |
outcome needs_connection (connect an account — chapter 7) |
424 |
credential_required |
outcome needs_api_key |
503 |
provider_unconfigured |
the provider has no API key configured on the platform |
503 |
agent_unavailable |
outcome unavailable |
502 |
engine_error / execution_failed |
the engine/agent failed |
502 |
result_unavailable |
run finished but the result could not be retrieved |
409 |
duplicate_execution_id |
a replayed x-trace-id UUID |
413 |
payload_too_large |
body over the cap (default 1 MiB) |
curl -s "https://api.plungeai.com/v1/agents/no-such-agent-xyz/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"prompt":"hi","max_tokens":8}'
10. Workflows plane
The workflows plane runs a CNL workflow — one or many agent tasks orchestrated by the engine —
and hands back a pointer to the result. Reach it with an ozk_ key
(Authentication) at POST /v1/workflows/execute (inline) or
POST /v1/workflows/:id/execute (saved). Base URL is always https://api.plungeai.com.
Source: orchestration/api-gateway/routes/workflows.ts, engine orchestration/cnl-engine/.
Where the Agents plane runs one task, the workflows plane runs a whole DAG:
parallel fan-out, sequential stages, data flow between tasks, and bounded autonomous
type: harness missions.
10.1 CNL in one page
CNL (the YAML workflow language) has a workflow with a name and a tasks[] array. Each task
is either a leaf task or a compound block; a top-level input: (sibling of workflow:) seeds the
run. Field names are from orchestration/cnl-engine/context.ts, schema-types.ts and the real
files in workflows/*.yaml.
| Field | On | Meaning |
|---|---|---|
name |
workflow |
workflow name |
tasks[] |
workflow |
ordered list of tasks/blocks |
type |
task | task | parallel | sequential | harness | dynamic | batch | debate (the TaskType enum also has the legacy agent_loop and internal output / validate) |
id |
task | task id — the key its output is stored under |
agent |
leaf task | the agent id to run (must be status:active, or the run is fenced 404/403) |
prompt |
leaf task | the task input; search agents take the literal query |
operation |
leaf task | a named operation for a structured tool-agent (e.g. serpapi-agent operation: amazon) |
| (agent params) | leaf task | any extra card parameters as task fields (e.g. num: 15, searchType: shopping) |
subtasks[] |
compound block | children of a parallel / sequential / … block |
Data flow. There is no depends_on — a reference is the dependency:
{input}injects the workflow-level input.{data:<task_id>}injects a prior task's stored output into an LLM prompt. Asequentialblock runs itssubtasksin order; aparallelblock runs them at once (Promise.all, no width cap — see the engine README "Parallelism"). Reference an upstream task and the engine orders around it.
A real 3-task workflow — a parallel search fan-out feeding one LLM synthesis (adapted from
workflows/amazon-price-comparison.yaml):
workflow:
name: price-check
tasks:
- type: parallel
id: search_phase
subtasks:
- type: task
id: amazon_search
agent: serpapi-agent
operation: amazon
prompt: "{input}"
num: 15
- type: task
id: shopping_search
agent: serper-agent
searchType: shopping
prompt: "{input}"
- type: task
id: recommendation
agent: llm-agent
prompt: |
For the search "{input}", pick the top 3 products.
Amazon results: {data:amazon_search}
Shopping results: {data:shopping_search}
Use only links that appear verbatim in the data.
input: "wireless headphones"
10.2 Inline execute
POST /v1/workflows/execute accepts both body shapes (routes/workflows.ts):
- JSON
{ "workflow": <object | YAML string>, "input"?: any, "inputs"?: any }. Missingworkflow→400 missing_workflow. - Raw
text/yaml— the fullworkflow:document as the body.
Success is a pointer, not the content: { success: true, workflow_id, final_task_id, request_id }.
Read the content from the result route (10.5).
curl -s "https://api.plungeai.com/v1/workflows/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"workflow":{"name":"doc-demo","tasks":[{"type":"task","id":"t1","agent":"llm-agent","prompt":"Reply with only the word: pong"}]},"input":"go"}'
The same run as a raw YAML body (the response mirrors the request Content-Type → YAML, see 10.6):
printf 'workflow:\n name: doc-demo-yaml\n tasks:\n - type: task\n id: t1\n agent: llm-agent\n prompt: "Reply with only the word: pong"\n' \
| curl -s "https://api.plungeai.com/v1/workflows/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @-
10.3 Streaming — execute-stream
POST /v1/workflows/execute-stream (same body shapes) forces the engine to stream and relays every
engine SSE frame verbatim, plus a relay-injected event: token per provider delta
(lib/stream-relay.ts workflowsEncoder). This is the named-event stream (distinct from the
OpenAI-shaped agent stream in Streaming). Event vocabulary (context.ts
CNLEventType):
| Event | When | Payload (data:) |
|---|---|---|
request_received |
run accepted | request metadata |
workflow_loaded |
workflow parsed | workflow name/id |
workflow_started |
execution begins | workflow id |
task_dispatched |
a task is sent to its agent | {task_id, agent, streaming?} |
streaming_started |
a task's provider stream opened | {task_id, stream_url} |
token |
a provider delta (relay-injected) | {task_id, delta} |
streaming_completed |
a task's provider stream closed | {task_id} |
task_completed |
a task finished | {task_id, …} |
task_error |
a task failed | {task_id, error} |
output_delivered |
a task output was stored | {task_id} |
parallel_start / parallel_complete |
a parallel block boundary |
block id |
sequential_start / sequential_complete |
a sequential block boundary |
block id |
batch_* / debate_* / dynamic_* |
block-specific progress | block payload |
condition_evaluated |
a task condition was resolved |
{result} |
heartbeat |
idle keepalive | — |
workflow_result |
final result frame | {final_task_id, …} |
workflow_completed |
terminal (success) | {final_task_id, execution_summary} |
workflow_error |
terminal (failure) | {error} |
The terminal event is workflow_completed (or workflow_error). Live-verified 2026-09-18 the
stream carried: request_received, workflow_loaded, workflow_started, task_dispatched,
streaming_started, token, streaming_completed, task_completed, workflow_result,
workflow_completed.
printf 'workflow:\n name: stream-demo\n tasks:\n - type: task\n id: t1\n agent: llm-agent\n prompt: "Reply with only the word: pong"\n' \
| curl -sN "https://api.plungeai.com/v1/workflows/execute-stream" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @-
10.4 Saved workflows
A saved workflow has an id in the engine's KV (workflow:{user}:{id}). Create one in Studio or
over the MCP plane (plungeai_build_workflow from a goal, or plungeai_workflow
action: create). Then run it by id:
# not run here — requires a saved workflow id you own
curl -s "https://api.plungeai.com/v1/workflows/<workflow_id>/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"input":"quarterly numbers"}'
Body: { "input"?: any, "inputs"?: any }. An unknown id → 404 workflow_not_found. A streaming
variant POST /v1/workflows/:id/execute-stream exists with the same body and the 10.3 event
vocabulary.
10.5 Results route
GET /v1/workflows/results/:workflowId/:taskId → { content, content_type, workflow_id, task_id },
with the same reconciliation as the agents plane: 404 not_ready (in flight), 409 execution_failed, 422 empty_completion.
RESP=$(curl -s "https://api.plungeai.com/v1/workflows/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"workflow":{"name":"res-demo","tasks":[{"type":"task","id":"t1","agent":"llm-agent","prompt":"Reply with only the word: pong"}]}}')
WF=$(printf '%s' "$RESP" | sed -n 's/.*"workflow_id":"\([^"]*\)".*/\1/p')
FT=$(printf '%s' "$RESP" | sed -n 's/.*"final_task_id":"\([^"]*\)".*/\1/p')
for i in 1 2 3 4 5 6 7 8; do
R=$(curl -s "https://api.plungeai.com/v1/workflows/results/$WF/$FT" \
-H "Authorization: Bearer $PLUNGE_API_KEY")
case "$R" in *'"content"'*) echo "$R"; break;; *) sleep 2;; esac
done
10.6 Response formats
One negotiated payload, four wires (lib/format.ts): json (default), yaml, markdown, text.
Precedence: body.format / ?format= → Accept header → the request Content-Type mirror
(text/yaml → yaml, else json). An explicit unknown format value → 400 invalid_format; an
unknown Accept type falls through to the mirror. markdown renders through the same renderer the
MCP plane uses; text is the bare result content.
10.7 The type: harness task
A type: harness task binds an inline mission onto the harness loop runtime — a bounded ReAct
agent with a tool fence, iteration cap, long-term memory, and capability injection. All fields and
a full example are in Skills, plugins & capability injection;
in short:
workflow:
name: research-brief
tasks:
- type: harness
id: brief
agent: universal-agent
goal: "One-paragraph brief on the latest Anthropic model; every claim a source URL."
mission: "You are a research analyst. Produce sourced, factual briefs."
allowed_tools: [web_search, web_fetch, task_complete]
success_criteria:
- "Every factual claim is followed by a source URL."
max_iterations: 8
skills: [research]
10.8 Legacy aliases, limits & headers
Deprecated aliases (same auth, same workflow-plane handlers; prefer /v1/workflows/*):
POST /v1/execute, POST /v1/cnl/execute, POST /v1/cnl/execute-stream
(api-gateway.ts).
Limits (see Errors, limits & headers for the full table):
- Body cap:
MAX_REQUEST_SIZE, default 1 MiB →413 payload_too_large. - Replayed
x-trace-idUUID →409 duplicate_execution_id(send a fresh UUID or omit the header). parallelfan-out has no engine-side width cap (Promise.all); throughput is bounded by the downstream agent service, not the engine.400 invalid_workflowfor a malformed CNL document;404 unknown_agent/403 agent_not_activewhen a task names an unknown or non-active agent (fenced before the run opens).- Every response carries
x-request-id(= the trace id); async/YAML/markdown responses also carryX-Execution-Id. See Traces & observability.
11. MCP plane
The MCP plane is bidirectional: inbound, the platform is an MCP server you point a client at;
outbound, you connect the platform to other MCP servers and call their tools during a run.
Both use an ozk_ key (Authentication). Base URL is
https://api.plungeai.com. Source: orchestration/api-gateway/routes/mcp.ts,
orchestration/mcp-gateway/, orchestration/mcp-executor/.
11.1 Inbound — the platform as an MCP server
POST /v1/mcp is a Streamable-HTTP MCP endpoint that proxies (signed) to the mcp-gateway. It is
stateless — no Mcp-Session-Id is required (send one and it is forwarded). Authenticate with an
ozk_ bearer. It speaks plain JSON-RPC: initialize, then tools/list / tools/call. Protocol
version 2025-06-18 (orchestration/mcp-gateway/USER-GUIDE.md).
https://mcp.plungeai.com/mcp is the same MCP server (the gateway's own hostname);
https://api.plungeai.com/v1/mcp is the One API route to it with an ozk_ bearer. Use whichever
your client makes easiest.
A convenience listing without JSON-RPC:
curl -s "https://api.plungeai.com/v1/mcp/tools" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Accept: application/json'
The same over JSON-RPC (what an MCP client sends):
curl -s "https://api.plungeai.com/v1/mcp" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
The 20 plungeai_* tools
tools/list returns 20 tools (live-verified 2026-09-18), in this order
(orchestration/mcp-gateway/server.ts):
| Tool | One line |
|---|---|
plungeai_execute_workflow |
run a CNL workflow (saved id or ad-hoc YAML); mode:"async" for long runs |
plungeai_execute_agent |
run one prompt-driven agent (e.g. llm-agent, skill-agent), optionally with a session_id |
plungeai_get_tool_contract |
the exact invocation contract for one agent (params schema, operations, live credential status) |
plungeai_execute_tool |
run one structured tool-agent with typed {agent_id, operation?, params} |
plungeai_run_mission |
run a bounded autonomous mission (goal + optional fence/iteration cap); async by default |
plungeai_learn |
manage your private skill library — learn / list / forget |
plungeai_list_agents |
discover platform building-block agents (the capability registry) |
plungeai_list_workflows |
list your saved workflows (what users call "my agents") |
plungeai_get_result |
fetch one execution's result (full conversation thread for chat runs) |
plungeai_get_workflow_status |
check an execution's status; returns a continuation when paused |
plungeai_workflow |
manage workflows — create/get/update/delete/versions |
plungeai_executions |
browse execution history — list/get/output/conversation/delete |
plungeai_followup |
ask a follow-up about a completed execution |
plungeai_continue |
continue a paused run — answer (message) or approve (approve:true) |
plungeai_chat |
persistent chat with the platform assistant — send/new/list_sessions/history |
plungeai_build_workflow |
generate or refine a saved workflow from a goal |
plungeai_memory |
long-term memory scoped to you — recall/remember/search_runs/get_run |
plungeai_templates |
workflow templates — list/get/use |
plungeai_schedule |
scheduled (cron) jobs — stats/list/get/create/update/pause/resume/delete/run_now/runs |
plungeai_whoami |
the authenticated identity — user id, tier, key label, rate-limit window |
Connecting a client
- Claude Code (one command):
claude mcp add --transport http plungeai https://mcp.plungeai.com/mcp --header "Authorization: Bearer <ozk key>" - Cursor / VS Code / JSON-configured clients — an
.mcp.jsonserver entry with"url": "https://mcp.plungeai.com/mcp"and anAuthorization: Bearerheader. - Claude Desktop / claude.ai — the connector UI cannot set a bearer header, so bridge with
mcp-remote https://mcp.plungeai.com/mcp --header "Authorization: Bearer <ozk key>". - Any client — point it at either URL; it is plain JSON-RPC over Streamable HTTP. The install
page
https://mcp.plungeai.com/installgenerates ready-made config.
11.2 Outbound — MCP runs against other servers
The outbound surface connects the platform to registry MCP servers, lists their tools, calls them,
and tears the run down. Secrets are resolved server-side in the mcp-executor (never in your
request): ${VAR} → a platform secret; ${{ credentials.<provider> }} → the acting user's BYOK
key / OAuth token, a miss becoming a structured needs_connection (orchestration/mcp-executor/CLAUDE.md).
Each run is a per-tenant Durable Object keyed customerUuid:runId; background runs are held in
resumable ~10-minute legs (the Ten-Minute Relay). Discover connectable servers with
GET /v1/discovery/search?kind=mcp-servers (Discovery).
| Route | Body | Returns |
|---|---|---|
POST /v1/mcp/runs |
{ "server_ids": ["<id>", …] } (non-empty string array) |
201 { run_id, tools, connected, failed, warnings, skipped } |
GET /v1/mcp/runs/:id/tools |
— | { tools, count } |
POST /v1/mcp/runs/:id/call |
{ "name": "mcp__<server>__<tool>", "args"?: {…}, "timeout_ms"?: n } |
{ content } |
DELETE /v1/mcp/runs/:id |
— | { success: true } |
Notes:
server_idsare registrymcp-servercatalog ids — not arbitrary URLs; the executor resolves each server's manifest and credentials. Nourl/authis accepted in the body.- Tool names are namespaced
mcp__<serverId>__<tool>so the loop's fence and per-tool permissions apply to MCP tools like native ones. - A server that just failed is skipped for the next run for 60s (per-tenant cooldown →
skipped). If none connect →502 no_servers_connectedwithfailed/warnings/skipped. A tool call that fails →502 tool_call_failed; a missingname→400 invalid_body. - Always
DELETEthe run when done (the DO also self-cleans a stale run).
A full cycle against the credential-free cloudflare-docs server — create, list tools, delete:
RID=$(curl -s "https://api.plungeai.com/v1/mcp/runs" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"server_ids":["cloudflare-docs"]}' \
| sed -n 's/.*"run_id":"\([^"]*\)".*/\1/p')
echo "run_id=$RID"
curl -s "https://api.plungeai.com/v1/mcp/runs/$RID/tools" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
echo
curl -s -X DELETE "https://api.plungeai.com/v1/mcp/runs/$RID" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
To call a tool on the run (not run here — it does real work):
# not run here
curl -s "https://api.plungeai.com/v1/mcp/runs/<run_id>/call" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"name":"mcp__cloudflare-docs__search_cloudflare_documentation","args":{"query":"durable objects"}}'
11.3 MCP servers inside a harness mission
Outbound runs are the manual surface. Inside a bounded mission, list the servers on the task's
mcp: field and the loop host connects them for the run (through the same mcp-executor), exposing
their tools to the agent under the fence. See
Skills, plugins & capability injection for the mcp:
field and a full type: harness example, and the Workflows plane for running
it. In-loop MCP is the inbound path for our agents; the POST /v1/mcp endpoint above is the
outbound control plane for external clients — the two never cross.
12. Traces & observability
Every One API response is traceable. This chapter covers the trace id, the GET /v1/traces/:id
read, execution history and cost, the response headers, and what is not observable through the
API today. Base URL is https://api.plungeai.com; reads use an ozk_ key
(Authentication). Source: orchestration/api-gateway/routes/traces.ts,
orchestration/api-gateway/lib/execution-row.ts.
12.1 The trace id
Every response carries x-request-id — this is the trace id. On execution calls it also equals
the run's workflow_id / execution_id, and it is echoed in the body as request_id
(live-verified 2026-09-18: x-request-id, x-execution-id, workflow_id and request_id were the
same UUID for one agent execute). So you can trace a run from any of them.
You may also supply the trace id: send x-trace-id: <uuid> and the run adopts it (a replayed
UUID that already ran → 409 duplicate_execution_id, see Errors).
12.2 Response headers
| Header | On | Meaning |
|---|---|---|
x-request-id |
every response | the trace id |
x-execution-id |
execution responses | the run id (= x-request-id for a run) |
x-error-code |
error responses | the machine error code (mirrors error.code) |
server-timing |
every response | total;dur=<ms>;desc="Total Response Time" |
x-content-type-options |
every response | nosniff |
No x-ocean-* headers are emitted today. Rate-limit state is not returned as a header — read it with
plungeai_whoami (the MCP plane) or see Authentication.
12.3 GET /v1/traces/:id
Reads one trace by id and joins two telemetry tables (both on ocean-telemetry via TELEMETRY_DB,
per the root CLAUDE.md DB map):
{ "trace_id": "<id>", "spans": [ … ], "gateway_requests": [ … ] }
spans— rows ofexecution_trace_spans(columns:id, trace_id, workflow_id, task_id, ts, type, agent, status, duration_ms, payload_ref, payload_inline), ordered by time, up to 500. These are written bytrace-serviceoff the async trace-events queue, so they can lag a just-finished run or be empty on a short one.gateway_requests— rows ofapi_gateway_requests(columns:id, trace_id, plane, route, user_id, status, upstream, duration_ms, cost_usd, created_at): the router's own request log — one row per API call on this trace, including the plane, route, HTTP status and per-request cost.
Because spans are queue-written, GET /v1/traces/:id for an unknown id and for a run whose
spans have not landed yet both return 200 with empty arrays — the endpoint does not distinguish
"no such trace" from "no telemetry yet" (there is no 404 on this route).
Execute, then read the trace by its request_id (the trace id round-trips):
RESP=$(curl -s "https://api.plungeai.com/v1/agents/llm-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"prompt":"Reply with only: pong","max_tokens":24}')
RID=$(printf '%s' "$RESP" | sed -n 's/.*"request_id":"\([^"]*\)".*/\1/p')
echo "trace=$RID"
sleep 3
TRACE=$(curl -s "https://api.plungeai.com/v1/traces/$RID" \
-H "Authorization: Bearer $PLUNGE_API_KEY")
echo "$TRACE"
echo "$TRACE" | grep -q "\"trace_id\":\"$RID\"" && echo TRACE_MATCH_OK
An unknown trace id — 200 with empty arrays, not a 404:
curl -s "https://api.plungeai.com/v1/traces/00000000-0000-4000-8000-000000000000" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
12.4 Execution history & cost
Every agent and workflow run opens a workflow_executions row on ocean-db (DB), source: 'api'
(lib/execution-row.ts). The row carries status (running → completed / failed), input,
workflow_name, duration, end_time, execution_summary (the per-task durations the pricer
reads) and, once priced, the run cost. It is closed from the engine's response after the run; a run
the platform cannot record does not run "for free".
Where a developer sees these rows:
- MCP plane —
plungeai_executions(list/get/output/conversation) andplungeai_get_resultreturn your run history and results as user-ready markdown (MCP plane). - Studio — your runs appear in the run history and per-run views.
- Dashboard —
dashboard.plungeai.comsurfaces run status and cost roll-ups.
Cost is computed asynchronously by the workflow-cost worker from execution_summary; a run whose
summary was preserved is priced (an empty-completion or retrieval fault is flipped to failed
without wiping the summary, so the reasoning tokens it spent are still metered).
12.5 What is not observable via the API today
- There is no public per-token or per-span streaming metrics endpoint; live progress comes from the SSE streams (Streaming, and the workflows event vocabulary in Workflows plane).
GET /v1/traces/:idspansdepend on async queue processing — treat an emptyspansarray as "not landed yet", and usegateway_requests(written inline) plus the execution row for the authoritative status.- There is no
404for an unknown trace (12.3), and no dedicated cost endpoint on this plane — read cost from the execution row via MCP / Studio / Dashboard (12.4). - Trace reads are scoped to the authenticated user; you cannot read another tenant's trace.
13. Errors, limits & headers
The platform has two error envelopes. The execution planes (tools, agents, workflows, MCP, discovery, traces) use the router envelope; the models plane (/v1/chat/completions, /v1/embeddings, /v1/models) uses the OpenAI-shaped envelope from the inference gateway. Branch on the HTTP status and, on the execution planes, the X-Error-Code header — not on any field's type.
13.1 The two envelopes
Execution planes (orchestration/api-gateway/lib/respond.ts, lib/format.ts):
{ "error": { "code": "unknown_agent", "message": "No such agent: foo", "…": "extra fields" } }
- Header
X-Error-Code: <code>is always set — clients can branch on it without parsing the body. - Header
x-request-idechoes the server-minted id. - The body is format-negotiated:
?format=/body.format/Accept/ atext/yamlcontent-type mirror selectjson(default),yaml,markdown, ortext. An unknown explicit format →400 invalid_format(as JSON).code+messagesurvive every format; extra fields survivejson/yaml.
Models plane (inference/gateway/src/errors.ts) — OpenAI shape, no X-Error-Code:
{ "error": { "message": "Invalid API key", "type": "authentication_error", "code": "invalid_api_key", "request_id": "…" } }
codeis a string,typegroups it,request_idis inside the body. Response headers carryx-request-id(andx-correlation-idwhen you sent anx-request-id), neverX-Error-Code.
13.2 Execution-plane error codes
| HTTP | code |
When | Do |
|---|---|---|---|
| 400 | invalid_json / invalid_body / invalid_request |
Body is not valid JSON / not an object | Fix the body |
| 400 | invalid_format |
?format=/body.format is not json|yaml|markdown|text |
Use a known format |
| 400 | missing_prompt |
Agent execute without a prompt/input string |
Send a prompt |
| 400 | missing_workflow |
Workflow execute without a workflow |
Send YAML/JSON workflow |
| 400 | invalid_workflow |
Engine rejected the workflow (structural) | Fix the CNL |
| 401 | unauthorized |
Missing/invalid/revoked ozk_ key |
Send a valid key (Auth) |
| 401 | invalid_signature |
Internal HMAC identity failed | Service-to-service only |
| 403 | refused |
Trust fence — agent/tool refused (13.5) | Do not retry |
| 404 | not_found |
No route for method+path | Check the path |
| 404 | unknown_agent |
No such (or inactive) agent id | Discover a valid id (Discovery) |
| 404 | unknown_tool |
No such tool id | Discover a valid id |
| 404 | workflow_not_found |
Saved workflow id not in the engine KV | Check the id/owner |
| 404 | not_ready |
Result not stored yet (still running) | Poll again |
| 409 | approval_required |
Trust fence — human approval pending (13.5) | Approve, then retry |
| 409 | execution_failed |
Result route: the run failed | Read the message |
| 409 | duplicate_execution_id |
Replayed x-trace-id UUID hit the run PK |
Send a fresh UUID or omit it |
| 413 | payload_too_large |
Body over the size cap (13.6) | Shrink the body |
| 422 | invalid_params |
Outcome needs_input — agent needs different args |
Fix the params |
| 422 | unknown_model |
Pinned model absent/inactive in the catalog | Discover a model |
| 422 | empty_completion |
Run completed but produced no visible content | Raise max_tokens (≥64) |
| 424 | connection_required |
Outcome needs_connection — no connected account |
Connect the account (Connectors) |
| 424 | credential_required |
Outcome needs_api_key — no API key for a connector |
Add the key |
| 429 | rate_limited |
Tier RPM/RPD or failed-auth cap (13.4) | Honour Retry-After |
| 500 | config_error |
Server misconfiguration | Report with x-request-id |
| 500 | internal_error |
Unhandled router error | Report with x-request-id |
| 502 | engine_error |
Engine failed (untagged) | Retry; report if persistent |
| 502 | upstream_error |
Upstream service/storage error | Retry with backoff |
| 502 | tool_call_failed |
Tool execution failed | Read the message |
| 502 | no_servers_connected |
MCP: no server connected | Connect a server |
| 502 | result_unavailable |
Run finished but the result could not be retrieved | Retry the result route |
| 503 | agent_unavailable |
Outcome unavailable — agent temporarily down |
Retry later |
| 503 | provider_unconfigured |
Chosen provider has no key on this platform | Choose another provider |
| 503 | money_plane_unavailable |
Model routes not staged on this tier | Call gateway.plungeai.com directly |
13.3 Outcome → HTTP mapping
Execute routes classify an agent/tool outcome (routes/_util.ts outcomeStatus). A success: false engine result tagged with an outcome remaps to a caller-actionable status; an untagged failure stays 502 engine_error.
| Outcome | HTTP | code |
|---|---|---|
needs_input |
422 | invalid_params |
needs_connection |
424 | connection_required |
needs_api_key |
424 | credential_required |
unavailable |
503 | agent_unavailable |
error (or untagged) |
502 | execution_failed / engine_error |
needs_approval |
409 | approval_required (13.5) |
13.4 Rate limits
Execution planes (orchestration/mcp-gateway/rate-limiter.ts, a Durable Object; enforced in lib/auth.ts). Counted on POST executions only — catalog GETs are free. Per tier:
| Tier | per minute | per day |
|---|---|---|
free |
30 | 1,000 |
pro (default) |
100 | 10,000 |
enterprise |
300 | 100,000 |
A key may carry a per-key rate_limit override (raises the per-minute cap). Failed authentications are metered per client IP (auth-fail: 20/min, 500/day) to bound key brute-force. On a limit hit: 429 rate_limited with a Retry-After header (seconds).
Money plane (inference/gateway/src/auth/rate-limit.ts). Per key, atomic per-minute window (RateLimiterDO, KV soft-cap fallback): default 600 req/min (RATE_LIMIT_RPM_DEFAULT) → 429 rate_limit_exceeded. Separate from billing caps: 429 insufficient_quota (monthly limit / trial allowance), 402 insufficient_balance (empty wallet), 429 spend_cap_exceeded (guardrail) — see 3.10, 3.13.
13.5 Trust fence
Two statuses gate risky actions:
403 refused— the agent/tool refused the action outright (policy). Do not retry unchanged.409 approval_required— a human must approve before the action runs. Approve out-of-band, then re-issue the request.
13.6 Limits: body caps, timeouts
- Body cap (execution planes):
MAX_REQUEST_SIZE, default 1 MiB (routes/_util.ts—1_048_576bytes). Enforced againstContent-Lengthfirst, then streamed; over →413 payload_too_large. The models plane relies on the provider/runtime body limits. - Timeouts: there is no gateway-imposed request timeout beyond the Cloudflare Workers runtime and each upstream's own. Streams have no idle timeout on the money/workflows plane; the agents plane sends
: OCEAN PROCESSINGkeepalives (4.3). - Storage errors are scrubbed: a D1/SQLite failure surfaces as
502 upstream_error"storage error" (the raw error is logged, never leaked); internal service-binding names are redacted from messages.
13.7 Retry guidance
- 429 — back off and honour
Retry-After. Distinguishrate_limited/rate_limit_exceeded(throttle) frominsufficient_quota/spend_cap_exceeded(billing — retrying won't help until the window resets or billing changes). - 5xx (
502/503) — retry with exponential backoff; the models plane already retries429/5xxonce per candidate and fails over (3.3). - 4xx other than 429 — do not retry unchanged; fix the request.
needs_*outcomes (422/424) are yours to resolve (params, connection, key). - Always include the
x-request-idwhen reporting an error.
13.8 Response headers
| Header | Plane | Meaning |
|---|---|---|
x-request-id |
both | Server-minted request id. Send your own x-request-id to have it echoed. |
X-Error-Code |
execution (errors) | The error code, for header-only branching. |
X-Execution-Id |
execution | The run id on results, streams, and async pointers. |
x-correlation-id |
models | Echo of your x-request-id when supplied. |
x-cache |
models | hit / miss on cache-eligible chat requests (3.11). |
Server-Timing |
execution | total;dur=<ms> timing. |
X-Content-Type-Options |
execution | nosniff on every router response. |
Retry-After |
both | Seconds to wait after a 429. |
Access-Control-Allow-Origin (+ CORS) |
both | *; both planes answer OPTIONS preflight. |
Content-Type |
both | application/json | text/event-stream | negotiated text/yaml|text/markdown|text/plain. |
13.9 Verified examples
Unauthenticated execution-plane request → 401 unauthorized with the x-error-code header:
curl -is "$API_BASE/v1/agents"
Unknown route → 404 not_found, header x-error-code: not_found, plus x-request-id:
curl -is "$API_BASE/v1/nope"
Agent execute without a prompt → 400 missing_prompt:
curl -s "$API_BASE/v1/agents/llm-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" -d '{}'
Unknown agent id → 404 unknown_agent (the trust/fence pre-dispatch check):
curl -s "$API_BASE/v1/agents/zzz-not-an-agent/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" -d '{"prompt":"hi"}'
14. Skills, plugins & capability injection
The platform does not ship one agent per behaviour. It ships one loop agent — the harness
runtime (universal-agent / agentic-agent, see Agents plane) — into which
capabilities are injected per run. Capability injection is the six kinds below: skills,
experts, persona, backgrounds, plugins, and MCP servers. You declare them on a type: harness task
and the loop host resolves them in one parallel pass before the run
(core/core-base/pack-enrich.ts). Base URL is https://api.plungeai.com; runs use an ozk_ key
(Authentication).
14.1 The six kinds
| Kind | What it injects | Where it comes from | Eager limit | Field |
|---|---|---|---|---|
| backgrounds | always-on ambient context, prepended first | registry card prompt_format |
3 | backgrounds: [id, …] |
| persona | identity / voice, prepended | persona-memory | 1 (one voice per run) | persona: id (aliases digital_twin, personas[0]) |
| experts | labelled ## EXPERT: <id> sections |
expert-memory | 3 | experts: [id, …] |
| skills | an ## ADDITIONAL INSTRUCTIONS body |
skill-memory, else the registry skill card | 5 | skills: [id, …] |
| plugins | a bundled SKILL.md index + script index (never MCP ids) | plugin-memory | — | plugins: [id, …] |
| mcp | remote MCP tools, connected for the run and exposed under the fence | mcp-executor (secrets resolved server-side) | — | mcp: [id, …] |
Injection order into the system prompt: backgrounds → persona → experts, then the skill body
under ## ADDITIONAL INSTRUCTIONS. Plugin scripts become available to the run_python tool;
plugin skills are advertised by name + description and pulled on demand with load_skill. MCP
ids are connected by the loop host through the mcp-executor (the same machinery as the
MCP plane outbound runs) and their tools appear as mcp__<server>__<tool> under the
mission's fence.
14.2 Limits & degradation
- Total eager budget across backgrounds + persona + experts + skills is
EAGER_BUDGET_BYTES= 24 KiB (pack-enrich.ts). Over budget, later items are listed by id + description instead of injected — the agent pulls them on demand viaload_skill(progressive disclosure). - Per-kind caps:
skills5,experts3,backgrounds3 (MAX_EAGER). Ids beyond the cap are deferred toload_skillthe same way. - Fail-soft: a missing or unresolvable capability degrades to a warning lane, it does not
fail the run. A missing MCP credential surfaces as a structured
needs_connection:<provider>. - Arrays replace, they don't union — the most specific layer wins per key (14.4).
14.3 The skill catalog
Skill sources live in skills/<id>/SKILL.md; the runtime injects the published registry skill
card. The live catalog is the source of truth — discover it with
GET /v1/discovery/search?kind=skills (Discovery). The ids on disk today
(skills/):
| id | one line |
|---|---|
web-search |
search the web and cite sources |
report |
structure findings into a clean report |
refactor |
refactor code toward a smaller, clearer shape |
code-review |
review a diff for correctness and simplification |
debug |
systematic root-cause debugging |
file-ops |
read/write/search files in the workspace |
prd |
draft a product requirements document |
qa |
test-and-verify discipline |
thinking |
structured reasoning before acting |
workflow |
author a CNL workflow |
biotech |
biotech-domain research helper |
curl -s "https://api.plungeai.com/v1/discovery/search?kind=skills&q=web-search&limit=5" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
Plugins are bundled SKILL.md indexes + runnable scripts (not MCP servers). Discover them with
kind=plugins; MCP servers (the mcp: kind) with kind=mcp-servers:
curl -s "https://api.plungeai.com/v1/discovery/search?kind=mcp-servers&limit=5" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
The discovery kinds
personasandexpertsexist but returned 0 cards live-verified 2026-09-18 — persona/expert text is resolved from the persona-memory / expert-memory services at run time, not from a discovery catalog.
14.4 Pre-built agent cards — mission_ref / pack
A pre-built agent is a registry agent card whose prompt_format holds one portable markdown
document: YAML frontmatter (a typed capability pack) plus a body that is the system prompt,
compiled on read by core/core-base/agent-markdown.ts. Reference it instead of writing the mission
inline:
- type: harness
mission_ref: research-analyst # canonical; `pack:` is an alias; a single-token `mission:` also works
Frontmatter keys the compiler reads: name, description, model, effort, tools, skills,
experts, persona, backgrounds, plugins, mcp, agents (named sub-agents), permissions,
success_criteria — plus the body. Unknown keys are ignored with a warning (forward-compatible);
ownership/visibility are stamped server-side, never read from frontmatter. Merge order (per key,
last wins): pre-built card → workflow root → task; arrays replace. So a card can define the base
mission and the task can override model or clear skills: [].
14.5 How to run it
Workflows plane (recommended) — a type: harness task carries the mission and its capability
fields; run it inline or save it (see Workflows plane). A full example:
workflow:
name: sourced-brief
tasks:
- type: harness
id: brief
agent: universal-agent
goal: "Write a one-paragraph brief on the latest Anthropic model; every claim a source URL."
mission: "You are a research analyst. Produce sourced, factual briefs."
allowed_tools: [web_search, web_fetch, task_complete]
success_criteria:
- "Every factual claim is followed by a source URL."
max_iterations: 8
skills: [web-search, report]
experts: [python-pro]
persona: analyst
backgrounds: [acme-corp]
plugins: [finance-bundle]
mcp: [brave-search]
allowed_tools is the fence (fail-closed — a tool not listed never executes); max_iterations
(alias max_turns) is the loop cap; effort (quick/standard/deep) sets a default cap when no
explicit number is given.
Agents plane — POST /v1/agents/:id/execute forwards only persona among the capability
fields (routes/agents.ts); skills, experts, backgrounds, plugins, mcp, goal,
allowed_tools are ignored there. To inject skills/plugins/MCP, use the type: harness task on
the workflows plane, or the MCP plane plungeai_run_mission.
A minimal harness run with a real skill injected, capped at 2 iterations:
curl -s "https://api.plungeai.com/v1/workflows/execute" \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"workflow":{"name":"cap-demo","tasks":[{"type":"harness","id":"h1","agent":"universal-agent","goal":"Reply with the single word pong, then call task_complete.","mission":"You are a terse assistant. Answer in one word.","allowed_tools":["task_complete"],"max_iterations":2,"skills":["report"]}]}}'
14.6 Seeing what was injected
The enrichment pass reports what it resolved and what degraded (pack-enrich.ts): capabilities that
could not be resolved appear as warning lanes, and those dropped for the eager budget/cap are
reported as deferred (loadable with load_skill). These surface in the run's result metadata and
in the trace spans (Traces & observability); a missing skill shows up as
a warning rather than silently absent behaviour. For the full runtime model — tool fence, memory,
delegation, recursion guard — see orchestration/HARNESS-README.md.
15. SDKs & clients
You do not need an Ocean-specific SDK to use the One API. The models plane is OpenAI-wire, so every OpenAI-compatible client works with a base-URL swap; the execution planes are plain HTTP+JSON and have a small typed TypeScript client. This chapter lists the options, shortest path first.
Keys throughout: $PLUNGE_API_KEY = ozk_ (execution planes), $PLUNGE_MODEL_KEY =
sk-ocean- (models plane). See Authentication & keys.
OpenAI SDK (models plane)
Point the official OpenAI SDK at https://api.plungeai.com/v1 with your sk-ocean- key.
This covers /v1/chat/completions, /v1/embeddings, and /v1/models.
Python:
from openai import OpenAI
client = OpenAI(
api_key="<your sk-ocean- key>",
base_url="https://api.plungeai.com/v1",
default_headers={"x-trace-id": "00000000-0000-4000-8000-000000000000"}, # optional UUID
)
resp = client.chat.completions.create(
model="anthropic/claude-sonnet-4-6",
messages=[{"role": "user", "content": "Reply with exactly the word: pong"}],
max_tokens=16,
)
TypeScript:
import OpenAI from 'openai'
const client = new OpenAI({
apiKey: process.env.PLUNGE_MODEL_KEY,
baseURL: 'https://api.plungeai.com/v1',
defaultHeaders: { 'x-trace-id': crypto.randomUUID() }, // optional
})
Ocean-only fields (models[], sort, @preset/<slug>) pass through as extra body params —
use the SDK's extra_body (Python) or just add them to the request object (TypeScript). See
Models plane.
@plungeai/one-api (execution planes, TypeScript)
A thin typed client generated from GET /v1/openapi.json. It wraps
openapi-fetch; every method maps 1:1 to a route.
Source: sdk/one-api-client/.
npm install @plungeai/one-api
The only entry point is createOneApi({ apiKey, baseUrl? }), which returns a client with
.GET, .POST, and .DELETE. Three real calls (method names verified against
sdk/one-api-client/src/index.ts and its README):
import { createOneApi } from '@plungeai/one-api'
const client = createOneApi({ apiKey: process.env.PLUNGE_API_KEY! })
// 1) GET /v1/tools — data.count and data.tools are typed
const { data, error } = await client.GET('/v1/tools')
if (data) console.log(data.count, data.tools.length)
// 2) POST /v1/agents/{id}/execute — path param and body both typed
const { data: exec } = await client.POST('/v1/agents/{id}/execute', {
params: { path: { id: 'llm-agent' } },
body: { prompt: 'Reply with exactly the word: pong', sync: true, max_tokens: 32 },
})
console.log(exec?.content) // "pong"
// 3) GET /v1/discovery/search — query params are typed
const { data: found } = await client.GET('/v1/discovery/search', {
params: { query: { q: 'web search', kind: 'agents', limit: 3 } },
})
The paths and components types are exported for advanced use. Regenerate the types after
any spec change with npm run regen — never hand-edit src/schema.d.ts. The package targets
One API 2.1.0. (Note: as of 2026-09-18 the package is publish-ready but not yet on npm;
until it ships, consume it from the in-repo path or npm pack.)
OpenAI-compatible frameworks (models plane)
Any framework that accepts an OpenAI base URL works. Set base URL
https://api.plungeai.com/v1 and the sk-ocean- key.
LiteLLM:
import litellm
resp = litellm.completion(
model="openai/anthropic/claude-sonnet-4-6", # openai/ tells LiteLLM to use the OpenAI wire
api_base="https://api.plungeai.com/v1",
api_key="<your sk-ocean- key>",
messages=[{"role": "user", "content": "pong?"}],
)
Vercel AI SDK:
import { createOpenAI } from '@ai-sdk/openai'
const ocean = createOpenAI({
baseURL: 'https://api.plungeai.com/v1',
apiKey: process.env.PLUNGE_MODEL_KEY,
})
const model = ocean('anthropic/claude-sonnet-4-6')
LangChain (Python):
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="anthropic/claude-sonnet-4-6",
base_url="https://api.plungeai.com/v1",
api_key="<your sk-ocean- key>",
)
MCP clients (the platform as an MCP server)
The platform is itself an MCP server. Point any MCP client at the inbound Streamable-HTTP
endpoint with your ozk_ key. There are two equivalent URLs (both live-verified, both
return the same tool table): https://api.plungeai.com/v1/mcp (the One API's signed
pass-through) and https://mcp.plungeai.com/mcp (the MCP gateway directly).
Claude Code:
claude mcp add --transport http plungeai https://api.plungeai.com/v1/mcp \
--header "Authorization: Bearer $PLUNGE_API_KEY"
Claude Desktop / other clients that speak mcp-remote:
{
"mcpServers": {
"plungeai": {
"command": "npx",
"args": ["mcp-remote", "https://api.plungeai.com/v1/mcp",
"--header", "Authorization: Bearer ${PLUNGE_API_KEY}"]
}
}
}
You can confirm the tool table over plain curl (JSON-RPC tools/list) — live-verified:
curl -s -X POST https://api.plungeai.com/v1/mcp \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
See MCP plane for inbound tools and outbound MCP runs.
Legacy SDKs
The @ocean-platform/external-sdk family predates the One API router. They still work
(they target the legacy /v1/execute alias, which the router maps to the workflow plane)
but are deprecated — prefer the OpenAI SDK (models) and @plungeai/one-api or plain HTTP
(execution planes). All four use an ozk_ key and expose callAgent / executeWorkflow /
executeYAML / executeStream.
| Package | Language | Install | Source |
|---|---|---|---|
@ocean-platform/external-sdk |
TypeScript | npm install @ocean-platform/external-sdk |
sdk/ocean-external-sdk/ |
ocean-external-sdk (Python) |
Python | pip install ocean-external-sdk |
sdk/ocean-external-sdk-python/ |
ocean-external-sdk-go |
Go | go get github.com/ocean-platform/ocean-external-sdk-go/ocean |
sdk/ocean-external-sdk-go/ |
ocean-external-sdk (Rust) |
Rust | ocean-external-sdk = "1.0" |
sdk/ocean-external-sdk-rust/ |
curl conventions used in this guide
- Base URL is always
https://api.plungeai.com. - Auth is
-H "Authorization: Bearer $PLUNGE_API_KEY"(or$PLUNGE_MODEL_KEYon the models plane);-H "X-API-Key: $PLUNGE_API_KEY"also works on the execution planes. - JSON bodies use
-H "Content-Type: application/json"; workflow YAML can be posted with-H "Content-Type: text/yaml" --data-binary @workflow.yaml. - Streaming examples add
-Nso curl does not buffer the SSE. - Correlation: add
-H "x-trace-id: <a UUID>"to tie several calls into one trace.
A plain execution-plane list call (the shape every ozk_ GET follows) is live-verified:
curl -s "https://api.plungeai.com/v1/agents?limit=3" \
-H "Authorization: Bearer $PLUNGE_API_KEY"