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.

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:


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"
  }
}
HTTPcodeWhen
400bad_requestMalformed body / missing required field.
400model_not_enabledmodel field references a model not enabled on this org or this key.
401unauthorizedMissing / invalid bearer token, or token revoked.
402insufficient_creditsBalance + grace ≤ 0. Top up at dynoyard.app/settings/billing.
403org_suspendedOrg temporarily suspended by an operator. Contact support.
404not_foundOrg subdomain doesn’t exist.
429rate_limitedPer-key or upstream concurrency cap. Retry with exponential backoff.
503no_backendAll backends serving this model are draining / down. Retry shortly.
5xxupstream_unavailableTransient 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
  }
}

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


Dynoyard, 2026. Pricing + rates subject to change with 30 days’ notice.