Skip to content
main-compute

Reference

Beta gaps

API reference

The frozen /v1 contract: models, chat, embeddings, responses and SSE.

Updated 2026-10-25 · MainAI Compute docs

This page renders the frozen MVP contract. Base URL: https://cloud.mainai.dk/v1. All request and response bodies are JSON unless a stream is requested. Timestamps are UTC; money is EUR cents in portal responses and EUR per million tokens in prices.

Endpoint mapLink to this section

GET

Path
/v1/models
Purpose
List allow-listed models

POST

Path
/v1/chat/completions
Purpose
Chat, JSON or SSE

POST

Path
/v1/embeddings
Purpose
Text embeddings

POST

Path
/v1/responses
Purpose
Responses API passthrough

POST

Path
/v1/completions
Purpose
Legacy text completion (beta compat)

Common headersLink to this section

Authorization

Required
Yes
Notes
Bearer sk-mai_…

Content-Type

Required
POST
Notes
application/json

Idempotency-Key

Required
No
Notes
Forwarded for safe retry of non-streaming POSTs

OpenAI-Beta

Required
No
Notes
Forwarded when a client sends it

x-request-id

Required
No
Notes
Caller correlation hint; MainAI returns its own value

Responses carry x-request-id. They may also carry x-mainai-model, x-mainai-cache and x-mainai-remaining-requests. No x-mainai-*, server or upstream version header is exposed. CORS is open for the documented methods and headers.

ModelsLink to this section

GET/v1/models

List models available to a key

Returns public offers that are active, probed and routing-synced, filtered to the key allow-list. The response hides provider routing and private addresses. DKK is display-only.

Request headers and body

Authorizationstringyes

Bearer sk-mai_…. The key must be active and the model must be in its allow-list.

x-request-idstringno

Optional caller-supplied correlation ID. MainAI always returns its own x-request-id.

Request
curl -s https://cloud.mainai.dk/v1/models \
  -H "Authorization: Bearer $MAINAI_API_KEY"

Response fields

object"list"yes

Always list.

dataarrayyes

Model objects.

data[].idstringyes

Model ID to send in inference requests.

data[].object"model"yes

Always model.

data[].owned_bystringyes

Publisher label. Always mainai.

data[].context_windowintegeryes

Maximum combined input and output context.

data[].max_outputintegeryes

Maximum generated tokens for one request.

data[].access"enterprise"yes

Commercial access tier; both public offers are Enterprise.

data[].line"thinker" or "enterprise"yes

Product line selected by staff.

data[].plan"enterprise"yes

Required commercial plan.

data[].availablebooleanyes

Whether the calling organization can use a listed model. A blocked organization is rejected before listing, so listed rows are true; out-of-scope models are omitted.

data[].pricingobjectyes

EUR per million tokens: input, cached input, cache write and output.

data[].pricing.*_per_million_eurnumberyes

Exact published rate for the named token class.

data[].pricing_referenceobjectyes

Display-only fixed DKK reference. Charged in EUR.

data[].pricing_reference.ratenumberyes

Fixed rate, currently 7.46 DKK per EUR.

200 response · application/json · private, max-age=0, must-revalidate
{
  "object": "list",
  "data": [
    {
      "id": "mainai-coder",
      "object": "model",
      "owned_by": "mainai",
      "context_window": 1000000,
      "max_output": 393216,
      "access": "enterprise",
      "line": "thinker",
      "plan": "enterprise",
      "available": true,
      "pricing": {"input_per_million_eur":0.60,"cached_input_per_million_eur":0.12,"cache_write_per_million_eur":0,"output_per_million_eur":1.80},
      "pricing_reference": {"currency":"DKK","base_currency":"EUR","rate":7.46,"per_million_micros":{"input":4476000,"cached_input":895200,"output":13428000,"cache_write":0}}
    },
    {
      "id": "mainai-cyber",
      "object": "model",
      "owned_by": "mainai",
      "context_window": 1000000,
      "max_output": 393216,
      "access": "enterprise",
      "line": "thinker",
      "plan": "enterprise",
      "available": true,
      "pricing": {"input_per_million_eur":0.60,"cached_input_per_million_eur":0.12,"cache_write_per_million_eur":0,"output_per_million_eur":1.80},
      "pricing_reference": {"currency":"DKK","base_currency":"EUR","rate":7.46,"per_million_micros":{"input":4476000,"cached_input":895200,"output":13428000,"cache_write":0}}
    }
  ]
}
  • The list is scoped to the key. A model outside the key allow-list is omitted rather than denied.
  • Responses are cacheable for 60 seconds. Do not use this endpoint as a health check.

Statuses

200
Model list returned.
401
Missing, malformed, revoked or expired key.
403
Organization suspended, KYC pending or Enterprise contract missing.
500
Portal catalog unavailable.

Chat completionsLink to this section

POST/v1/chat/completions

Create a chat completion

OpenAI-compatible chat completion. Supports non-streaming JSON and server-sent events. Fields are passed to the selected engine; unsupported fields may be ignored by the model runtime.

Request headers and body

Authorizationstringyes

Bearer sk-mai_…. The key must be active and the model must be in its allow-list.

Content-Typestringyes

application/json.

modelstringyes

A model ID from /v1/models.

messagesarrayyes

Conversation turns. Each item has role (system, user, assistant, tool) and content (string or content-part array).

streambooleanno

When true, returns text/event-stream. Default false.

stream_optionsobjectno

Set { "include_usage": true } to receive a final usage chunk on streams.

max_tokensintegerno

Output token cap. Bounded by the model's maximum output.

temperaturenumberno

Sampling temperature.

top_pnumberno

Nucleus sampling probability mass.

stopstring or arrayno

Up to four stop sequences.

seedintegerno

Best-effort deterministic sampling.

toolsarrayno

Tool definitions. Tool calling depends on model capability.

tool_choicestring or objectno

auto, none, required or a named tool.

response_formatobjectno

{ "type": "json_object" } for JSON mode when supported by the model.

presence_penaltynumberno

−2.0 to 2.0.

frequency_penaltynumberno

−2.0 to 2.0.

userstringno

Caller-side end-user identifier for correlation.

Request
curl -s https://cloud.mainai.dk/v1/chat/completions \
  -H "Authorization: Bearer $MAINAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "mainai-coder",
    "messages": [
      { "role": "system", "content": "Answer with a single paragraph." },
      { "role": "user", "content": "What does the API layer own in this architecture?" }
    ],
    "max_tokens": 300,
    "temperature": 0.2
  }'

Response fields

idstringyes

Completion identifier (chatcmpl_…).

object"chat.completion"yes

Object type.

createdintegeryes

Unix timestamp in seconds.

modelstringyes

Served model ID.

choicesarrayyes

Completion candidates.

choices[].indexintegeryes

Candidate index.

choices[].messageobjectyes

Assistant message with role and content; may include tool_calls.

choices[].finish_reasonstringyes

stop, length, tool_calls, content_filter or null while streaming.

usageobjectyes

prompt_tokens, completion_tokens, total_tokens.

usage.prompt_tokens_details.cached_tokensintegerno

Prefix-cached input tokens, when the engine reports them.

usage.cache_creation_tokensintegerno

Tokens written to a provider cache, when reported.

200 response · application/json
{
  "id": "chatcmpl_mai_01j9x4",
  "object": "chat.completion",
  "created": 1770000000,
  "model": "mainai-coder",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The API layer owns the public /v1 contract, key checks and header sanitation."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 42,
    "completion_tokens": 21,
    "total_tokens": 63,
    "prompt_tokens_details": { "cached_tokens": 0 }
  }
}

Streaming

With stream: true, the response is text/event-stream. Each event carries one JSON chunk; the final data frame is [DONE]. Chunk delta.content is incremental. When stream_options.include_usage is set, a final chunk with an empty choices array carries usage.

Event stream (abridged)
event: message
data: {"id":"chatcmpl_mai_01j9x4","object":"chat.completion.chunk","created":1770000000,"model":"mainai-coder","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

event: message
data: {"id":"chatcmpl_mai_01j9x4","object":"chat.completion.chunk","created":1770000000,"model":"mainai-coder","choices":[{"index":0,"delta":{"content":"The API"},"finish_reason":null}]}

event: message
data: {"id":"chatcmpl_mai_01j9x4","object":"chat.completion.chunk","created":1770000000,"model":"mainai-coder","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]
  • Use model exactly as returned by /v1/models.
  • One concurrent stream per key is soft-limited by the router.
  • The routing layer keeps an SSE stream open for up to 300 seconds of idle time.

Statuses

200
Completion or event stream.
400
Invalid JSON or invalid request fields.
401
Missing, revoked or expired key.
402
Wallet or key budget exhausted.
403
Model gate, key allow-list or organization suspended.
404
Unknown model ID.
413
Request body larger than 8 MiB.
429
Key or route rate limit.
503
Engine unavailable or connect timeout.
504
Upstream timeout.

EmbeddingsLink to this section

POST/v1/embeddings

Create embeddings

OpenAI-compatible embeddings. Use the embedding model in the key allow-list. Input may be a string or an array of strings or token arrays, depending on engine support.

Request headers and body

Authorizationstringyes

Bearer sk-mai_…. The key must be active and the model must be in its allow-list.

Content-Typestringyes

application/json.

modelstringyes

An embedding model, for example mainai-coder.

inputstring or arrayyes

Text to embed. Arrays are batched.

encoding_format"float" or "base64"no

Defaults to float.

dimensionsintegerno

Requested output dimensions when the model supports truncation.

userstringno

Caller-side end-user identifier.

Request
curl -s https://cloud.mainai.dk/v1/embeddings \
  -H "Authorization: Bearer $MAINAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "mainai-coder",
    "input": ["first document", "second document"],
    "encoding_format": "float"
  }'

Response fields

object"list"yes

Always list.

dataarrayyes

One item per input.

data[].object"embedding"yes

Object type.

data[].indexintegeryes

Input index.

data[].embeddingarrayyes

Vector of floats, or base64 string when requested.

modelstringyes

Served model.

usageobjectyes

prompt_tokens and total_tokens.

200 response · application/json
{
  "object": "list",
  "data": [
    { "object": "embedding", "index": 0, "embedding": [0.0123, -0.0456, 0.0789] },
    { "object": "embedding", "index": 1, "embedding": [0.0321, 0.0110, -0.0678] }
  ],
  "model": "mainai-coder",
  "usage": { "prompt_tokens": 8, "total_tokens": 8 }
}
  • Vector length and normalization are fixed by the model. Store the model ID with each vector.

Statuses

200
Embeddings returned.
400
Invalid input or batch too large.
401
Missing, revoked or expired key.
402
Wallet or key budget exhausted.
403
Model gate, key allow-list or organization suspended.
404
Unknown model ID.
413
Request body larger than 8 MiB.
429
Key or route rate limit.
503
Engine unavailable.

ResponsesLink to this section

POST/v1/responses

Create a response

OpenAI Responses API shape, proxied without translation. Use it when the client already speaks Responses; chat completions is the better-documented path for new integrations. Streaming uses named SSE events.

Request headers and body

Authorizationstringyes

Bearer sk-mai_…. The key must be active and the model must be in its allow-list.

Content-Typestringyes

application/json.

modelstringyes

A model ID from /v1/models.

inputstring or arrayyes

User input as text or input items.

instructionsstringno

System-level instructions.

max_output_tokensintegerno

Output cap.

streambooleanno

When true, emits typed SSE events.

toolsarrayno

Tool definitions, when supported by the model.

storebooleanno

Passed through; portal persistence is not guaranteed.

Request
curl -s https://cloud.mainai.dk/v1/responses \
  -H "Authorization: Bearer $MAINAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "mainai-cyber",
    "input": "Outline three checks before a production database restore.",
    "max_output_tokens": 350
  }'

Response fields

idstringyes

Response identifier (resp_…).

object"response"yes

Object type.

created_atintegeryes

Unix timestamp in seconds.

statusstringyes

completed, in_progress, failed or incomplete.

modelstringyes

Served model.

outputarrayyes

Output items; text lives in message content parts.

usageobjectno

Token usage, when the engine reports it.

200 response · application/json (abridged)
{
  "id": "resp_mai_01j9x5",
  "object": "response",
  "created_at": 1770000000,
  "status": "completed",
  "model": "mainai-cyber",
  "output": [
    {
      "type": "message",
      "role": "assistant",
      "content": [{ "type": "output_text", "text": "1. Verify the backup checksum…" }]
    }
  ],
  "usage": { "input_tokens": 18, "output_tokens": 96, "total_tokens": 114 }
}

Streaming

Responses streaming is passthrough. Expect named events such as response.created, response.output_text.delta and response.completed, each with a data: JSON line.

Event stream (abridged)
event: response.created
data: {"type":"response.created","response":{"id":"resp_mai_01j9x5","status":"in_progress"}}

event: response.output_text.delta
data: {"type":"response.output_text.delta","delta":"1. Verify"}

event: response.completed
data: {"type":"response.completed","response":{"id":"resp_mai_01j9x5","status":"completed"}}
  • Responses is a passthrough surface. Feature coverage follows the engine behind the model.
  • Prefer chat completions for tool-calling production code until Responses parity is tested for your model.

Statuses

200
Response or event stream.
400
Invalid JSON or unsupported field.
401
Missing, revoked or expired key.
402
Wallet or key budget exhausted.
403
Model gate, key allow-list or organization suspended.
404
Unknown model ID.
429
Rate limit.
503
Engine unavailable.

Legacy completionsLink to this section

POST/v1/completions

Create a legacy text completion

Legacy completions endpoint accepted by the API and passed through. It is not part of the frozen contract table; treat it as a compatibility surface.

Request headers and body

Authorizationstringyes

Bearer sk-mai_…. The key must be active and the model must be in its allow-list.

modelstringyes

A model ID from /v1/models.

promptstring or arrayyes

Text prompt.

max_tokensintegerno

Output cap.

temperaturenumberno

Sampling temperature.

streambooleanno

SSE when supported by the engine.

Request
curl -s https://cloud.mainai.dk/v1/completions \
  -H "Authorization: Bearer $MAINAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "mainai-coder", "prompt": "Write a one-line health check.", "max_tokens": 40 }'

Response fields

idstringyes

Completion identifier.

object"text_completion"yes

Object type.

choices[].textstringyes

Generated text.

usageobjectyes

Token counts.

200 response · application/json (abridged)
{
  "id": "cmpl_mai_01j9x6",
  "object": "text_completion",
  "choices": [{ "index": 0, "text": "curl -fsS /health", "finish_reason": "stop" }],
  "usage": { "prompt_tokens": 7, "completion_tokens": 6, "total_tokens": 13 }
}
  • New integrations should use chat completions.

Statuses

200
Completion returned.
400
Invalid request.
401
Invalid key.
402
Wallet or key budget exhausted.
403
Model gate, allow-list or organization suspended.
404
Unknown model ID.
503
Engine unavailable.

Server-sent eventsLink to this section

Chat and Responses support streaming. Chat frames are data: <json> lines terminated by data: [DONE]. Responses frames add an event: name. A stream chunk is an ordinary object with a delta instead of a full message.

Stream with curl
curl -N https://cloud.mainai.dk/v1/chat/completions \
  -H "Authorization: Bearer $MAINAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "mainai-coder",
    "messages": [{ "role": "user", "content": "Count to five." }],
    "stream": true,
    "max_tokens": 80
  }'
  • Parse until [DONE]; do not assume the connection closes cleanly.
  • Accumulate choices[0].delta.content.
  • Chat finish_reason arrives on the last content chunk. A stream can end without one if the connection fails.
  • Set stream_options.include_usage when you need final token counts.
  • Do not retry a stream after content has reached the user unless duplication is acceptable.

OpenAI SDK compatibilityLink to this section

The API follows the OpenAI wire shape for the routes above. Point an OpenAI SDK at the /v1 base URL and use a MainAI key. The SDK's own retries, streaming parser and typed objects work as usual.

TypeScript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.MAINAI_API_KEY,
  baseURL: "https://cloud.mainai.dk/v1",
});

const completion = await client.chat.completions.create({
  model: "mainai-coder",
  messages: [{ role: "user", content: "Say ready" }],
  max_tokens: 12,
});

console.log(completion.choices[0]?.message.content);

See SDKs for Python and environment-variable patterns. Not every OpenAI feature is implemented; unsupported fields can be ignored or rejected by the engine.

Error tableLink to this section

400

code
invalid_json
When
Malformed JSON body.

400

code
invalid_request_error
When
Passthrough validation failure.

401

code
invalid_api_key
When
Missing, malformed, revoked or expired key.

402

code
insufficient_credit
When
Wallet balance or key budget exhausted.

403

code
model_not_allowed
When
Model outside the key allow-list.

403

code
kyc_required
When
Identity approval is missing.

403

code
contract_required
When
KYC is approved; Enterprise contract is missing.

403

code
verification_required
When
Compatibility code for an access-tier mismatch.

403

code
org_suspended
When
Organization suspended.

404

code
model_not_found
When
Unknown model ID.

404

code
not_found
When
Unknown /v1 endpoint.

413

code
payload_too_large
When
Body over 8 MiB.

429

code
rate_limit_exceeded
When
Key or gateway rate limit.

500

code
server_error
When
Upstream or portal fault.

502/503

code
upstream_unavailable
When
Gateway or engine unavailable.

504

code
timeout
When
Upstream timeout.
Error shape
{
  "error": {
    "message": "Invalid API key.",
    "type": "auth_error",
    "param": null,
    "code": "invalid_api_key"
  }
}

Compatibility gapsLink to this section

  • No /v1/audio, /v1/images, /v1/files, /v1/batches, /v1/fine_tuning or assistants.
  • No provider-specific extensions are guaranteed.
  • /v1/completions is accepted but absent from the frozen contract table.
  • The model list includes exact EUR pricing and the fixed DKK reference; explanations live in Models.
  • No webhook callbacks for long generations.

Authentication · Errors · Rate limits · Caching