DocsOpen Moon AI
Browse the docs

Chat completions

Reference for POST /v1/chat/completions: parameters, messages, response, SSE streaming and chunk format, tools, images, output length and errors.

On this page

POST /v1/chat/completions is the endpoint for chat models. The request body and the response follow the OpenAI Chat Completions API: the OpenAI SDKs use it with no special setup.

HTTP
POST https://api.realmoon.ai/v1/chat/completions
  • Authentication: an sk-moon-… key with the inference scope (see Authentication).
  • Body: JSON (Content-Type: application/json).
  • Response: a chat.completion object, or an SSE stream of chat.completion.chunk chunks with "stream": true.
  • Billing: pay as you go, from the wallet of the key’s workspace (see Costs).

ExampleLink to the Example section

cURL
curl https://api.realmoon.ai/v1/chat/completions \
  -H "Authorization: Bearer $MOONAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "anthropic/claude-haiku-4.5",
        "messages": [
          {"role": "system", "content": "Answer in one sentence."},
          {"role": "user", "content": "What is an API?"}
        ],
        "max_tokens": 300,
        "temperature": 0.3
      }'
Python
import os
from openai import OpenAI

client = OpenAI(base_url="https://api.realmoon.ai/v1", api_key=os.environ["MOONAI_API_KEY"])

response = client.chat.completions.create(
    model="anthropic/claude-haiku-4.5",
    messages=[
        {"role": "system", "content": "Answer in one sentence."},
        {"role": "user", "content": "What is an API?"},
    ],
    max_tokens=300,
    temperature=0.3,
)
print(response.choices[0].message.content)
JavaScript
import OpenAI from 'openai';

const client = new OpenAI({ baseURL: 'https://api.realmoon.ai/v1', apiKey: process.env.MOONAI_API_KEY });

const response = await client.chat.completions.create({
  model: 'anthropic/claude-haiku-4.5',
  messages: [
    { role: 'system', content: 'Answer in one sentence.' },
    { role: 'user', content: 'What is an API?' },
  ],
  max_tokens: 300,
  temperature: 0.3,
});
console.log(response.choices[0].message.content);

ParametersLink to the Parameters section

The body is a JSON object. The parameters in the table are passed on to the model, except provider and transforms, which are checked separately (see Provider preferences); any other parameter is dropped without an error (see Ignored parameters). A model can reject a parameter it does not support (400 unsupported_parameter).

ParameterTypeRequiredDescription
modelstringyesThe ID of a chat model from GET /v1/models. Missing: 400 missing_required_parameter. Unknown: 404 model_not_found.
messagesarrayyesThe conversation, see Messages. Missing or malformed: 400 unsupported_content.
streambooleannotrue to receive the response as an SSE stream, see Streaming. false by default.
max_tokensintegernoMaximum number of output tokens, reasoning included. See Response length.
max_completion_tokensintegernoSame as max_tokens, under the name used by OpenAI reasoning models.
temperaturenumbernoSampling randomness.
top_pnumbernoNucleus sampling.
top_k, min_p, top_a, repetition_penaltynumbernoSampling settings, for models that accept them.
frequency_penalty, presence_penaltynumbernoRepetition penalties.
stopstring or arraynoSequences that stop generation.
seedintegernoSeed, for models that honor it.
logit_biasobjectnoPer-token bias, for models that accept it.
logprobsbooleannoRequests token probabilities, for models that return them.
top_logprobsintegernoNumber of most likely tokens returned at each position, with logprobs.
response_formatobjectnoJSON output ({"type": "json_object"}) or output that follows a schema ({"type": "json_schema", "json_schema": {…}}), depending on the model.
toolsarraynoFunctions the model can call, see Tools.
tool_choicestring or objectnonone, auto, required, a specific function ({"type": "function", "function": {"name": "…"}}) or {"type": "allowed_tools", …}.
parallel_tool_callsbooleannoAllows several tool calls in a single response.
functions, function_callarray, string or objectnoThe legacy form of tools, still accepted.
reasoning_effortstringnoReasoning effort (for example low, medium, high), for models that accept it.
reasoningobjectnoReasoning settings. Only the effort, max_tokens, exclude and enabled keys are passed on.
include_reasoningbooleannoAsks the model to return its reasoning, when it allows it.
verbositystringnoLevel of detail, for models that accept it.
user, safety_identifier, prompt_cache_keystringnoYour end user’s identifier or a cache key. Replaced with a pseudonym if it contains personal data that Moon Blur detects (see Privacy).
nintegernoOnly 1 is accepted (otherwise 400 n_not_supported).
providerobjectnoProvider preferences, see below.
transformsarrayno["middle-out"] only, see below.

Sampling settings depend on the model: a model may ignore some of them.

Provider preferencesLink to the Provider preferences section

The same model can be served by several providers. The provider object narrows down or orders that choice; it never widens it.

KeyValueEffect
onlyarray of namesServes the request only through these providers.
ignorearray of namesExcludes these providers.
orderarray of namesTries the providers in this order.
quantizationsarray of namesKeeps only these quantization levels.
sortprice, throughput or latencySorts providers by price, throughput or latency.
allow_fallbacksbooleanfalse: no fallback to another provider.
require_parametersbooleantrue: only providers that support every parameter in the request.
max_priceobjectMaximum price you accept, in US dollars: prompt and completion per million tokens, request per request, image per image.
data_collection"deny"Excludes providers that state they reuse data.
zdrbooleantrue: only providers that state they retain nothing.
  • Lists hold 64 names at most.
  • A default value (allow_fallbacks: true, zdr: false, data_collection: "allow", empty list) is ignored.
  • Any other key, or an invalid value, is rejected with 400 unsupported_parameter and a param that names it (for example provider.foo).
  • The maximum price passed on is never higher than the model’s price in /v1/models: a preference never gets the request served at a higher price than the hold covers.
  • Not every model supports these preferences. On a model that does not, for example a model hosted by Moon AI, an active preference is rejected with 400 unsupported_parameter (param: "provider").
  • The data_collection and zdr commitments are the ones providers declare; Moon AI does not verify them.

transforms: ["middle-out"] shortens the middle of a conversation that is too long for the model’s context window. Any other value is rejected (400 unsupported_parameter, param: "transforms"), as is middle-out on a model that does not support provider preferences.

MessagesLink to the Messages section

messages is an array of message objects, from oldest to newest.

FieldTypeRequiredDescription
rolestringyessystem, user, assistant or tool.
contentstring, array of parts, or nullyesThe content. null is allowed for an assistant message that carries only tool_calls.
namestringnoThe author’s name. Replaced with a pseudonym if it contains personal data that Moon Blur detects.
tool_callsarraynoOn an assistant message: the tool calls the model made, sent back as is.
tool_call_idstringin a tool messageThe ID of the call this message answers.

A content array holds parts:

Part typeShapeNotes
text{"type": "text", "text": "…"}Text.
refusal{"type": "refusal", "refusal": "…"}A refusal returned by a model, in an assistant message you send back.
image_url{"type": "image_url", "image_url": {"url": "…", "detail": "auto"}}An https:// or http:// URL, or data:image/…;base64,…. See Images and files.
input_audio{"type": "input_audio", "input_audio": {"data": "<base64>", "format": "wav"}}Base64 audio only, for models that read audio.
file{"type": "file", "file": {"filename": "quote.pdf", "file_data": "data:application/pdf;base64,…"}}A file in base64, as a data: URL, or at an http(s):// URL.

Any other part is rejected with 400 unsupported_content, in particular:

  • a file referenced by its ID (file_id): send its content in file_data instead;
  • a video;
  • audio sent as a file: use an input_audio part;
  • audio on a model whose audio price is not known.

ResponseLink to the Response section

Without streaming, the response is a chat.completion object.

Response
{
  "id": "gen-6f1c2b7e9a4d4c0e8b3a5d2f1e0c9b8a",
  "object": "chat.completion",
  "created": 1793577600,
  "model": "anthropic/claude-haiku-4.5",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "An API is an interface that lets two pieces of software exchange data."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 24, "completion_tokens": 18, "total_tokens": 42 }
}
FieldTypeDescription
idstringThe request ID at Moon AI (gen-…), the same as the X-Moon-Request-Id header. Use it to read the cost with GET /v1/generation.
objectstringchat.completion.
createdintegerTimestamp in Unix seconds.
modelstringThe model you called, or the exact name of the version served. For a Moon AI in-house model, always the Moon AI ID.
choicesarrayA single response (n is always 1).
choices[].message.rolestringassistant.
choices[].message.contentstring or nullThe response text. null when the model only calls tools.
choices[].message.tool_callsarrayThe tool calls the model requests, see Tools.
choices[].message.refusalstringThe model’s refusal, when it declines to answer.
choices[].message.reasoningstringThe reasoning, when the model returns it (reasoning_content on some models).
choices[].finish_reasonstringstop, length (output limit reached), tool_calls, content_filter…
choices[].logprobsobjectToken probabilities, with logprobs: true.
usage.prompt_tokens, usage.completion_tokens, usage.total_tokensintegerTokens counted.
usage.prompt_tokens_details, usage.completion_tokens_detailsobjectDetails the model provides: cached tokens, reasoning tokens.

The response never includes the cost or the provider that served the model: provider, system_fingerprint, native_finish_reason and the cost fields of usage are removed.

Response headersLink to the Response headers section

HeaderValue
X-Moon-Request-Idgen-…, the request ID.
X-Moon-Prompt-Storagenone: the content is not stored.
X-Moon-Bluron if Moon Blur scanned the request, off if it is turned off for the key.
X-Moon-Blur-RedactionsNumber of values Moon Blur replaced, when there are any.

StreamingLink to the Streaming section

With "stream": true, the response arrives as Server-Sent Events (Content-Type: text/event-stream) in the OpenAI format. The HTTP status (200) and the headers go out with the first byte.

cURL
curl -N https://api.realmoon.ai/v1/chat/completions \
  -H "Authorization: Bearer $MOONAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "anthropic/claude-haiku-4.5",
        "messages": [{"role": "user", "content": "Write a haiku about the Moon."}],
        "stream": true
      }'
Python
stream = client.chat.completions.create(
    model="anthropic/claude-haiku-4.5",
    messages=[{"role": "user", "content": "Write a haiku about the Moon."}],
    stream=True,
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    if chunk.usage:
        print("\n", chunk.usage)
JavaScript
const stream = await client.chat.completions.create({
  model: 'anthropic/claude-haiku-4.5',
  messages: [{ role: 'user', content: 'Write a haiku about the Moon.' }],
  stream: true,
});
for await (const chunk of stream) {
  const text = chunk.choices[0]?.delta?.content;
  if (text) process.stdout.write(text);
  if (chunk.usage) console.log('\n', chunk.usage);
}

Event formatLink to the Event format section

Each event is a data: line followed by a JSON object, then a blank line. The last event is data: [DONE]. Lines that start with : are comments.

Stream
data: {"id":"gen-6f1c2b7e9a4d4c0e8b3a5d2f1e0c9b8a","object":"chat.completion.chunk","created":1793577600,"model":"anthropic/claude-haiku-4.5","choices":[{"index":0,"delta":{"role":"assistant","content":"Pale"},"finish_reason":null}]}

data: {"id":"gen-6f1c2b7e9a4d4c0e8b3a5d2f1e0c9b8a","object":"chat.completion.chunk","created":1793577600,"model":"anthropic/claude-haiku-4.5","choices":[{"index":0,"delta":{"content":" moon above the rooftops"},"finish_reason":null}]}

: keep-alive

data: {"id":"gen-6f1c2b7e9a4d4c0e8b3a5d2f1e0c9b8a","object":"chat.completion.chunk","created":1793577600,"model":"anthropic/claude-haiku-4.5","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: {"id":"gen-6f1c2b7e9a4d4c0e8b3a5d2f1e0c9b8a","object":"chat.completion.chunk","created":1793577600,"model":"anthropic/claude-haiku-4.5","choices":[],"usage":{"prompt_tokens":16,"completion_tokens":21,"total_tokens":37}}

data: [DONE]
Chunk fieldDescription
idThe request ID (gen-…), the same in every chunk.
objectchat.completion.chunk.
created, modelAs in the non-streamed response.
choices[].delta.roleassistant, in the first chunk.
choices[].delta.contentThe next piece of text.
choices[].delta.tool_callsPieces of tool calls: index, then id and function.name at the start, and function.arguments split into pieces to concatenate.
choices[].delta.reasoningA piece of the reasoning, when the model returns it (reasoning_content on some models).
choices[].finish_reasonnull while the response continues, then the finish reason.
usageIn one of the last chunks: the tokens counted, sometimes with an empty choices array.
  • The chunk that carries usage always arrives, even without stream_options: check that choices is not empty before reading it.
  • : keep-alive comments arrive while the model is thinking, to keep the connection open. The SDKs ignore them.
  • When Moon Blur replaced a value, it is put back in the text, the reasoning and the tool arguments, even when a placeholder is split across two chunks.
  • If the stream is cut on our side or at the provider, a final data: {"error": {"message": "upstream stream interrupted", "type": "server_error", "code": "upstream_error"}} event signals that the response is incomplete. The SDKs raise it as an error during iteration.
  • A stream lasts one hour at most, and it is cut if the model sends nothing for 5 minutes.
  • The cost is settled when the stream ends. If your client closes the connection before that, the request is settled at the cost the provider reports for what it produced, or keeps part of its hold (see Settlement).

Tools (function calling)Link to the Tools (function calling) section

Only tools of type function, run by your own program, are accepted. A tool of any other type (run by the provider, such as a web search) is rejected with 400 unsupported_parameter and param: "tools".

cURL
curl https://api.realmoon.ai/v1/chat/completions \
  -H "Authorization: Bearer $MOONAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "anthropic/claude-haiku-4.5",
        "messages": [{"role": "user", "content": "What is the weather like in Lyon?"}],
        "tools": [{
          "type": "function",
          "function": {
            "name": "get_weather",
            "description": "Current weather in a city",
            "parameters": {
              "type": "object",
              "properties": {"city": {"type": "string"}},
              "required": ["city"]
            }
          }
        }],
        "tool_choice": "auto"
      }'
Python
import json

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Current weather in a city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]
messages = [{"role": "user", "content": "What is the weather like in Lyon?"}]

first = client.chat.completions.create(model="anthropic/claude-haiku-4.5", messages=messages, tools=tools)
call = first.choices[0].message.tool_calls[0]
args = json.loads(call.function.arguments)

messages.append(first.choices[0].message)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps({"city": args["city"], "sky": "clear", "temperature": 18})})

final = client.chat.completions.create(model="anthropic/claude-haiku-4.5", messages=messages, tools=tools)
print(final.choices[0].message.content)
JavaScript
const tools = [{
  type: 'function',
  function: {
    name: 'get_weather',
    description: 'Current weather in a city',
    parameters: {
      type: 'object',
      properties: { city: { type: 'string' } },
      required: ['city'],
    },
  },
}];
const messages = [{ role: 'user', content: 'What is the weather like in Lyon?' }];

const first = await client.chat.completions.create({ model: 'anthropic/claude-haiku-4.5', messages, tools });
const call = first.choices[0].message.tool_calls[0];
const args = JSON.parse(call.function.arguments);

messages.push(first.choices[0].message);
messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify({ city: args.city, sky: 'clear', temperature: 18 }) });

const final = await client.chat.completions.create({ model: 'anthropic/claude-haiku-4.5', messages, tools });
console.log(final.choices[0].message.content);
Message returned by the model
{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_01",
      "type": "function",
      "function": { "name": "get_weather", "arguments": "{\"city\":\"Lyon\"}" }
    }
  ]
}
  • Not every model can call tools.
  • Arguments (function.arguments) arrive as a JSON string. When Moon Blur masked a value, it is put back in the arguments, which remain valid JSON.
  • finish_reason is tool_calls when the model is waiting for the results of your functions.

Images and filesLink to the Images and files section

A model that reads images accepts image_url parts in a user message.

cURL
curl https://api.realmoon.ai/v1/chat/completions \
  -H "Authorization: Bearer $MOONAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "anthropic/claude-haiku-4.5",
        "messages": [{
          "role": "user",
          "content": [
            {"type": "text", "text": "Describe this image."},
            {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
          ]
        }]
      }'
Python
import base64

with open("photo.jpg", "rb") as f:
    data_url = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()

response = client.chat.completions.create(
    model="anthropic/claude-haiku-4.5",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "Describe this image."},
            {"type": "image_url", "image_url": {"url": data_url}},
        ],
    }],
)
print(response.choices[0].message.content)
JavaScript
import fs from 'node:fs';

const dataUrl = 'data:image/jpeg;base64,' + fs.readFileSync('photo.jpg').toString('base64');

const response = await client.chat.completions.create({
  model: 'anthropic/claude-haiku-4.5',
  messages: [{
    role: 'user',
    content: [
      { type: 'text', text: 'Describe this image.' },
      { type: 'image_url', image_url: { url: dataUrl } },
    ],
  }],
});
console.log(response.choices[0].message.content);
  • An image at an https:// URL is fetched by the model provider, not by Moon AI.
  • On OpenAI models, an image with no detail (or with auto) is sent with detail: "high".
  • For most models, a PDF sent in a file part is read by the model if it can read files; otherwise, its text is extracted before sending.
  • A request’s hold takes its attachments into account. An image for a model whose image pricing is not known, a PDF file or an audio clip can be held up to the size of the context window: the hold can be high, but it is released after the response (see Costs).

Moon Blur reads only text: images, files and audio are sent as is.

ReasoningLink to the Reasoning section

Reasoning models accept reasoning_effort or the reasoning object, depending on their family. When the model returns its reasoning, it arrives in a field of the message or of the stream chunk (reasoning or reasoning_content, depending on the model). Reasoning tokens are output tokens: they count in usage and toward the output limit.

Response lengthLink to the Response length section

  • With max_tokens (or max_completion_tokens), the model produces no more than this value, reasoning included. A larger reasoning.max_tokens budget is brought down to this value.
  • Without max_tokens, the response gets at least 4,096 output tokens (fewer if the model’s limit is lower). If the balance or a spend cap cannot cover that size, the request is refused (402) rather than served in a few words. Beyond that, the limit rises up to the model’s own limit (16,384 tokens at most), as long as a small share of what is left in the balance and the caps covers it, to leave room for concurrent requests. It also leaves room for the input within the context window.
  • For an OpenAI reasoning model called directly, the limit is sent as max_completion_tokens.

Setting max_tokens makes the hold smaller and more predictable. We recommend it when many requests run at the same time.

Ignored parametersLink to the Ignored parameters section

These parameters are removed before sending, without an error: store, metadata, service_tier, modalities, audio, prediction, web_search_options, plugins, stream_options (the server itself requests usage at the end of the stream) and any parameter that is not in the parameter table. So the store parameter cannot ask the provider to keep your requests.

ErrorsLink to the Errors section

HTTPcodeMeaning
400nullinvalid JSON body or body must be a JSON object.
400missing_required_parametermodel is missing (param: "model").
400unsupported_contentmessages is missing or malformed, or a message part the API does not serve (see Messages).
400n_not_supportedn is not 1.
400unsupported_parameterA tool of a type other than function, an invalid tool_choice, a rejected provider preference, or a parameter the model rejects.
400 or 413context_length_exceededThe request is too long for the model’s context window (param: "messages"), with an explanation.
400 or 413no_available_route, invalid_requestThe model rejected the request, with an explanation.
402insufficient_credits, spend_cap_exceeded, hold_exceeds_capThe balance or a cap does not cover the hold (see Costs).
403org_policyYour organization’s rules refuse the API or this model.
404model_not_foundUnknown model, or one the API does not serve (param: "model").
413moon_blur_payload_too_largeA text exceeds 500,000 characters, too long for Moon Blur: nothing is sent or held.
429model_rate_limitedThe model is getting too many requests, nothing is charged; Retry-After says when to retry.
429credits_held_in_progressYour in-flight requests are holding the balance that is missing; retry after Retry-After (2 seconds).
500 to 504upstream_errorThe provider failed, or cannot serve the request right now.
503nullupstream unavailable: the provider could not be reached.
503pricing_unavailableThe model’s price is temporarily unknown: the request is refused rather than billed incorrectly.
503moon_blur_unavailableMoon Blur could not scan the request: it is not sent, and nothing is held. Retry-After: 5.
503billing_unavailable, org_policy_unavailableThe balance or the organization’s rules are temporarily unreadable.
504upstream_timeoutThe provider did not respond in time. The hold is kept (see Settlement).

Key and rate errors, common to the whole API, are in Errors. Model refusals are passed on without the provider’s name, links or internal IDs.

See alsoLink to the See also section

Moon AI

Moon AI in action

0:00

Explore

Screenshots of chat.realmoon.ai taken on October 29, 2026, with fictional data.

Moon AI

Your preferences

Visits counted without cookies.

Our server counts visits without cookies or trackers. Your theme and your answer about ad measurement are remembered on this device.

Ad measurement

With your consent, our ad networks’ trackers (Google) measure our ads, from the visit to the sign-up and then to the first subscription and its amount, and may show you our ads. Your answer is valid for 13 months; you can change it here at any time.

No answer saved on this device.

After you decline, no tag is loaded anymore and the site deletes the cookies they had set on realmoon.ai. Those on the ad networks’ own domains are managed with them or in your browser.

The Moon AI app has its own privacy settings.