API contract
OpenAI-compatible inference — endpoints, auth, streaming, tool calls, errors, billing.
Drop-in OpenAI-compatible inference for frontier OSS LLMs behind one endpoint, one API key, pay-as-you-go billing.
- Base URL
https://<your-org>.dynoyard.app/v1 - Auth
Authorization: Bearer sk-dyno-… - Format identical to OpenAI’s
/v1/chat/completionsschema. Any OpenAI SDK works by swappingbase_url+ key.
Each org has one persistent endpoint and as many API keys as you want
(prod / staging / per-customer / etc.). All keys see the same model
catalog; switch models per request via the body’s model field.
1. Available models
The active catalog evolves as new frontier OSS models ship; rather
than freeze a list here, the canonical source is your org’s
GET /v1/models endpoint or the public pricing page.
Current families (mid-2026): Kimi (Moonshot), MiMo (Xiaomi),
DeepSeek, Qwen, GLM. Each is exposed via a stable dynoyard-side
slug; check /v1/models for the canonical ids your org can call.
Listing models
GET /v1/models returns the catalog enabled on your org. New models
roll out by appearing in this list — no client code change needed.
Response mirrors OpenRouter’s shape so catalog-aware tooling (LiteLLM,
openrouter-go) reads pricing + context length without translation.
{
"object": "list",
"data": [
{
"id": "kimi-k2-thinking",
"object": "model",
"owned_by": "dynoyard",
"created": 1747353600,
"name": "Kimi K2 Thinking",
"description": "Moonshot Kimi K2 — thinking mode, 262K context.",
"context_length": 262144,
"capabilities": ["tools", "streaming", "reasoning"],
"pricing": {
"prompt": "0.60000",
"completion": "2.50000",
"unit": "USD per 1M tokens"
}
}
]
}
Response is cached briefly per org — safe to call on app startup.
2. Chat completions
POST /v1/chat/completions
Standard OpenAI body shape. Required:
{
"model": "kimi-k2-thinking",
"messages": [
{"role": "user", "content": "Hello"}
]
}
Optional fields honored: temperature, top_p, max_tokens,
stop, stream, stream_options, tools, tool_choice,
response_format, seed, frequency_penalty, presence_penalty,
logit_bias, user.
Streaming
Set stream: true. Server-Sent Events; each data: line is a JSON
delta in OpenAI’s standard streaming shape. Final non-[DONE] event
includes the usage block plus a Dynoyard cost block (see §4).
POST /v1/chat/completions
{
"model": "mimo-v2-5-pro",
"messages": [{"role": "user", "content": "Stream me"}],
"stream": true
}
Tool calls
OpenAI standard tools[] + tool_choice fields. The model emits
tool_calls on assistant messages; you echo tool_calls + tool
role responses on the next turn exactly like the OpenAI API.
{
"model": "kimi-k2-thinking",
"messages": [],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}
],
"tool_choice": "auto"
}
Thinking-mode (reasoning models)
Reasoning-capable models (Kimi K2 Thinking, DeepSeek R1, MiMo V2.5
Pro) emit a reasoning_content field on each assistant message
alongside content. It’s the model’s chain of thought. Use it for
telemetry / debugging UI, ignore it for plain chat.
Multi-turn conversations: you may echo reasoning_content back in
prior assistant messages or omit it; the server pads missing values
internally so you never have to worry about the “must be passed
back” upstream error.
3. Authentication
Per-org API keys, named at creation. Each key shows once as
sk-dyno-<random>; only the hash lands in our DB.
Authorization: Bearer sk-dyno-db8ef1574dd17df09d7d1f17716029affad6dd225aca4685
Create / list / revoke / rotate keys on the dashboard’s Keys page. Old key invalidates within seconds of revocation or rotation.
Optional per-key restrictions:
- Allowed models — restrict a key to a subset of the org’s
enabled models (e.g. staging key only sees
mimo-v2-5-pro). - Monthly cap — coming soon; hard cap in USD; key returns 429 past threshold.
4. Cost reporting
Every successful chat-completion response surfaces the dollar amount it cost the caller. Two channels, both populated by default — no opt-in flag required.
usage.cost (OpenRouter-compatible)
Inline field most catalog-aware tooling reads — LiteLLM, openrouter-py, openrouter-js, openrouter-go. Lands on every chat-completion response (streaming + non-streaming).
{
"id": "...",
"model": "mimo-v2-5-pro",
"choices": [],
"usage": {
"prompt_tokens": 8421,
"completion_tokens": 142,
"total_tokens": 8563,
"cost": 0.034210
}
}
Response headers (non-streaming only)
Convenient for shell scripts and log proxies that don’t parse JSON.
Token counts are already in the body’s usage.* block — only the
non-redundant fields get headers:
X-Dynoyard-Cost-USD: 0.034210
X-Dynoyard-Model: mimo-v2-5-pro
For streaming, usage.cost lands in the final SSE event alongside
the rest of the usage block (we set
stream_options.include_usage: true automatically so this fires
unconditionally).
The same number feeds your dashboard’s Spend tile. Customer-side parsing is optional but recommended for any client that meters per-request budget.
5. Error responses
Standard JSON envelope. HTTP status carries the actionable category; the body is for human display + telemetry.
{
"error": {
"message": "Rate limit reached. Retry after a short backoff.",
"type": "rate_limit",
"code": "rate_limited"
}
}
| HTTP | code | When |
|---|---|---|
| 400 | bad_request | Malformed body / missing required field. |
| 400 | model_not_enabled | model field references a model not enabled on this org or this key. |
| 401 | unauthorized | Missing / invalid bearer token, or token revoked. |
| 402 | insufficient_credits | Balance + grace ≤ 0. Top up at dynoyard.app/settings/billing. |
| 403 | org_suspended | Org temporarily suspended by an operator. Contact support. |
| 404 | not_found | Org subdomain doesn’t exist. |
| 429 | rate_limited | Per-key or upstream concurrency cap. Retry with exponential backoff. |
| 503 | no_backend | All backends serving this model are draining / down. Retry shortly. |
| 5xx | upstream_unavailable | Transient upstream issue. Already retried on our side; safe to retry. |
Transient 429 / 5xx are retried server-side with jittered backoff (total budget ~12s) before surfacing the error. You’ll only see these codes after that budget is exhausted.
6. Quickstart — curl
curl https://<your-org>.dynoyard.app/v1/chat/completions \
-H "Authorization: Bearer sk-dyno-..." \
-H "Content-Type: application/json" \
-d '{
"model": "kimi-k2-thinking",
"messages": [{"role": "user", "content": "ship it"}],
"max_tokens": 100
}'
7. Quickstart — OpenAI SDK (Python)
from openai import OpenAI
client = OpenAI(
base_url="https://<your-org>.dynoyard.app/v1",
api_key="sk-dyno-...",
)
# Switch models per request — same client, same key.
for model in ["kimi-k2-thinking", "mimo-v2-5-pro", "deepseek-r1"]:
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "one-liner intro"}],
)
print(model, "→", resp.choices[0].message.content[:80])
print(" cost:", resp.model_dump()["usage"].get("cost"))
8. Quickstart — OpenAI SDK (Node / TypeScript)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://<your-org>.dynoyard.app/v1",
apiKey: process.env.DYNOYARD_API_KEY!,
});
const resp = await client.chat.completions.create({
model: "kimi-k2-thinking",
messages: [{ role: "user", content: "ship it" }],
});
console.log(resp.choices[0].message.content);
// @ts-expect-error — usage.cost is non-standard on the OpenAI SDK type
console.log("cost:", resp.usage?.cost);
9. Quickstart — Streaming (Node)
const stream = await client.chat.completions.create({
model: "mimo-v2-5-pro",
messages: [{ role: "user", content: "stream me" }],
stream: true,
stream_options: { include_usage: true },
});
for await (const event of stream) {
process.stdout.write(event.choices[0]?.delta?.content ?? "");
// @ts-expect-error — usage.cost is non-standard on the OpenAI SDK type
if (event.usage?.cost !== undefined) {
console.log("\ncost:", event.usage.cost);
}
}
10. Management API — Credits
Separate API surface for org admin tasks. Auth via management keys
(sk-dyno-mgmt-…), distinct from inference keys (sk-dyno-…). Mint
them on the dashboard’s Settings page (owner role required).
Management keys can’t authorize inference calls; inference keys
can’t authorize management calls.
Base URL is the management host (not your org subdomain):
https://api.dynoyard.app/v1/management
Check credits
GET /v1/management/credits
curl https://api.dynoyard.app/v1/management/credits \
-H "Authorization: Bearer sk-dyno-mgmt-..."
Response:
{
"credits": {
"balance_usd": 9.97,
"currency": "USD"
},
"usage": {
"spend_24h_usd": 0.0312,
"spend_30d_usd": 0.4827
}
}
balance_usd— prepaid balance in USD. Pay-as-you-go: top up via the dashboard’s Settings → Billing page (/settings/billing). Min top-up is $10. Saved cards unlock auto-topup — set a low-balance threshold + refill amount and we charge your card off-session before you hit empty.spend_24h_usd/spend_30d_usd— rolling spend computed from the same per-model rates the chat-completionusage.costfield uses, so the numbers reconcile to the cent.
How spend lands
Every successful /v1/chat/completions response is metered per
request and debited from your balance idempotently. Balance
updates propagate within a second so the next request sees the
fresh value. A periodic reconciliation pass settles any gap as a
safety net.
402 insufficient_credits
When your balance reaches zero (plus any auto-topup grace), the next request returns HTTP 402:
{
"error": {
"message": "out of credits — top up at https://dynoyard.app/settings/billing",
"type": "insufficient_credits",
"code": "insufficient_credits"
}
}
Auto-topup gives ~one request of grace runway while the off-session charge clears. Manual top-up accounts hit 402 the instant balance reaches zero.
11. Support
- Status / incidents: status.dynoyard.app
- Email: hello@dynoyard.app
- Security: security@dynoyard.app (we respond within 48h)
Dynoyard, 2026. Pricing + rates subject to change with 30 days’ notice.