# Dawnstack — the complete API reference You are most likely reading this because a developer handed you this URL. Everything needed to call the Dawnstack API is in this one file: the base URL, authentication, every endpoint, the live model catalogue with prices, the error contract, and every page of the human docs. Generated: 2026-08-09T15:00:00Z **The prices below were live at that instant.** They are rendered from the registry on every request, not baked in — but this text is a copy, and a copy ages. `GET https://api.dawnstackai.com/v1/models` is the authority and needs no API key. Re-fetch this URL rather than committing it to a repo. --- ## Start here - **Base URL:** `https://api.dawnstackai.com/v1` - **Auth:** `Authorization: Bearer sk-dawn-...` on every request. Server-side only — `/v1` sends no CORS headers on purpose, because a key in a browser is a key you have given away. - **It is OpenAI-compatible.** Point any OpenAI SDK at the base URL, change the key, change the model id. Request shape, response shape, streaming frames, tool calls and the error envelope are all OpenAI's. No Dawnstack SDK exists and none is needed. - **Balance is prepaid, in US dollars.** There is no credit unit; one was retired on 30 Jul 2026. Costs are reported in micro-dollars, where 1 micros = $0.000001. - **Every response carries `x-request-id`.** A chat completion also returns it as `dawnstack.request_id`, alongside `dawnstack.cost_micros` — the figure of record for what a call cost. Quote a request id when reporting a problem. ### The error envelope Every error from `/v1` has this shape, which is OpenAI's: ```json { "error": { "message": "Insufficient balance: $0.001230 required, $0.000400 available", "type": "insufficient_quota", "param": null, "code": "INSUFFICIENT_BALANCE", "details": { "required_micros": 1230, "required_usd": 0.00123, "available_micros": 400, "available_usd": 0.0004 } } } ``` `type` is OpenAI's vocabulary and is what the SDKs branch on. `code` is ours, stable, and safe to switch on. **An empty balance is 402, not 429** — do not treat it as a rate limit and retry; it needs a top-up. The full status table is in the Errors page below. ### Machine-readable, no key required - `GET https://api.dawnstackai.com/v1/models` — the live catalogue. - `GET https://api.dawnstackai.com/openapi.json` — OpenAPI 3.1, generated from the schemas the API actually validates against. --- ## The catalogue Text models. Prices are per million tokens. | Model | Maker | Context | Input $/M | Output $/M | Can do | | --- | --- | --- | --- | --- | --- | | `llama-3.1-8b` | Meta | 131,072 | $0.03 | $0.06 | tools | | `llama-3.2-3b` | Meta | 80,000 | $0.077 | $0.503 | chat | | `llama-3.3-70b` | Meta | 131,072 | $0.15 | $0.48 | tools | | `mistral-small-3.1-24b` | Mistral AI | 128,000 | $0.112 | $0.3 | tools, json, images | | `gemma-4-26b` | Google | 256,000 | $0.15 | $0.45 | chat | | `gpt-oss-20b` | OpenAI | 131,072 | $0.045 | $0.21 | tools, json | | `gpt-oss-120b` | OpenAI | 131,072 | $0.055 | $0.255 | tools, json | | `qwen3-30b` | Qwen (Alibaba) | 32,768 | $0.077 | $0.503 | chat | | `kimi-k2.6` | Moonshot AI | 262,144 | $1.425 | $6 | chat | | `deepseek-v3.2` | DeepSeek | 163,840 | $0.39 | $0.57 | tools, json | | `deepseek-v4-flash` | DeepSeek | 1,048,576 | $0.135 | $0.27 | tools, json | | `qwen3-235b` | Qwen (Alibaba) | 262,144 | $0.135 | $0.825 | tools, json | | `qwen3-vl-30b` | Qwen (Alibaba) | 262,144 | $0.225 | $0.9 | tools, json, images | | `mistral-nemo` | Mistral AI | 131,072 | $0.028 | $0.045 | tools | | `gemma-3-27b` | Google | 131,072 | $0.12 | $0.24 | tools, json, images | | `mistral-small-24b-2501` | Mistral AI | 32,768 | $0.075 | $0.12 | json | | `gemma-3-4b` | Google | 131,072 | $0.075 | $0.15 | tools, json, images | | `gemma-4-e4b` | Google | 131,072 | $0.03 | $0.15 | tools | | `phi-4` | Microsoft | 16,384 | $0.105 | $0.21 | json | | `gemma-3-12b` | Google | 131,072 | $0.075 | $0.225 | tools, json, images | | `qwen3-14b` | Qwen (Alibaba) | 40,960 | $0.18 | $0.36 | tools | | `qwen3-32b` | Qwen (Alibaba) | 40,960 | $0.12 | $0.42 | tools | | `gemma-4-31b` | Google | 262,144 | $0.195 | $0.57 | tools, json, images | | `hy3` | Tencent | 262,144 | $0.21 | $0.87 | tools | | `hermes-3-llama-3.1-70b` | Nous Research | 131,072 | $1.05 | $1.05 | json | | `qwen3-vl-235b-a22b` | Qwen (Alibaba) | 262,144 | $0.3 | $1.32 | tools, json, images | | `qwen3.6-35b-a3b` | Qwen (Alibaba) | 262,144 | $0.15 | $1.425 | tools, json | | `deepseek-v3.1-terminus` | DeepSeek | 163,840 | $0.405 | $1.425 | tools, json | | `hermes-3-llama-3.1-405b` | Nous Research | 131,072 | $1.5 | $1.5 | json | | `qwen3-next-80b-a3b` | Qwen (Alibaba) | 262,144 | $0.135 | $1.65 | tools, json | | `step-3.7-flash` | StepFun | 262,144 | $0.3 | $1.725 | tools | | `glm-4.7` | Z.ai | 202,752 | $0.6 | $2.625 | tools | | `mimo-v2.5` | Xiaomi | 262,144 | $0.6 | $3 | tools, json | | `deepseek-r1-0528` | DeepSeek | 163,840 | $0.75 | $3.225 | chat | | `qwen3-235b-a22b-thinking-2507` | Qwen (Alibaba) | 262,144 | $0.345 | $3.45 | tools | | `glm-5.2` | Z.ai | 1,048,576 | $1.125 | $3.6 | tools, json | | `deepseek-v4-pro` | DeepSeek | 1,048,576 | $1.95 | $3.9 | tools, json | | `mimo-v2.5-pro` | Xiaomi | 1,048,576 | $1.5 | $4.5 | tools, json | | `qwen3.6-27b` | Qwen (Alibaba) | 262,144 | $0.48 | $4.8 | tools, json | | `glm-5.1` | Z.ai | 202,752 | $1.575 | $5.25 | tools, json | | `inkling` | Thinking Machines | 524,288 | $1.425 | $6.075 | tools | Speech models price per second of audio or per character of text, never per token. | Model | Maker | Billed per | Input $/M | Output $/M | | --- | --- | --- | --- | --- | | `whisper-large-v3-turbo` | OpenAI | second | $5 | $0 | | `qwen3-asr-1.7b` | Qwen (Alibaba) | second | $11.25 | $0 | | `kokoro-82m` | Hexgrad | character | $0.93 | $0 | Aliases. Opt-in by name; there is no language detection anywhere in this API. | Alias | Serves | Why | | --- | --- | --- | | `dawn-swahili-best` | `gemma-3-27b` | Cheapest real Swahili reply we serve. gpt-oss-20b tokenizes Swahili 11% better (1.47x against 1.65x vs English) and still costs 6.4x more per request, because it is a reasoning model and the thinking is billed. Measured on an identical short prompt, not inferred. | | `dawn-multilingual` | `gemma-3-27b` | Most even across the eight African languages measured (1.96x mean) and by far the best on Amharic at 1.94x, against 4.11x for Qwen and 7.69x for GLM. Does not bill for reasoning, so a one-line answer stays a one-line bill. 131k context. | Some open models with strong African-language performance carry non-commercial licences and cannot be served by anyone. They are absent from this list rather than silently swapped for a weaker model. --- ## Contents **Getting started** — Quickstart, Authentication, Models, Connect an AI assistant **Core** — Chat completions, Streaming, Tool calling, Structured output, Prompt caching, Transcriptions, Speech **Reference** — Pricing and billing, Rate limits, Errors, Idempotency, Request ids, Not supported yet, Changelog **Guides** — Migrating from OpenAI, Migrating from OpenRouter, SDK and framework compatibility --- ## Quickstart Source: https://dawnstackai.com/docs/quickstart Make your first request in under a minute. The API is OpenAI-compatible, so if your code already talks to OpenAI you change two lines. **curl** ```bash curl https://api.dawnstackai.com/v1/chat/completions \ -H "Authorization: Bearer $DAWNSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "llama-3.1-8b", "messages": [{"role": "user", "content": "What is the capital of Kenya? One sentence."}] }' ``` **Python** ```python from openai import OpenAI client = OpenAI( api_key=os.environ["DAWNSTACK_API_KEY"], base_url="https://api.dawnstackai.com/v1", ) response = client.chat.completions.create( model="llama-3.1-8b", messages=[{"role": "user", "content": "What is the capital of Kenya? One sentence."}], ) print(response.choices[0].message.content) ``` **Node** ```javascript import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.DAWNSTACK_API_KEY, baseURL: "https://api.dawnstackai.com/v1", }); const response = await client.chat.completions.create({ model: "llama-3.1-8b", messages: [{ role: "user", content: "What is the capital of Kenya? One sentence." }], }); console.log(response.choices[0].message.content); ``` And the response: ```json { "id": "chatcmpl-8f2a1c", "object": "chat.completion", "created": 1785097284, "model": "llama-3.1-8b", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "The capital of Kenya is Nairobi." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 21, "completion_tokens": 8, "total_tokens": 29 }, "dawnstack": { "cost_micros": 2, "cost_usd": 0.000002, "request_id": "req_8f2a1c" } } ``` ### What you need Three things, in order. 1. **An account.** [Sign in](/login) with Google or a magic link. No card. 2. **A balance.** A new account gets a **starter balance** ($0.02) to try the API on the models listed below. After that, top up from $1.00 in your own currency on the [balance page](/dashboard/balance), or redeem a code. 3. **An API key.** Create one in [the dashboard](/dashboard/keys). It is shown once and stored only as a hash, so if you lose it, revoke it and make another. > **Note** > > **The starter balance is for evaluating, not a free tier** — there is no ongoing free usage. It runs > on four inexpensive models: `mistral-nemo`, `llama-3.1-8b`, `mistral-small-24b-2501` and > `gemma-3-4b`. > > How far it goes depends on which one you pick, and the spread is worth seeing: roughly **600 > requests** on `mistral-nemo` down to **200** on `gemma-3-4b`, for a typical short exchange. That is > the same per-token pricing on [/models](/models) doing its work. No model is subsidised here. > > Any other model returns `403 STARTER_BALANCE_ONLY` before it costs you anything, naming the ones you > can call. `GET /v1/models` carries `starter: true` on each of them, so you never have to guess. ### Migrating from OpenAI Two lines: the base URL and the key. Everything else in your code stays as it is, including the `openai` SDK itself, the request shape, the response shape, streaming, and tool calling. ```diff client = OpenAI( - api_key=os.environ["OPENAI_API_KEY"], + api_key=os.environ["DAWNSTACK_API_KEY"], + base_url="https://api.dawnstackai.com/v1", ) ``` The one thing you do have to change is the model id. See [Models](/docs/models) for the catalogue, or read the [migration guide](/docs/migrating-from-openai) for the full list of differences. ### What it costs Nothing until you make a request, and then only what the model actually generated. The response above cost $0.000006. Prices are per million tokens and published in full on the [pricing page](/docs/pricing) and at `GET /v1/models`, which needs no API key. A price list you have to authenticate to read is not a price list. --- ## Authentication Source: https://dawnstackai.com/docs/authentication One header, one key format. How keys are created, what they can and cannot do, and what happens when one leaks. Every request to `/v1` carries a bearer token: ```http Authorization: Bearer sk-dawn-... ``` Keys look like `sk-dawn-` followed by 48 hex characters. The `sk-` prefix is functional rather than cosmetic: it is what the API dispatches on to tell a key from a browser session. The `dawn-` segment makes a leaked key greppable and attributable to us, which is the difference between a scanner flagging it and nobody noticing. ### Creating a key In [the dashboard](/dashboard/keys). A key is shown **once** and stored only as a hash, so there is no "show key" button and no support request that can recover one. If you lose it, revoke it and make another. > **Warning: Keys are secrets** > > Use a key from a server, never from a browser or a mobile app. Anything shipped to a device can be > read off it. `/v1` deliberately sends no CORS headers for this reason: a browser cannot call it > directly, which is a feature. ### An API key cannot manage API keys `/api/v1/keys` is session-only. A key can spend your balance; it cannot mint replacements, list your other keys, or revoke them. That asymmetry is the whole point. If a key leaks, revoking it ends the exposure. If a key could create keys, an attacker would mint a permanent replacement in the seconds before you noticed, and revoking the original would achieve nothing. ### Revoking Immediate. Revocation purges the cached lookup as well as marking the row, so the key stops working on the next request rather than when a 60-second cache expires. A revocation that takes a minute to apply is not a revocation. ### What is authenticated where | Surface | Auth | Notes | | --- | --- | --- | | `POST /v1/chat/completions` | API key, or a session cookie | The session path exists for the playground and is rate limited separately | | `GET /v1/models` | none | Public. A price list you have to authenticate to read is not a price list | | `GET /v1/models/{id}` | none | Public, same reasoning | | `/api/v1/*` | session cookie | The dashboard's own surface. Not for programmatic use | ### Errors A missing, malformed or revoked key is a `401` with `type: "authentication_error"` and a `WWW-Authenticate: Bearer` header. See [Errors](/docs/errors) for the full shape. A key that is valid but whose account has no balance is a `402`, not a `401`. The distinction matters: one means "fix your credentials", the other means "top up". See [Pricing and billing](/docs/pricing). --- ## Models Source: https://dawnstackai.com/docs/models The full catalogue with prices, context windows and measured capabilities, rendered live from the same registry the API serves from. Pass a model id as `model`. `GET /v1/models` returns this same list at runtime and needs no API key. See "The catalogue" above for the full table of models, prices and capabilities. ### Capabilities are measured, not claimed `Can do` above comes from a probe that sends a real tool call and a real `json_schema` request to every model and records what came back. It is not copied from a supplier's catalogue tags. That matters because those tags are wrong. One 70B model is tagged for structured output by its provider and ignores `response_format` outright: four consecutive `strict: true` requests returned conversational prose, and `json_object` mode returned JSON wrapped in code fences, which `JSON.parse` also rejects. We declare only what we measured. Asking a model for a capability it does not declare is a `400` naming the parameter, before any billable work. An unmeasured capability counts as absent, so you always get a clear error rather than a plausible wrong answer. #### Two fields say "modality", and they answer different questions `GET /v1/models` publishes both, and picking the wrong one is easy: | Field | Question it answers | Values | | --- | --- | --- | | `modality` | Which endpoint serves this model, and therefore what `pricing.unit` counts | `text`, `stt`, `tts` | | `input_modalities` | What the model can read | `["text"]` or `["text", "image"]` | **Filter on `input_modalities` to find models that accept images.** A model that reads images is still a chat model billed per token, so it is still `modality: "text"`. Filtering on `modality` for image support finds nothing, which is what a customer reported in August 2026 and why `input_modalities` exists. See [Images](/docs/chat-completions#images). ### Aliases An alias resolves to whichever model is currently best for a job. You opt in by naming it. | Alias | Serves | Why | | --- | --- | --- | | `dawn-swahili-best` | `gemma-3-27b` | Cheapest real Swahili reply we serve. gpt-oss-20b tokenizes Swahili 11% better (1.47x against 1.65x vs English) and still costs 6.4x more per request, because it is a reasoning model and the thinking is billed. Measured on an identical short prompt, not inferred. | | `dawn-multilingual` | `gemma-3-27b` | Most even across the eight African languages measured (1.96x mean) and by far the best on Amharic at 1.94x, against 4.11x for Qwen and 7.69x for GLM. Does not bill for reasoning, so a one-line answer stays a one-line bill. 131k context. | > **Warning: There is no language detection anywhere in this API** > > We never inspect your prompt and quietly route it to a different model. Doing that changes your > output, your bill and your reproducibility with no way for you to know why. An alias is the only > form of routing here, and you have to ask for it by name. Where an alias points changes as better models appear. Your code does not. The usage record keeps **both** ids — what you asked for and what served it — so a bill from last month stays explainable after an alias repoints. ### Models that reason first Some models think before answering, and the thinking is billed even though it is never returned. Two consequences that catch people out: - **A better tokenizer does not pay for itself on short answers.** One model needs a quarter of another's tokens for Amharic and still costs more for a one-line reply, because the reasoning dwarfs the saving. Compare per *request* for your workload, not per token. - **A small `max_tokens` can buy silence.** The budget goes on reasoning and you get an empty string and a bill. We raise the ceiling for these models rather than hand you nothing, and you are billed on tokens actually produced, so a request that finishes early costs what it costs. Use `reasoning_effort` to control it directly: **Python** ```python client.chat.completions.create( model="glm-4.7", messages=[{"role": "user", "content": "What is 2+2?"}], reasoning_effort="none", ) ``` ### Licences Some open models with strong African-language performance carry non-commercial licences and cannot be served by anyone, us included. They are **absent** from the list rather than silently replaced with a weaker model. Asking for one is a `403` that names the licence, not a `404`: pretending a model does not exist would be a lie about something we deliberately refuse to sell. ### Retrieving one model **curl** ```bash curl https://api.dawnstackai.com/v1/models/llama-3.1-8b ``` **Python** ```python client.models.retrieve("llama-3.1-8b") ``` Aliases resolve here too, and the response carries `alias_for` and the rationale when you ask for one. --- ## Connect an AI assistant Source: https://dawnstackai.com/docs/mcp Connect Dawnstack to Claude, ChatGPT or Cursor and your assistant can read the catalogue and these docs, then build against the API. No key to paste. Dawnstack runs a Model Context Protocol server. Connect it to an assistant that speaks MCP and the assistant can read the model catalogue with live prices, read this documentation, check the balance, and run a request to verify a choice before writing it into your code. The endpoint is: ```text https://api.dawnstackai.com/mcp ``` There is no key to paste. The first time your assistant calls it, you will be sent to a Dawnstack page to approve the connection, and that is all. ### What the assistant can do | Tool | What it does | |---|---| | `list_models` | The catalogue: prices, context windows, measured speed, measured per-language cost | | `get_model` | One model in detail, including which parameters it really supports | | `search_docs` | Finds the pages here that answer a question | | `get_docs` | Reads a page, or the whole reference | | `get_account` | Balance, and which models it can currently call | | `run_model` | One chat completion, billed to your balance, for checking a model behaves as expected | Everything except `run_model` is read-only and costs nothing. `run_model` spends your balance at the prices `list_models` reports, caps output at 512 tokens by default, and tells you what each call cost. > **Warning: It cannot create API keys** > > A connected assistant can spend your balance but cannot mint a key, take a payment or change your > account. Keys are session-only by design, so one compromised connection cannot turn itself into > permanent access. Disconnect any assistant from [Connected applications](/dashboard/connections). ### Connecting **Claude Code** ```bash claude mcp add --transport http dawnstack https://api.dawnstackai.com/mcp ``` **Cursor** ```json { "mcpServers": { "dawnstack": { "url": "https://api.dawnstackai.com/mcp" } } } ``` **Claude** ```text Settings -> Connectors -> Add custom connector Name: Dawnstack URL: https://api.dawnstackai.com/mcp On a Team or Enterprise plan an owner adds it once in Organization settings, and members then click Connect. ``` **ChatGPT** ```text Settings -> Apps & Connectors -> Advanced -> Developer mode Then add a connector with: https://api.dawnstackai.com/mcp Developer mode is available on paid plans, and a workspace admin can turn it off for everyone. ``` Then ask for something. *Build me a script that classifies support tickets, using Dawnstack, on a model that is cheap for Swahili.* The assistant will read the catalogue, pick a model on the measured numbers rather than on a guess, and write code against this API. ### What to expect the first time Your assistant will ask you to connect. Approving takes you to a Dawnstack page that names the application by its hostname, says what it will be able to do, and waits for you to click. If you are not signed in, you sign in first and come straight back. > **Note** > > The approval page shows the application's **hostname**, not the name it gives itself. Any application > can call itself anything; the hostname is the part that is hard to fake. If the hostname is not the > tool you meant to connect, do not approve it. A new account has a starter balance, so the assistant can run real requests before you pay anything. When it runs out, `run_model` says so and tells you where to top up. Everything read-only keeps working. ### What it is not This is not a way to run production traffic. An assistant calling `run_model` in a loop is spending your balance on round trips, and the tool caps output low for that reason. Once you know which model you want, have the assistant generate code that calls [chat completions](/docs/chat-completions) with your own [API key](/docs/authentication). That is the path everything else in these docs describes, and it is the one that streams, calls tools, returns structured output and accepts images. ### Building without connecting If you would rather not connect anything, hand your assistant [llms-full.txt](https://dawnstackai.com/llms-full.txt). It is this entire reference plus the live catalogue as one file, and it is the same text `get_docs` returns. --- ## Chat completions Source: https://dawnstackai.com/docs/chat-completions The endpoint the product exists for. Every OpenAI parameter is forwarded verbatim, and anything genuinely unsupported is rejected by name rather than dropped. `POST /v1/chat/completions` The request and response are OpenAI's. Field names are theirs and are not tidied, because the whole migration story is that an unmodified client works. ### Parameters | Field | Notes | | --- | --- | | `model` | Required. A model id or an alias. See [Models](/docs/models) | | `messages` | Required. Roles `system`, `user`, `assistant`, `tool`. `content` is a string, or an array of parts for [images](#images) | | `max_tokens` | Defaults to 1024, ceiling 32000. The reservation is priced off this | | `max_completion_tokens` | OpenAI's newer spelling for the same cap. Send either one. Sending both with different values is a `400` | | `temperature`, `top_p`, `stop`, `seed` | Forwarded. Ranges are OpenAI's | | `frequency_penalty`, `presence_penalty` | Forwarded | | `stream`, `stream_options` | See [Streaming](/docs/streaming) | | `tools`, `tool_choice` | See [Tool calling](/docs/tool-calling) | | `response_format` | See [Structured output](/docs/structured-output) | | `reasoning_effort` | `none`, `low`, `medium`, `high`. See [Models](/docs/models#models-that-reason-first) | | `logprobs`, `top_logprobs` | Forwarded | | `prompt_cache_key` | See [Prompt caching](/docs/prompt-caching) | > **An unsupported parameter is an error, not a silence** > > Two cases, one answer. A parameter that is not in the table above, and a parameter this particular > model does not support, both return a `400` naming the parameter before any billable work happens. > The code is `unsupported_parameter` either way. > > That rule came from getting it wrong twice. This schema listed five fields for months, and a caller > who sent `tools` got a `200` with prose, no tool call, and nothing saying why. Then in August 2026 a > customer capped their output with `max_completion_tokens`, which we did not yet recognise, and were > billed for a full-length reply instead. Silent and billable is the worst pair available. > > The exception is a short list of OpenAI bookkeeping fields that cannot change your output or your > bill: `user`, `store`, `metadata`, `service_tier`, `safety_identifier` and `n: 1`. Those are > accepted and ignored, because rejecting them would break working clients over fields that never did > anything. Everything else is refused by name. ### The response ```json { "id": "chatcmpl-8f2a1c", "object": "chat.completion", "created": 1785097284, "model": "llama-3.1-8b", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "The capital of Kenya is Nairobi." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 21, "completion_tokens": 8, "total_tokens": 29, "prompt_tokens_details": { "cached_tokens": 0 } }, "dawnstack": { "cost_micros": 2, "cost_usd": 0.000002, "request_id": "req_8f2a1c" } } ``` `dawnstack` is additive and non-standard. An OpenAI client ignores unknown fields, so it costs you nothing, and it means you do not have to reconcile a call against your ledger afterwards to know what it cost. `cost_micros` is the integer of record; `cost_usd` is the same number in dollars. `finish_reason` is passed through from the model. `stop` means it finished, `length` means it hit `max_tokens`, `tool_calls` means it wants you to call a function, and `error` means the stream failed after it had started. ### Multi-turn Send the whole conversation. There is no server-side state, no thread id, and nothing to clean up. **Python** ```python messages = [{"role": "system", "content": "Answer in one sentence."}] while True: messages.append({"role": "user", "content": input("> ")}) reply = client.chat.completions.create(model="llama-3.1-8b", messages=messages) text = reply.choices[0].message.content messages.append({"role": "assistant", "content": text}) print(text) ``` Set `prompt_cache_key` to the same value across a conversation's turns and the repeated prefix gets cheaper. See [Prompt caching](/docs/prompt-caching). ### Images Some models accept images. Send `content` as an array of parts instead of a string, exactly as you would against OpenAI: ```json { "model": "gemma-3-27b", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "What is in this picture?" }, { "type": "image_url", "image_url": { "url": "https://example.com/photo.jpg" } } ] } ] } ``` **Check `input_modalities` on [`GET /v1/models`](/docs/models) before you send one.** A model that reads images publishes `["text", "image"]`; everything else publishes `["text"]`. Sending an image to a text-only model is a `400` naming `messages`, before any billable work. `input_modalities` is the field to filter on, not `modality`. `modality` says which endpoint serves the model and therefore what its price is counted in, so every chat model is `"text"` there whether or not it reads images. > **Warning: detail is forwarded and not honoured** > > `image_url.detail` is passed through to the supplier, and none of our current suppliers acts on it. > Setting `"low"` does not reduce the tokens an image costs. Measure with the `usage` block on the > response rather than assuming a hint was applied. Images are estimated generously when your balance is reserved, and the reservation is refunded down to the real figure once the supplier reports actual usage. You are billed on that figure, never on the estimate. ### What is not supported `n > 1` is not implemented: ask twice. Sending `n: 1` is fine and is what we already do. Everything else that is missing is on [Not supported yet](/docs/not-supported-yet). --- ## Streaming Source: https://dawnstackai.com/docs/streaming Server-sent events, token usage in the stream, tool calls while streaming, and exactly what you are billed when a client disconnects. Pass `stream: true`. Frames are `chat.completion.chunk`, terminated by `data: [DONE]`. **Python** ```python stream = client.chat.completions.create( model="llama-3.1-8b", messages=[{"role": "user", "content": "Count to five."}], stream=True, ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True) ``` **Node** ```javascript const stream = await client.chat.completions.create({ model: "llama-3.1-8b", messages: [{ role: "user", content: "Count to five." }], stream: true, }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); } ``` The stream opens with a role frame, then content frames, then a frame carrying `finish_reason`: ``` data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]} data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"One"},"finish_reason":null}]} data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} data: [DONE] ``` ### Token counts in the stream Ask for them with `stream_options`: **Python** ```python stream = client.chat.completions.create( model="llama-3.1-8b", messages=[{"role": "user", "content": "Count to five."}], stream=True, stream_options={"include_usage": True}, ) ``` You then get one extra frame, after the final choice frame, with an **empty `choices` array**: ``` data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":9,"total_tokens":23},"dawnstack":{"cost_micros":1,"cost_usd":0.000001,"request_id":"req_8f2a1c"}} ``` If your loop reads `chunk.choices[0]` unconditionally it will find nothing on that frame. That is OpenAI's shape, and the SDKs handle it; hand-rolled parsers usually need a guard. ### What the stream cost `dawnstack.cost_micros` on that frame is what the request cost, in micro-dollars, and it is the same number your balance is debited by — not an estimate of it. It is priced from the token counts in the same frame, by the same function that settles the charge, so the two cannot disagree. That means a streaming client never has to reconcile a call against its ledger afterwards, or recompute a price from `GET /v1/models`. Read it off the last frame. > **You have to ask for the usage frame to get the cost** > > `dawnstack` rides on the usage frame, so it only arrives when you send > `stream_options: {"include_usage": true}`. Without it the stream ends at the final choice frame and > reports no cost at all. There is no other route to it while streaming: the balance endpoints are > part of the dashboard's own surface and are not a public API. A stream that failed part way sends **no** usage frame, and therefore no cost. A usage block on a broken stream would read as a completed request. **You are still billed for what was generated** — the meter runs on tokens the model produced, not on whether you received them — so a stream you abandon or that dies mid-answer still costs what it cost. To find out how much, read `x-request-id` off the response and quote it. ### Tool calls They stream. `delta.tool_calls` accumulates by `index`: the first frame carries the id, type and function name, and later frames append a few characters of `function.arguments` at a time. Reassembling them is the client's job, and every SDK already does it. See [Tool calling](/docs/tool-calling). ### When a stream fails part way Once the response headers are sent there is no status code left to fail with, so a mid-stream failure closes the stream with `finish_reason: "error"` rather than `"stop"`. Check it. A parser that reads only `content` cannot tell a truncated answer from a complete one, and that is the whole reason this exists. ### Disconnecting > **Warning: A disconnect is not a free request** > > If you close the connection part way through, you are still billed for what the model generated > before you left. The model does not stop when you hang up, and our supplier bills us for those > tokens. Billing is otherwise identical whether you stream or not. Same reservation, same settle, same ledger entry, same usage record. Send the same prompt at `temperature: 0` with and without `stream` and the two cost the same. ### Not available for every model A small number of models are served through a path where streaming is not implemented. They are filtered out of anywhere that offers streaming, and asking for it explicitly is a clean error rather than a reservation followed by a failure. ### Idempotency and streaming do not combine `Idempotency-Key` with `stream: true` is a `400`. Replaying a stream from cache would mean buffering every response on the chance of a retry. Refusing is honest; accepting the header and ignoring it would let you believe you were protected from a double charge when you were not. See [Idempotency](/docs/idempotency). --- ## Tool calling Source: https://dawnstackai.com/docs/tool-calling Functions, the round trip, and which models actually support it. This is the single most common reason existing agent code would not work against a new provider. Pass `tools` and, optionally, `tool_choice`. Both are OpenAI's shape and are forwarded verbatim. **Python** ```python tools = [{ "type": "function", "function": { "name": "get_weather", "description": "Current weather for a city.", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }, }] reply = client.chat.completions.create( model="llama-3.3-70b", messages=[{"role": "user", "content": "What is the weather in Nairobi?"}], tools=tools, ) call = reply.choices[0].message.tool_calls[0] print(call.function.name, call.function.arguments) ``` `finish_reason` is `tool_calls` and `content` is empty. That combination is the model asking you to run something, not the model saying nothing. ### The round trip Send the result back in a message with `role: "tool"`, carrying the `tool_call_id` from the call: **Python** ```python messages = [ {"role": "user", "content": "What is the weather in Nairobi?"}, reply.choices[0].message, { "role": "tool", "tool_call_id": call.id, "content": '{"temp_c": 24, "conditions": "clear"}', }, ] final = client.chat.completions.create(model="llama-3.3-70b", messages=messages, tools=tools) print(final.choices[0].message.content) ``` ### Which models support it Check `Can do` on [Models](/docs/models), or `supported_parameters` on `GET /v1/models`. Sending `tools` to a model that does not declare support is a `400` naming the parameter, before any billable work. Those declarations are measured by sending a real tool call to each model, not copied from a supplier's catalogue. Suppliers over-claim. ### Streaming Tool calls stream. `delta.tool_calls` accumulates by `index`, arguments arriving a few characters at a time, and `finish_reason` is `tool_calls` on the final frame. The SDKs reassemble this for you. > **Note** > > This did not work until 30 July 2026. The frame rewriter forwarded only `delta.content`, so a > streamed tool call arrived as an empty stream ending in `finish_reason: "stop"` — indistinguishable > from a model that had nothing to say. If you tried tool calling with `stream: true` before then and > concluded it was unsupported, it works now. --- ## Structured output Source: https://dawnstackai.com/docs/structured-output JSON mode and json_schema, and why the list of models that support it is shorter than any supplier's catalogue says. Pass `response_format`, in either of OpenAI's two forms. **json_schema** ```python reply = client.chat.completions.create( model="gpt-oss-120b", messages=[{"role": "user", "content": "Nairobi, Kenya. Population about 5.5 million."}], response_format={ "type": "json_schema", "json_schema": { "name": "city", "strict": True, "schema": { "type": "object", "properties": { "city": {"type": "string"}, "country": {"type": "string"}, "population": {"type": "integer"}, }, "required": ["city", "country", "population"], "additionalProperties": False, }, }, }, ) import json print(json.loads(reply.choices[0].message.content)) ``` **json_object** ```python reply = client.chat.completions.create( model="gpt-oss-120b", messages=[ {"role": "system", "content": "Reply with JSON only."}, {"role": "user", "content": "Nairobi, Kenya. Population about 5.5 million."}, ], response_format={"type": "json_object"}, ) ``` `json_object` needs the word JSON somewhere in your messages, same as OpenAI. Prefer `json_schema` where the model supports it: it constrains the shape rather than asking politely. ### The supported list is shorter than suppliers claim > **Warning: We do not repeat a supplier's capability tags** > > One 70B model is tagged `structured-output` and `json` by its provider and ignores `response_format` > outright. Four consecutive `strict: true` requests came back as conversational prose, and > `json_object` mode returned JSON wrapped in code fences, which `JSON.parse` rejects too. > > So we send a real `strict: true` request to every model and record what comes back. The bar is > "`JSON.parse` succeeds **and** the required keys are present" — fenced JSON fails, because parsing > the content is the entire point of asking. Check `Can do` on [Models](/docs/models) or `supported_parameters` on `GET /v1/models`. Asking a model that does not declare it is a `400`: ```json { "error": { "message": "Model \"llama-3.3-70b\" does not support `response_format`.", "type": "invalid_request_error", "param": "response_format", "code": "unsupported_parameter" } } ``` An error before any billable work, rather than a paid-for answer in the wrong shape. ### Reasoning models They think before producing the JSON, and the thinking is billed. Give them room: a `max_tokens` sized for the JSON alone can be consumed entirely by reasoning, leaving you an empty string. See [Models](/docs/models#models-that-reason-first). --- ## Prompt caching Source: https://dawnstackai.com/docs/prompt-caching Repeated prefixes are cached automatically. What is reported, what it changes about your bill today, and what it does not. A long system prompt sent on every turn is mostly identical every time. Our supplier caches matching prefixes automatically, so you already get the latency benefit with no change to your code. ### What is reported Every response carries the count: ```json "usage": { "prompt_tokens": 2048, "completion_tokens": 31, "total_tokens": 2079, "prompt_tokens_details": { "cached_tokens": 1920 } } ``` > **You are billed on full prompt_tokens today** > > `cached_tokens` is reported, not discounted. Whether to pass the saving through is a pricing > decision and it has not been made yet. > > We report it anyway, because the alternative is a silent margin windfall on every long prompt. > `BRAND.md` requires that the price is legible, and a cost benefit you cannot see is not legible. > When the pricing decision is made it will appear in the [changelog](/docs/changelog). ### Raising the hit rate Pass `prompt_cache_key`. It keeps a conversation's turns on the same cache lineage, which matters for multi-turn chat where the shared prefix is the whole system prompt plus the history so far. **Python** ```python client.chat.completions.create( model="llama-3.3-70b", messages=messages, prompt_cache_key=f"conversation-{conversation_id}", ) ``` Use a value that is stable for a conversation and different between conversations. A user id works; a per-request uuid defeats the point. ### What helps - **Put the stable part first.** Caching matches on prefixes, so a system prompt followed by history caches well and a timestamp at the top of every message caches nothing. - **Do not reorder.** A tool list shuffled between calls is a different prefix. - **Longer prefixes gain more.** A fifty-token system prompt is not worth designing around. --- ## Transcriptions Source: https://dawnstackai.com/docs/transcriptions Turn speech into text on OpenAI's own audio path. Billed per second of audio you send, rounded up, with the price published the same way every other model's is. `POST /v1/audio/transcriptions` The same path OpenAI uses, so an existing client needs a new base URL and nothing else. Send a file as `multipart/form-data`, get text back. ### Parameters | Field | Type | Notes | | --- | --- | --- | | `file` | file | **Required.** The audio. Up to 20 MB. | | `model` | string | **Required.** A model with modality `stt`. See [Models](/docs/models). | | `language` | string | ISO-639-1 code. Supplying it improves accuracy and costs nothing. | | `prompt` | string | Context to bias the transcription, such as proper nouns or place names. | | `response_format` | string | `json` (default), `verbose_json`, or `text`. | > **Note** > > A `Content-Length` header is required. The file size is checked before the body is read, so a > chunked upload is refused rather than accepted and then failing partway through. ### Pricing Billed **per second of audio submitted, rounded up to the whole second**. You pay for the audio you sent, not for what was left after silence was trimmed, so the cost is predictable from the file itself. A three-minute voice note costs about $0.0009. The $1.00 minimum top-up covers roughly a thousand of them. The price is on `pricing.usd_per_million_input` from `GET /v1/models`, alongside `pricing.unit: "second"` — the same two fields text models publish, reading in a different unit. ### Example **curl** ```bash curl https://api.dawnstackai.com/v1/audio/transcriptions \ -H "Authorization: Bearer $DAWNSTACK_API_KEY" \ -F file=@voice-note.m4a \ -F model=whisper-large-v3-turbo \ -F language=sw ``` **Python** ```python from openai import OpenAI client = OpenAI( api_key="sk-dawn-...", base_url="https://api.dawnstackai.com/v1", ) with open("voice-note.m4a", "rb") as f: result = client.audio.transcriptions.create( model="whisper-large-v3-turbo", file=f, language="sw", ) print(result.text) ``` ### The response ```json { "text": "Habari yako rafiki.", "dawnstack": { "cost_micros": 25, "billed_seconds": 5, "request_id": "req_01J..." } } ``` `billed_seconds` is the number you were actually charged for, so the bill can be checked against the file without a second call. The `dawnstack` block is additive — an OpenAI client ignores it. With `response_format: "verbose_json"` you also get `language`, `duration` (the billed seconds), and `segments` with per-segment start and end times. With `response_format: "text"` the body is the bare transcript, `text/plain`, exactly as OpenAI returns it. ### Limits **20 MB per file.** A Worker has a fixed memory budget and the whole file passes through it. Longer audio needs an asynchronous path, which is not built yet — for now, split the file. **`srt` and `vtt` are not supported.** Asking for either returns a `400` naming the parameter rather than quietly giving you `json`. The segment timings needed to build them are in `verbose_json` today, so you can generate subtitles yourself in the meantime. ### Which languages actually work Whisper is MIT licensed and is the strongest speech model we can serve without a licence problem. Its quality on African languages is **not something we have measured yet, and we will not claim it until we have.** Publishing that measurement — including if the answer is bad — is on the roadmap. Passing `language` explicitly helps. Automatic detection is least reliable for exactly the languages that are least represented in training data. > **Note** > > Meta's MMS has considerably better African-language coverage and is CC-BY-NC, which forbids > commercial use. It stays in our catalogue marked as blocked rather than quietly omitted, because > the gap between "best available" and "best we may sell" is the point. --- ## Speech Source: https://dawnstackai.com/docs/speech Turn text into audio on OpenAI's own path. Billed per input character, so the price is known before you send it. Repeated phrases cost a quarter. `POST /v1/audio/speech` JSON in, audio out. The same path and the same field names OpenAI uses. ### Parameters | Field | Type | Notes | | --- | --- | --- | | `model` | string | **Required.** A model with modality `tts`. See [Models](/docs/models). | | `input` | string | **Required.** The text to speak. Up to 4,096 characters. | | `voice` | string | Voice name, model-specific. | | `response_format` | string | `mp3` (default), `opus`, `flac`, `wav`, `pcm`. | | `speed` | number | `0.25` to `4`. Defaults to `1`. | ### Pricing Billed **per character of `input`**, which means the price is fully determined before you send the request — no estimate, no reservation, no refund. Multiply the length of your text by `pricing.usd_per_million_input` from `GET /v1/models`, where `pricing.unit` reads `character`. > **Note** > > Speech synthesis is priced per character and transcription per second. They are different units > because that is how the underlying models are priced, and converting one into the other would mean > inventing a characters-per-second figure that varies by language and voice. #### Repeated phrases cost a quarter The same text, in the same voice, at the same speed and format always produces the same audio. So we keep it, and serving it back costs you **25% of the generation price**. This is aimed squarely at the case it came from: a shop bot that says *"your order is confirmed"* a thousand times a day should pay to generate it once. Every response carries the outcome, so the discount is checkable rather than asserted: ``` x-dawnstack-cached: hit # served from cache, billed at 25% x-dawnstack-cached: miss # generated, billed in full x-dawnstack-cost-micros: 6 # what this request cost ``` The cache is keyed on the text, voice, model, format **and** speed together — change any one of them and you get a fresh generation, because any one of them changes the audio. It is shared across all accounts. That is what makes it worth having, and it is safe because synthesis is a pure function of inputs you supplied yourself: a hit returns audio you could have generated, and reveals only that the same sentence had been requested before. ### Example **curl** ```bash curl https://api.dawnstackai.com/v1/audio/speech \ -H "Authorization: Bearer $DAWNSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "kokoro-82m", "input": "Your order is confirmed.", "response_format": "mp3" }' \ --output confirmed.mp3 ``` **Python** ```python from openai import OpenAI client = OpenAI( api_key="sk-dawn-...", base_url="https://api.dawnstackai.com/v1", ) response = client.audio.speech.create( model="kokoro-82m", input="Your order is confirmed.", response_format="mp3", ) response.write_to_file("confirmed.mp3") ``` ### The response The raw audio bytes, with the content type matching `response_format` — not JSON containing a URL, and not a link you have to fetch separately. Write the body to a file and play it. ### Limits **4,096 characters per request.** This is also the ceiling on what a single call can cost. **One model today.** Kokoro-82M, Apache-2.0 licensed. Open-weight text-to-speech is thinner ground than transcription and African-language coverage is worse; we would rather ship one model we can serve honestly than a list padded with ones we cannot. **Streaming is not supported yet.** The full audio is generated, then returned. --- ## Pricing and billing Source: https://dawnstackai.com/docs/pricing What a request costs, how the reserve-then-settle meter works, and what the balance actually is. Prices are live from the same registry the API serves from. Prices are per million tokens, in US dollars, and published in full. `GET /v1/models` returns the same figures and needs no API key. See "The catalogue" above for the full table of models, prices and capabilities. ### Your balance Prepaid, denominated in US dollars, and it does not expire. Top up from $1.00 in your own currency on the [balance page](/dashboard/balance). There is no subscription, no card on file and no monthly minimum. A new account gets a **starter balance** ($0.02) to try the API on four inexpensive models, which is a few hundred requests depending which one you pick. After that you top up or redeem a code. See [Quickstart](/docs/quickstart) for the list. > **Note** > > There used to be a second unit called credits, where one credit was $0.00001. It was retired on > 30 July 2026 because it was US dollars multiplied by 100,000 and nothing else: no separate price, no > exchange rate of its own. If you are reading older material, `credits: 6` is `$0.000006`. ### Units in the API A cost appears twice in every response, and the pair is deliberate: | Field | What it is | | --- | --- | | `cost_micros` | An integer count of **micro-dollars**, where 1 micros = $0.000001. This is the figure of record. Sum these; they carry no floating-point error | | `cost_usd` | The same number in dollars. So a log line is readable without knowing the scale | The same pairing appears on `/api/v1/balance` as `balance_micros` and `balance_usd`. **Streaming reports it too**, on the `include_usage` frame — see [Streaming](/docs/streaming#what-the-stream-cost). Both paths carry the same `dawnstack` block, so one parser reads either. A stream that fails part way sends no usage frame and therefore no cost, and is still billed for what it generated. ### Reserve, then settle Output length is unknowable before a request runs, so a single debit afterwards would let an empty balance run work we had already paid for. Instead: 1. **Reserve.** The worst case is priced from your `max_tokens` and held. Not enough balance is a `402` here, before any provider spend. 2. **Run.** 3. **Settle.** The provider returns real token counts, the actual cost is charged and the remainder released. Three things follow, and all three are visible in your ledger: - **A reservation you see mid-request is not a charge.** It comes back. - **A provider failure returns the whole reservation.** A failed call costs nothing. - **Billing is on the counts the model returned, never on our estimate.** The estimate exists only to size the reservation, and the reservation prices your whole `max_tokens` budget as output, so it is normally well above what a request actually costs. The excess is refunded. This bullet used to claim the estimate "errs high for African languages". **That was backwards.** The estimate is roughly four characters per token, an English approximation, and a language that tokenizes less efficiently produces MORE tokens per character, so for those languages the input estimate reads low, not high. It is corrected here rather than quietly deleted because it is a statement about how you are billed. ### Rounding A charge rounds **up**, to the micro-dollar. At these unit prices rounding down would make small requests free, which is a hole rather than a courtesy. The smallest possible request costs $0.000001. ### Reasoning tokens are billed A model that thinks before answering is billed for the thinking, even though it is never returned to you. On a short reply this can dominate: the same one-line answer costs about $0.00001 on one model and about $0.003 on another. [Models](/docs/models) marks which ones these are. ### Streaming bills identically Same reservation, same settle, same ledger entry. **A disconnect is not a free request**: the model keeps generating after you hang up and our supplier bills us for those tokens, so you are billed for what was produced before you left. ### Paying in your own currency Prices are set in US dollars because that is what our own costs are in. You are charged in the currency you pick at checkout, converted at the day's rate, and the exact total is shown before you confirm. You receive the full dollar amount you bought: the processing fee rides on top and never comes out of the balance. ### What we do not do - No monthly commitment, no seats, no tiers. - No charge for a failed request. - No silent repricing. A price change appears in the [changelog](/docs/changelog) and never retroactively changes what an existing balance is worth. --- ## Rate limits Source: https://dawnstackai.com/docs/rate-limits Sixty requests a minute per key, the headers that tell you where you stand, and how to back off correctly. **60 requests per minute, per API key.** Fixed window. Requests authenticated with a browser session rather than a key — which is the playground, not anything you would write — are limited to 20 per minute per account, separately. ### Headers Every rate-limited response carries all three, including the `429`: | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | The ceiling for this window | | `X-RateLimit-Remaining` | What is left | | `X-RateLimit-Reset` | Unix seconds when the window rolls over | A `429` also carries `Retry-After`, in seconds. > **Note** > > These headers appear on calls that are actually metered, which means key-authenticated calls to > `/v1/chat/completions`. `GET /v1/models` is public and unmetered, so it does not carry them. ### Backing off Read `Retry-After` and wait. If you are writing your own retry loop, exponential backoff with jitter on `429` and `502`, and no retry at all on `400`, `401`, `402`, `403` or `404` — none of those get better by being sent again. **Python** ```python import time, random, openai def with_retry(fn, attempts=5): for i in range(attempts): try: return fn() except openai.RateLimitError as e: wait = float(e.response.headers.get("retry-after", 0)) or (2 ** i + random.random()) time.sleep(wait) except openai.APIStatusError as e: # 402 means top up, 4xx means fix the request. Neither is helped by retrying. if e.status_code != 502: raise time.sleep(2 ** i + random.random()) raise RuntimeError("still rate limited after retries") ``` The `openai` SDK already retries `429` and `5xx` with backoff by default. Set `max_retries` on the client rather than writing your own unless you need something specific. ### If you need more Ask. There is no self-serve tier that raises it, and the limit exists to stop a runaway loop spending a balance in seconds rather than to ration capacity. Tell us what you are building and what rate you need. ### What counts A request counts when it is accepted, including one that then fails at the model. A request refused by the limiter does not consume any of your balance. --- ## Errors Source: https://dawnstackai.com/docs/errors The exact response body for every status, which ones are worth retrying, and how to report one so it can be looked up. Every error from `/v1` has the same shape, which is OpenAI's: ```json { "error": { "message": "Insufficient balance: $0.001230 required, $0.000400 available", "type": "insufficient_quota", "param": null, "code": "INSUFFICIENT_BALANCE", "details": { "required_micros": 1230, "required_usd": 0.00123, "available_micros": 400, "available_usd": 0.0004 } } } ``` `message` is written to be read by a person. `type` is OpenAI's vocabulary, which is what the SDKs branch on. `code` is ours, stable, and safe to switch on. `param` names the offending field when there is one. `details` is additive and only present where there is something useful to add. ### Every status | Status | `type` | What it means | Retry? | | --- | --- | --- | --- | | `400` | `invalid_request_error` | You sent a parameter we do not implement, or one this model does not support, or an image to a model that does not read images, or `Idempotency-Key` with `stream: true`. The message names it, and `param` carries it | No | | `401` | `authentication_error` | Key missing, malformed or revoked. Also carries `WWW-Authenticate` | No | | `402` | `insufficient_quota` | Not enough balance. `details` says how much was needed and how much you had | After topping up | | `403` | `permission_error` | The model exists but its licence forbids commercial use, so nobody can serve it. Pick another from `GET /v1/models` | No | | `404` | `not_found_error` | No such model. `GET /v1/models` has the list | No | | `409` | `invalid_request_error` | An identical `Idempotency-Key` is still in flight. Wait and retry | Yes, shortly | | `422` | `invalid_request_error` | The body failed validation, or an `Idempotency-Key` was reused with a different body. `details` has the field errors | No | | `429` | `rate_limit_error` | Over the limit. `Retry-After` says when | Yes, after waiting | | `500` | `api_error` | Our fault. `request_id` is in the body; quote it | Yes, with backoff | | `502` | `api_error` | The upstream model failed. **You are not charged.** Usually transient | Yes, with backoff | > **402 is a 402, not a 429** > > OpenAI returns `429` with `insufficient_quota` for an exhausted balance. We keep the type and use > `402`, because `429` means "slow down and retry" and retrying an empty balance is a loop. A client > reading `type` gets OpenAI's semantics; a client reading only the status gets the more honest of the > two. ### Reading one **Python** ```python import openai try: reply = client.chat.completions.create(model="llama-3.1-8b", messages=messages) except openai.APIStatusError as e: print(e.status_code) # 402 print(e.message) # "Insufficient balance: $0.001230 required, …" print(e.body["error"]["type"]) # "insufficient_quota" print(e.response.headers["x-request-id"]) ``` **Node** ```javascript import OpenAI from "openai"; try { await client.chat.completions.create({ model: "llama-3.1-8b", messages }); } catch (err) { if (err instanceof OpenAI.APIError) { console.log(err.status, err.message, err.type, err.headers["x-request-id"]); } } ``` > **Warning: If you tried this before 30 July 2026** > > Errors used to be returned as `{"ok": false, "error": {"code", "message"}}` on this surface, with no > `type` and with `message` one level from where the SDKs look. So `e.message` was empty and > `e.body["error"]["type"]` did not exist, and a `402` surfaced as an error with nothing in it. That is > fixed. If you wrote a workaround, you can delete it. ### Report an error Every response carries `x-request-id`, and `/v1/chat/completions` also returns it as `dawnstack.request_id` in the body. Quote it. See [Request ids](/docs/request-ids). ### A note on the dashboard surface `/api/v1/*` uses a different envelope, `{"ok": false, "error": {...}}`. That is the dashboard's own contract, it is not part of the public API, and you should not write against it. --- ## Idempotency Source: https://dawnstackai.com/docs/idempotency Retry a request safely without paying twice. Stripe's convention, backed by a database constraint rather than a cache read. A network timeout does not tell you whether the work happened. Send an `Idempotency-Key` and the retry is safe. **curl** ```bash curl https://api.dawnstackai.com/v1/chat/completions \ -H "Authorization: Bearer $DAWNSTACK_API_KEY" \ -H "Idempotency-Key: 8f2a1c94-6e3b-4d5a-9c11-2f7e0a1b3d44" \ -H "Content-Type: application/json" \ -d '{"model":"llama-3.1-8b","messages":[{"role":"user","content":"Hello"}]}' ``` **Python** ```python import uuid key = str(uuid.uuid4()) # generated ONCE, reused across every retry of this request reply = client.chat.completions.create( model="llama-3.1-8b", messages=[{"role": "user", "content": "Hello"}], extra_headers={"Idempotency-Key": key}, ) ``` Generate the key once, before the first attempt, and reuse it for every retry of that same request. A key generated inside the retry loop protects nothing. ### What happens on a repeat | Situation | Response | | --- | --- | | Same key, same body, first request finished | The **original response**, replayed, free. Carries `Idempotent-Replay: true` | | Same key, same body, first request still running | `409`. Wait a moment and retry | | Same key, **different** body | `422`. A key identifies one request, not a slot | | Same key, more than 24 hours later | Treated as new. Records expire | A replay is byte-identical to the original, including the `id`, the token counts and the cost. It is not re-run and it is not charged again. ### The claim is a database constraint > **Note** > > This was built on a key-value cache first and measurably did not work. That store is eventually > consistent, so two simultaneous retries both read "no claim", both proceeded, and both were billed. > Measured before the fix: two concurrent requests sharing a key were charged $0.00027 each. After: > three concurrent requests, one served, two refused, charged once. > > The claim is now a composite primary key insert, so the database picks the winner. If anyone ever > moves it back to a cache, the double charge comes back with it. Two consequences you can rely on: - **A failed request releases its key.** A crash does not lock you out of the retry the failure was inviting. - **A claim that is never completed expires after five minutes.** A worker that dies mid-request does not leave a key nobody can ever use. ### Not with streaming `Idempotency-Key` plus `stream: true` is a `400`. Replaying a stream from cache would mean buffering every response on the chance of a retry. Refusing is honest; accepting the header and quietly ignoring it would let you believe you were protected when you were not. ### When to use it Any request where a double charge matters and a retry is plausible: a queue worker, a webhook handler, anything behind a proxy with its own timeout. It costs nothing when nothing goes wrong. --- ## Request ids Source: https://dawnstackai.com/docs/request-ids Every response carries one. What to log, where to find it after the fact, and what quoting it lets us do. Every response from every endpoint carries `x-request-id`. Chat completions also return it in the body: ```json "dawnstack": { "cost_micros": 2, "cost_usd": 0.000002, "request_id": "req_8f2a1c" } ``` Both are the same value. The header is there for errors, where there may be no useful body; the body field is there so it lands in your own logs next to the thing it describes without you having to reach for response headers. ### Log it **Python** ```python reply = client.chat.completions.with_raw_response.create( model="llama-3.1-8b", messages=messages, ) print(reply.headers["x-request-id"]) parsed = reply.parse() print(parsed.model_extra["dawnstack"]["request_id"]) ``` **Node** ```javascript const res = await client.chat.completions .create({ model: "llama-3.1-8b", messages }) .withResponse(); console.log(res.response.headers.get("x-request-id")); console.log(res.data.dawnstack.request_id); ``` On an error, the SDKs expose the headers on the exception, and a `500` also puts the id in the error body so there is something to quote even if headers were lost in transit. See [Errors](/docs/errors). ### What it gets you A request id resolves to one usage record: the model asked for, the model that actually served it, token counts, what it cost, how long it took, and whether the settle succeeded. Quoting one turns "a request failed this morning" into a lookup. Without it we have your account and a time range, which is a search. ### It is not a secret It identifies a request, not you, and it carries no key material. Paste it into an issue. --- ## Not supported yet Source: https://dawnstackai.com/docs/not-supported-yet The OpenAI endpoints and parameters this API does not implement, stated plainly, so you find out here rather than at a 404. Chat completions is the surface. Everything below is absent, and knowing that now is worth more than discovering it at a `404`. ### Endpoints | Endpoint | Status | | --- | --- | | `POST /v1/chat/completions` | Implemented | | `GET /v1/models` | Implemented, public | | `GET /v1/models/{id}` | Implemented, public | | `POST /v1/embeddings` | Not implemented | | `POST /v1/completions` (legacy) | Not implemented, and not planned. Use chat completions | | `POST /v1/images/generations` | Not implemented | | `POST /v1/audio/transcriptions` | Implemented. `srt` and `vtt` output are not | | `POST /v1/audio/speech` | Implemented. Streaming synthesis is not | | `POST /v1/moderations` | Not implemented | | `POST /v1/responses` | Not implemented | | Assistants, threads, files, batches, fine-tuning | Not implemented | ### Parameters | Parameter | Status | | --- | --- | | `n` | `n: 1` is accepted and is what we already do. Above 1 is a `400`. Ask twice | | `logit_bias` | Refused with a `400`. It would change your output, so accepting and dropping it is not an option we will take | | `user` | Accepted and ignored. We already attribute usage to your key | | `store`, `metadata`, `safety_identifier` | Accepted and ignored. Bookkeeping fields that change neither output nor bill | | `service_tier` | Accepted and ignored. There is no priority lane, and nothing here will claim one | **Anything else in the request body is refused with a `400` naming it**, before any billable work. The same applies when a **model** does not support something you sent. Both carry the code `unsupported_parameter`. See [Errors](/docs/errors). This page said the opposite until August 2026: that unrecognised fields were passed through to the provider. Neither was true. They were silently dropped, which is how a customer came to cap their output with `max_completion_tokens`, have the field discarded, and be billed for a full-length reply. A parameter you send is now either acted on or refused by name, and the short list above is the only set that is quietly accepted, because none of it can change what you get or what you pay. ### Metering is unit-agnostic, and speech proved it Every usage record carries a `unit` rather than assuming tokens. Today that is `token` for text, `second` for transcription and `character` for synthesis. This was built before there was anything but text to meter, on the argument that hardcoding tokens would force a rewrite of the billing path on the day audio shipped. Audio shipped on 31 July 2026 and the pricing function needed no new arithmetic — the same line prices all three units, because the unit changes what the caller counts, not how the counting is priced. > **What we will not do** > > Announce a date. `BRAND.md` forbids promising what is not live, and "coming soon" on a docs page is > the most expensive version of that. When something ships it appears in the > [changelog](/docs/changelog). > > If you need one of these, tell us what you are building. That is more useful to us than a roadmap > vote, and it is how the order actually gets decided. --- ## Changelog Source: https://dawnstackai.com/docs/changelog Every change to the public API that could affect your code. Model ids, prices, response fields and error shapes, newest first. Breaking changes are marked. Everything else is additive. ### 6 August 2026 All of this came from one customer integrating against us and writing down what they found. If you hit something similar, [tell us](/contact). **Fixed — `max_completion_tokens` is honoured.** OpenAI's newer spelling for `max_tokens` was not recognised, and unrecognised fields were silently dropped, so a request that capped output at 5 tokens produced a full-length reply and a full-length bill. Send either spelling. Sending both with different values is now a `400`, because two contradictory caps have no correct silent winner. **Breaking — an unknown parameter is now a `400` instead of being ignored.** This is what the docs promised and the code did not do. If you are sending a field we do not implement, you will now see `unsupported_parameter` naming it rather than a `200` that quietly did something else. `user`, `store`, `metadata`, `service_tier`, `safety_identifier` and `n: 1` are the exception: accepted and ignored, because none of them can change your output or your bill. `logit_bias` and `n > 1` are refused, because they would. See [Not supported yet](/docs/not-supported-yet#parameters). **Additive — `input_modalities` on `GET /v1/models`.** `["text"]` or `["text", "image"]`. Vision models publish `modality: "text"`, because `modality` says which endpoint serves the model and a vision model is still a chat model billed per token, so filtering on it for image support found nothing. Filter on `input_modalities` instead. **Fixed — sending an image to a model that cannot read one is now a `400`.** It was forwarded to the model and billed, and came back as a confused text answer. Seven models accept images; see [Images](/docs/chat-completions#images). **Fixed — `tool_calls` is omitted on ordinary replies** rather than sent as an empty array. Our supplier returns `[]` on non-tool responses and we were passing it through. OpenAI omits the key entirely, and strict typed deserializers treat absent and empty as different things. **Fixed — published speed figures no longer include a cold start.** The nightly benchmark took two samples and reported the median, and the median of two values is their mean, so one cold load was averaged into the published number. One model advertised 8787ms to first token against 89 to 362ms for the rest of the catalogue. The first of the two nightly probes is now discarded as a warm-up, which is where a cold load lands. ### 3 August 2026 **Additive — a streamed response now reports what it cost.** The `include_usage` frame carries a `dawnstack` block alongside `usage`, matching the non-streaming response: ```json "dawnstack": { "cost_micros": 1, "cost_usd": 0.000001, "request_id": "req_8f2a1c" } ``` Until now only the non-streaming response reported a cost, so a streaming client had no way to learn what a call cost without recomputing it from `GET /v1/models`. The figure is priced from the token counts in the same frame by the same function that settles the charge, so it is what you are billed, not an approximation of it. It rides on the usage frame, so you still have to send `stream_options: {"include_usage": true}` to see it. See [Streaming](/docs/streaming). ### 31 July 2026 **Speech, on OpenAI's own paths.** `POST /v1/audio/transcriptions` and `POST /v1/audio/speech` are live. An existing OpenAI audio client needs a new base URL and nothing else. Two models: `whisper-large-v3-turbo` (MIT) for transcription, `kokoro-82m` (Apache-2.0) for synthesis. See [Transcriptions](/docs/transcriptions) and [Speech](/docs/speech). **Additive — `pricing.unit` now returns `second` and `character`, not only `token`.** Transcription bills per second of audio submitted, rounded up; synthesis bills per character of input. If you parse `GET /v1/models`, do not assume `unit` is `token` — it always carried the unit, and now it carries three of them. **Additive — `modality` on `GET /v1/models`.** `text`, `stt` or `tts`, so you can pick the right endpoint before calling one. **Additive — `context_window` is now `null` for speech models** rather than `0`. Audio models have no context window; their limits are file size and duration. Text models are unaffected. ### 30 July 2026 **Breaking — `dawnstack.credits` is now `dawnstack.cost_micros` and `dawnstack.cost_usd`.** Credits were US dollars multiplied by 100,000 and nothing else, so the unit carried no information the dollar figure did not. The additive block now reads: ```json "dawnstack": { "cost_micros": 2, "cost_usd": 0.000002, "request_id": "req_8f2a1c" } ``` One micro-dollar is $0.000001. An existing balance was migrated exactly; nothing changed in value. See [Pricing and billing](/docs/pricing). **Breaking — errors on `/v1` now use OpenAI's envelope.** They previously returned `{"ok": false, "error": {"code", "message"}}`, which has no `type` and puts `message` one level from where the SDKs read it, so `e.message` was empty on every failure. Now: ```json { "error": { "message": "…", "type": "insufficient_quota", "param": null, "code": "INSUFFICIENT_BALANCE" } } ``` If you wrote a workaround for empty error messages, you can delete it. See [Errors](/docs/errors). **Fixed — tool calls now stream.** `delta.tool_calls` was dropped by the frame rewriter, so a streamed tool call arrived as an empty stream ending in `finish_reason: "stop"`. If you concluded tool calling did not work with `stream: true`, it does now. `finish_reason` is also no longer flattened to `"stop"`, so `length` and `tool_calls` come through. **Added — `stream_options: { include_usage: true }`.** It was silently stripped before, so a stream never carried token counts. See [Streaming](/docs/streaming). **Added — `GET /v1/models/{id}`,** OpenAI's retrieve-model endpoint. `client.models.retrieve()` used to 404. **Added — response headers.** `Retry-After` and `X-RateLimit-Reset` on `429`, `WWW-Authenticate` on `401`, and the leading `delta: {"role": "assistant"}` frame on every stream. **Changed — `x-request-id` is the header name everywhere.** The playground used `x-dawnstack-request-id`. **Changed — session-authenticated requests to `/v1/chat/completions` are now rate limited** at 20 a minute per account. They were unlimited. Key-authenticated requests are unaffected at 60 a minute. **Changed — the docs.** This reference replaced a single page. Old `#anchor` links still land on the reference, though not on the exact section. ### 30 July 2026, earlier **Changed — prices fell 53% to 86% on output** across the catalogue, at unchanged margin, when supply moved to a different provider. The catalogue grew from 9 models to 45. > **Warning: Some model ids changed with it** > > Two Llama ids gained a `-Turbo` suffix upstream and were repointed. If you pinned a provider ref > rather than one of our ids, re-check against `GET /v1/models`. **Changed — `GET /v1/models` is now public.** It required an API key, so nobody could read the catalogue or the prices before signing up and no third-party tool could discover us. It also now publishes `supported_parameters`, `quantization`, the maker, and measured speed. **Fixed — `tools` and `response_format` were being silently dropped.** The request schema listed five fields and everything else was stripped, so a caller who sent `tools` got a `200` with prose and no explanation. The full OpenAI parameter set is forwarded now, and `tool_calls` is returned in the response. If you tried tool calling before this date and it appeared to do nothing, that is why. ### 26 July 2026 `POST /v1/chat/completions` went live. --- ## Migrating from OpenAI Source: https://dawnstackai.com/docs/migrating-from-openai Two lines of configuration and a model id. What carries over unchanged, what differs, and the three things worth checking before you switch traffic. The base URL and the key. Keep the `openai` SDK, keep your request and response handling, keep your streaming loop and your tool-calling round trip. **Python** ```python from openai import OpenAI client = OpenAI( api_key=os.environ["DAWNSTACK_API_KEY"], base_url="https://api.dawnstackai.com/v1", ) ``` **Node** ```javascript import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.DAWNSTACK_API_KEY, baseURL: "https://api.dawnstackai.com/v1", }); ``` **Env only** ```bash # Most tools read these, so often no code change at all. export OPENAI_API_KEY="$DAWNSTACK_API_KEY" export OPENAI_BASE_URL="https://api.dawnstackai.com/v1" ``` ### What carries over unchanged Chat completions, streaming, tool calling and the round trip, structured output, the sampling parameters, `logprobs`, the `usage` object, `finish_reason`, `Idempotency-Key`, and the error shape. ### What you have to change **The model id.** There is no `gpt-4o` here. Pick from [Models](/docs/models), or use an alias and let the choice move as better models appear. | If you were using | A reasonable starting point | | --- | --- | | A small fast model for classification or extraction | `llama-3.1-8b` | | A general-purpose mid-size model | `llama-3.3-70b`, `gpt-oss-120b` | | A reasoning model | See the models marked as reasoning on [Models](/docs/models) | | Anything where the prompt is not English | `dawn-multilingual`, or read the per-language cost on the models page | These are starting points, not equivalences. Run your own evaluation. ### Three things to check before you switch traffic 1. **Capabilities are per model and measured.** Check `Can do` on [Models](/docs/models) before assuming `tools` or `response_format` will work. Sending one to a model that does not declare it is a `400` rather than a quiet fallback to prose. 2. **Reasoning tokens are billed and can dominate a short reply.** If you are moving a high-volume, short-answer workload, compare per *request* rather than per token. 3. **Language cost is not flat.** Swahili and Amharic tokenize less efficiently than English, so the same sentence costs more. We publish the multiplier per model rather than hiding it in an average. ### What is missing Embeddings, images, moderations, the Responses API, assistants and batches. `n > 1` is not supported. Audio is present: `/v1/audio/transcriptions` and `/v1/audio/speech` both work on the paths your client already calls. The full list is on [Not supported yet](/docs/not-supported-yet). If your app uses embeddings alongside chat, keep that half where it is; nothing stops you pointing two clients at two providers. ### Errors `402` means an empty balance rather than a billing problem with a card on file, and it carries `type: "insufficient_quota"` so an SDK's own handling still fires. There is no organisation-level quota and no per-model rate limit: the limit is 60 requests a minute per key. See [Errors](/docs/errors) and [Rate limits](/docs/rate-limits). > **Running both in parallel** > > Two clients, two base URLs, and route on a flag. It is the cheapest way to compare quality and cost > on your own traffic rather than on a benchmark, and it means the rollback is a config change. --- ## Migrating from OpenRouter Source: https://dawnstackai.com/docs/migrating-from-openrouter Both are OpenAI-compatible, so the code does not change. What does change is the model ids, the routing behaviour, and how you pay. Both endpoints speak the OpenAI protocol, so your request and response handling, your streaming loop and your tool-calling round trip all carry over untouched. Three things do change: the model ids, the routing, and the payment. ### The two lines **Python** ```python from openai import OpenAI client = OpenAI( api_key=os.environ["DAWNSTACK_API_KEY"], base_url="https://api.dawnstackai.com/v1", ) ``` **Node** ```ts import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.DAWNSTACK_API_KEY, baseURL: "https://api.dawnstackai.com/v1", }); ``` **Env only** ```bash OPENAI_API_KEY=sk-dawn-... OPENAI_BASE_URL=https://api.dawnstackai.com/v1 ``` ### Model ids drop the provider prefix OpenRouter namespaces every model by the company that trained it. We use the id from our own catalogue, which you can read without an API key at [`GET /v1/models`](https://api.dawnstackai.com/v1/models) or on the [models page](/models). | OpenRouter | Dawnstack | |---|---| | `meta-llama/llama-3.1-8b-instruct` | `llama-3.1-8b` | | `mistralai/mistral-nemo` | `mistral-nemo` | | `google/gemma-3-4b-it` | `gemma-3-4b` | | `deepseek/deepseek-chat` | `deepseek-v3.2` | | `openai/gpt-oss-20b` | `gpt-oss-20b` | An unknown id is a `404` with the id echoed back, not a silent substitution. > **Note** > > The catalogues are not the same size. We serve open-weight models only, so if your integration calls > a closed model there is no equivalent id here yet. Check the models page before you switch traffic. ### Routing is explicit, and there is no fallback This is the substantive behavioural difference, and it cuts both ways. OpenRouter routes a request across several upstream providers and can fail over between them. We do not: a model id maps to one backend, and if that backend is failing your request fails rather than being served by somebody else at a different price and a different quantisation. That means: - **`provider` preferences, `route: "fallback"` and `models: [...]` arrays do nothing here.** They are not part of the OpenAI protocol, and we reject an unsupported parameter by name rather than ignoring it, so you will see a `400` naming the field instead of a quietly different result. - **Reproducibility is the trade.** The same id gets you the same weights at the same quantisation every time, and the quantisation is published per model. - **Retries are yours.** If you relied on fallback for availability, keep a second model id in your own config and switch on error. ### Aliases, if you want a choice made for you `dawn-swahili-best` and `dawn-multilingual` resolve to whichever model currently measures best for that job. They are opt-in **by name only**: nothing inspects your prompt and quietly picks a different model, because that would change your output, your bill and your reproducibility with no way for you to know why. The usage row records both the id you asked for and the id that served, so a bill stays explicable after an alias is repointed. ### Paying OpenRouter takes a dollar-denominated card. We take a prepaid balance, topped up in local currency through mobile money and cards, from $1.00. There is no monthly commitment and no minimum spend. New accounts get a starter balance, which is enough to port your code and see real output on the cheaper models before paying anything. ### Three things to check before you switch traffic 1. **Every model id you call exists here.** `GET /v1/models` is public, so this is one script. 2. **The capabilities you use are declared.** Tool calling and structured output are **measured** per model rather than copied from a supplier's catalogue, and asking for one a model does not declare is a `400` before any paid work. See [Tool calling](/docs/tool-calling) and [Structured output](/docs/structured-output). 3. **Your error handling reads `error.type`.** Our envelope is OpenAI's, and an empty balance is a `402` with `insufficient_quota` rather than a `429`. ### What is not here yet Written plainly, because finding out at request time is worse: no closed-weight models, no provider-preference routing, no prompt-caching discount, and no request logging you can browse. The current list is in [Not supported yet](/docs/not-supported-yet). --- ## SDK and framework compatibility Source: https://dawnstackai.com/docs/sdk-compatibility Working configuration for the OpenAI SDKs, LangChain, LlamaIndex, the Vercel AI SDK and LiteLLM, plus the two settings that trip people up. Anything that speaks the OpenAI chat completions API works. Point it at `https://api.dawnstackai.com/v1` with a Dawnstack key. ### OpenAI SDKs **Python** ```python from openai import OpenAI client = OpenAI( api_key=os.environ["DAWNSTACK_API_KEY"], base_url="https://api.dawnstackai.com/v1", max_retries=3, ) ``` **Node** ```javascript import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.DAWNSTACK_API_KEY, baseURL: "https://api.dawnstackai.com/v1", maxRetries: 3, }); ``` The SDKs retry `429` and `5xx` with backoff on their own. They do not retry `402`, which is correct: an empty balance does not fix itself. ### LangChain **Python** ```python from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="llama-3.3-70b", api_key=os.environ["DAWNSTACK_API_KEY"], base_url="https://api.dawnstackai.com/v1", ) ``` **JS** ```javascript import { ChatOpenAI } from "@langchain/openai"; const llm = new ChatOpenAI({ model: "llama-3.3-70b", apiKey: process.env.DAWNSTACK_API_KEY, configuration: { baseURL: "https://api.dawnstackai.com/v1" }, }); ``` Use `langchain-openai`, not a provider-specific package. Tool calling and structured output work through it, on models that declare support. ### LlamaIndex **Python** ```python from llama_index.llms.openai_like import OpenAILike llm = OpenAILike( model="llama-3.3-70b", api_key=os.environ["DAWNSTACK_API_KEY"], api_base="https://api.dawnstackai.com/v1", is_chat_model=True, is_function_calling_model=True, ) ``` > **Warning: is_chat_model=True is not optional** > > `OpenAILike` defaults it to `False` and then calls the legacy `/v1/completions` endpoint, which does > not exist here. The symptom is a `404` that looks like a bad base URL. ### Vercel AI SDK **TypeScript** ```typescript import { createOpenAI } from "@ai-sdk/openai"; import { streamText } from "ai"; const dawnstack = createOpenAI({ apiKey: process.env.DAWNSTACK_API_KEY, baseURL: "https://api.dawnstackai.com/v1", compatibility: "compatible", }); const result = streamText({ model: dawnstack("llama-3.3-70b"), prompt: "Explain server-sent events in two sentences.", }); ``` > **Note** > > The default, `"strict"`, sends OpenAI-only fields that a compatible provider is not obliged to > implement. `"compatible"` is what the SDK documents for every non-OpenAI endpoint. ### LiteLLM **Python** ```python from litellm import completion response = completion( model="openai/llama-3.3-70b", messages=[{"role": "user", "content": "Hello"}], api_key=os.environ["DAWNSTACK_API_KEY"], api_base="https://api.dawnstackai.com/v1", ) ``` The `openai/` prefix routes it through LiteLLM's OpenAI handler. Without it, LiteLLM tries to infer the provider from the model name and fails. ### Anything else If a tool takes a base URL and a key, it works. Two settings cause almost every failure: - **A trailing path.** The base URL is `https://api.dawnstackai.com/v1`, and most clients append `/chat/completions` themselves. Including it yourself produces a `404` on `/v1/chat/completions/chat/completions`. - **A legacy completions call.** Some wrappers default to `/v1/completions`, which is not implemented. Look for a "chat model" flag. If something still does not work, tell us what client and what version. That is the kind of report that turns into a fix rather than a workaround. --- ## Not covered here, deliberately `/api/v1/*` is the dashboard's own surface. It is session-authenticated, it is not versioned for third parties, and it will change without notice. **Do not write against it.** Everything a developer is meant to call lives under `/v1`. Human version of all of the above: https://dawnstackai.com/docs