POST / 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.
POST https://api.realmoon.ai/v1/chat/completions- Authentication: an
sk-moon-…key with theinferencescope (see Authentication). - Body: JSON (
Content-Type:).application/ json - Response: a
chat.completionobject, or an SSE stream ofchat.completion.chunkchunks with"stream":.true - Billing: pay as you go, from the wallet of the key’s workspace (see Costs).
ExampleLink to the Example section
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
}'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)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 ).
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | yes | The ID of a chat model from GET /. Missing: 400 . Unknown: 404 . |
messages | array | yes | The conversation, see Messages. Missing or malformed: 400 . |
stream | boolean | no | true to receive the response as an SSE stream, see Streaming. false by default. |
max_tokens | integer | no | Maximum number of output tokens, reasoning included. See Response length. |
max_completion_tokens | integer | no | Same as max_tokens, under the name used by OpenAI reasoning models. |
temperature | number | no | Sampling randomness. |
top_p | number | no | Nucleus sampling. |
top_k, min_p, top_a, repetition_penalty | number | no | Sampling settings, for models that accept them. |
frequency_penalty, presence_penalty | number | no | Repetition penalties. |
stop | string or array | no | Sequences that stop generation. |
seed | integer | no | Seed, for models that honor it. |
logit_bias | object | no | Per-token bias, for models that accept it. |
logprobs | boolean | no | Requests token probabilities, for models that return them. |
top_logprobs | integer | no | Number of most likely tokens returned at each position, with logprobs. |
response_format | object | no | JSON output ({"type": ) or output that follows a schema ({"type": ), depending on the model. |
tools | array | no | Functions the model can call, see Tools. |
tool_choice | string or object | no | none, auto, required, a specific function ({"type": ) or {"type": . |
parallel_tool_calls | boolean | no | Allows several tool calls in a single response. |
functions, function_call | array, string or object | no | The legacy form of tools, still accepted. |
reasoning_effort | string | no | Reasoning effort (for example low, medium, high), for models that accept it. |
reasoning | object | no | Reasoning settings. Only the effort, max_tokens, exclude and enabled keys are passed on. |
include_reasoning | boolean | no | Asks the model to return its reasoning, when it allows it. |
verbosity | string | no | Level of detail, for models that accept it. |
user, safety_identifier, prompt_cache_key | string | no | Your end user’s identifier or a cache key. Replaced with a pseudonym if it contains personal data that Moon Blur detects (see Privacy). |
n | integer | no | Only 1 is accepted (otherwise 400 ). |
provider | object | no | Provider preferences, see below. |
transforms | array | no | ["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.
| Key | Value | Effect |
|---|---|---|
only | array of names | Serves the request only through these providers. |
ignore | array of names | Excludes these providers. |
order | array of names | Tries the providers in this order. |
quantizations | array of names | Keeps only these quantization levels. |
sort | price, throughput or latency | Sorts providers by price, throughput or latency. |
allow_fallbacks | boolean | false: no fallback to another provider. |
require_parameters | boolean | true: only providers that support every parameter in the request. |
max_price | object | Maximum 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. |
zdr | boolean | true: only providers that state they retain nothing. |
- Lists hold 64 names at most.
- A default value (
allow_fallbacks:,true zdr: false,data_collection:, empty list) is ignored."allow" - Any other key, or an invalid value, is rejected with
400and aunsupported_parameter paramthat names it (for exampleprovider.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_collectionandzdrcommitments are the ones providers declare; Moon AI does not verify them.
transforms: shortens the middle of a conversation that is too long for the model’s context window. Any other value is rejected (400 , param: ), 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.
| Field | Type | Required | Description |
|---|---|---|---|
role | string | yes | system, user, assistant or tool. |
content | string, array of parts, or null | yes | The content. null is allowed for an assistant message that carries only tool_calls. |
name | string | no | The author’s name. Replaced with a pseudonym if it contains personal data that Moon Blur detects. |
tool_calls | array | no | On an assistant message: the tool calls the model made, sent back as is. |
tool_call_id | string | in a tool message | The ID of the call this message answers. |
A content array holds parts:
| Part type | Shape | Notes |
|---|---|---|
text | {"type": | Text. |
refusal | {"type": | A refusal returned by a model, in an assistant message you send back. |
image_url | {"type": | An https:// or http:// URL, or data:image/. See Images and files. |
input_audio | {"type": | Base64 audio only, for models that read audio. |
file | {"type": | A file in base64, as a data: URL, or at an http(s):// URL. |
Any other part is rejected with 400 , in particular:
- a file referenced by its ID (
file_id): send its content infile_datainstead; - a video;
- audio sent as a
file: use aninput_audiopart; - audio on a model whose audio price is not known.
ResponseLink to the Response section
Without streaming, the response is a chat.completion object.
{
"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 }
}| Field | Type | Description |
|---|---|---|
id | string | The request ID at Moon AI (gen-…), the same as the X-Moon-Request-Id header. Use it to read the cost with GET /. |
object | string | chat.completion. |
created | integer | Timestamp in Unix seconds. |
model | string | The model you called, or the exact name of the version served. For a Moon AI in-house model, always the Moon AI ID. |
choices | array | A single response (n is always 1). |
choices[].message.role | string | assistant. |
choices[].message.content | string or null | The response text. null when the model only calls tools. |
choices[].message.tool_calls | array | The tool calls the model requests, see Tools. |
choices[].message.refusal | string | The model’s refusal, when it declines to answer. |
choices[].message.reasoning | string | The reasoning, when the model returns it (reasoning_content on some models). |
choices[].finish_reason | string | stop, length (output limit reached), tool_calls, content_filter… |
choices[].logprobs | object | Token probabilities, with logprobs: . |
usage.prompt_tokens, usage.completion_tokens, usage.total_tokens | integer | Tokens counted. |
usage.prompt_tokens_details, usage.completion_tokens_details | object | Details 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
| Header | Value |
|---|---|
X-Moon-Request-Id | gen-…, the request ID. |
X-Moon-Prompt-Storage | none: the content is not stored. |
X-Moon-Blur | on if Moon Blur scanned the request, off if it is turned off for the key. |
X-Moon-Blur-Redactions | Number of values Moon Blur replaced, when there are any. |
StreamingLink to the Streaming section
With "stream": , the response arrives as Server-Sent Events (Content-Type: ) in the OpenAI format. The HTTP status (200) and the headers go out with the first byte.
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
}'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)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.
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 field | Description |
|---|---|
id | The request ID (gen-…), the same in every chunk. |
object | chat.completion.chunk. |
created, model | As in the non-streamed response. |
choices[].delta.role | assistant, in the first chunk. |
choices[].delta.content | The next piece of text. |
choices[].delta.tool_calls | Pieces of tool calls: index, then id and function.name at the start, and function.arguments split into pieces to concatenate. |
choices[].delta.reasoning | A piece of the reasoning, when the model returns it (reasoning_content on some models). |
choices[].finish_reason | null while the response continues, then the finish reason. |
usage | In one of the last chunks: the tokens counted, sometimes with an empty choices array. |
- The chunk that carries
usagealways arrives, even withoutstream_options: check thatchoicesis not empty before reading it. : keep-alivecomments 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:event signals that the response is incomplete. The SDKs raise it as an error during iteration.{"error": {"message": "upstream stream interrupted", "type": "server_error", "code": "upstream_error"}} - 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 and param: .
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"
}'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)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);{
"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_reasonistool_callswhen 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 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"}}
]
}]
}'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)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 withauto) is sent withdetail:."high" - For most models, a PDF sent in a
filepart 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(ormax_completion_tokens), the model produces no more than this value, reasoning included. A largerreasoning.max_tokensbudget 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
| HTTP | code | Meaning |
|---|---|---|
| 400 | null | invalid or body . |
| 400 | missing_required_parameter | model is missing (param: ). |
| 400 | unsupported_content | messages is missing or malformed, or a message part the API does not serve (see Messages). |
| 400 | n_not_supported | n is not 1. |
| 400 | unsupported_parameter | A tool of a type other than function, an invalid tool_choice, a rejected provider preference, or a parameter the model rejects. |
| 400 or 413 | context_length_exceeded | The request is too long for the model’s context window (param: ), with an explanation. |
| 400 or 413 | no_available_route, invalid_request | The model rejected the request, with an explanation. |
| 402 | insufficient_credits, spend_cap_exceeded, hold_exceeds_cap | The balance or a cap does not cover the hold (see Costs). |
| 403 | org_policy | Your organization’s rules refuse the API or this model. |
| 404 | model_not_found | Unknown model, or one the API does not serve (param: ). |
| 413 | moon_blur_payload_too_large | A text exceeds 500,000 characters, too long for Moon Blur: nothing is sent or held. |
| 429 | model_rate_limited | The model is getting too many requests, nothing is charged; Retry-After says when to retry. |
| 429 | credits_held_in_progress | Your in-flight requests are holding the balance that is missing; retry after Retry-After (2 seconds). |
| 500 to 504 | upstream_error | The provider failed, or cannot serve the request right now. |
| 503 | null | upstream : the provider could not be reached. |
| 503 | pricing_unavailable | The model’s price is temporarily unknown: the request is refused rather than billed incorrectly. |
| 503 | moon_blur_unavailable | Moon Blur could not scan the request: it is not sent, and nothing is held. Retry-After: . |
| 503 | billing_unavailable, org_policy_unavailable | The balance or the organization’s rules are temporarily unreadable. |
| 504 | upstream_timeout | The 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
- Models: IDs and prices.
- Balance and request cost: read what a chat request cost.
- Limits: rate limits, timeouts and retries.