Chat completions
Create a completion, stream it, and use tool calling.
EndpointPermalink to Endpoint
/v1/chat/completionsCreate a chat completion. Request and response follow the OpenAI chat-completions shape.
Authentication: Required
Request bodies are limited to 10 MB. A larger body is rejected before it is parsed.
Request fieldsPermalink to Request fields
| Field | Type | Notes |
|---|---|---|
| model | string | Required. An id from GET /v1/models. |
| messages | array | Required. Ordered conversation messages. |
| stream | boolean | Stream the response as server-sent events. |
| temperature | number | Sampling temperature. |
| top_p | number | Nucleus sampling parameter. |
| max_tokens | integer | Cap on generated tokens. The legacy name; used only when max_completion_tokens is absent. |
| max_completion_tokens | integer | Cap on generated tokens. Wins over max_tokens when both are sent. |
| tools | array | Tool definitions the model may call. |
| tool_choice | string or object | Accepts "auto", "none", "required", or an object naming one function. |
| web_search_options | object | Requests hosted web search. Admission is capability-gated; an entry that does not support it never receives the signal. |
| reasoning | object | Reasoning configuration for entries that expose it. |
| include_reasoning | boolean | Return reasoning content alongside the answer. |
| provider | object | Routing preferences. |
| stream_options | object | Options that apply when stream is true. |
| Field | Type | Notes |
|---|---|---|
| role | string | Required. The speaker for this message. |
| content | string or array | Required. Plain text, or an array of content parts. |
| name | string | Optional name for the speaker. |
| tool_calls | array | Tool calls this message is making. |
| tool_call_id | string | The call this message is a result for. |
| reasoning_content | string | Reasoning text carried alongside the message. |
Two request fields are server-controlled and cannot be set by a caller
tool_choice accepts both shapes
A basic completionPermalink to A basic completion
"tk-cmd">curl https://api.relane.ai/v1/chat/completions \
"tk-flag">-H "Authorization: Bearer rl_sk_live_YOUR_KEY" \
"tk-flag">-H "Content-Type: application/json" \
"tk-flag">-d '{
"model": "MODEL_ID",
"messages": [
{ "role": "user", "content": "Summarise this changelog entry in one sentence." }
]
}'const res = await fetch(class="tk-str">"https:class="tk-commentclass="tk-str">">//api.relane.ai/v1/chat/completions", {
method: class="tk-str">"POST",
headers: {
Authorization: class="tk-str">`Bearer ${process.env.RELANE_API_KEY}`,
class="tk-str">"Content-Type": class="tk-str">"application/json",
},
body: JSON.stringify({
model: modelId, class=class="tk-str">"tk-comment">// from GET /v1/models
messages: [{ role: class="tk-str">"user", content: class="tk-str">"Summarise this changelog entry in one sentence." }],
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(class="tk-str">`${error.type} (${error.code}): ${error.message}`);
}
const completion = await res.json();
console.log(completion.choices[0].message.content);StreamingPermalink to Streaming
Set stream to true and the response arrives as server-sent events: each event is a data: line, events are separated by a blank line, and the stream ends with a [DONE] sentinel.
Do not parse the stream chunk by chunk
function createSseParser() {
const decoder = new TextDecoder();
let buffer = class="tk-str">"";
function parseEvent(raw) {
class=class="tk-str">"tk-comment">// An SSE event may carry several data: lines; the spec joins them with a newline.
const data = raw
.split(class="tk-str">"\n")
.filter((line) => line.startsWith(class="tk-str">"data:"))
.map((line) => line.slice(5).trimStart())
.join(class="tk-str">"\n");
if (!data || data === class="tk-str">"[DONE]") return null;
return JSON.parse(data);
}
function drain(flush) {
class=class="tk-str">"tk-comment">// Normalise framing so a CRLF stream parses identically to an LF one. Safe on the payload:
class=class="tk-str">"tk-comment">// JSON escapes newlines inside strings, so a raw CR LF here is always framing.
buffer = buffer.replace(/\r\n/g, class="tk-str">"\n");
const events = [];
let sep;
while ((sep = buffer.indexOf(class="tk-str">"\n\n")) !== -1) {
const raw = buffer.slice(0, sep);
buffer = buffer.slice(sep + 2);
const event = parseEvent(raw);
if (event) events.push(event);
}
class=class="tk-str">"tk-comment">// At end of stream, accept a final event that never got its blank line.
if (flush && buffer.trim()) {
const raw = buffer;
buffer = class="tk-str">"";
const event = parseEvent(raw);
if (event) events.push(event);
}
return events;
}
return {
class=class="tk-str">"tk-comment">// Feed one network chunk (Uint8Array); returns the events it completed.
push(chunk) {
class=class="tk-str">"tk-comment">// stream: true holds a split multi-byte character in the decoder until its remaining
class=class="tk-str">"tk-comment">// bytes arrive, instead of emitting a replacement character.
buffer += decoder.decode(chunk, { stream: true });
return drain(false);
},
class=class="tk-str">"tk-comment">// Call once the body ends, to flush the decoder and any trailing event.
end() {
buffer += decoder.decode();
return drain(true);
},
};
}Feed every chunk to push, and call end once the body is finished so the decoder is flushed and a trailing event without its blank line is not lost.
const res = await fetch(class="tk-str">"https:class="tk-commentclass="tk-str">">//api.relane.ai/v1/chat/completions", {
method: class="tk-str">"POST",
headers: {
Authorization: class="tk-str">`Bearer ${process.env.RELANE_API_KEY}`,
class="tk-str">"Content-Type": class="tk-str">"application/json",
},
body: JSON.stringify({ model: modelId, messages, stream: true }),
});
class=class="tk-str">"tk-comment">// A non-200 fails before the stream opens.
if (!res.ok) {
const { error } = await res.json();
throw new Error(class="tk-str">`${error.type} (${error.code}): ${error.message}`);
}
const parser = createSseParser();
const reader = res.body.getReader();
let text = class="tk-str">"";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
for (const event of parser.push(value)) {
class=class="tk-str">"tk-comment">// A 200 that later fails is still a failure. Raise rather than returning a truncated answer.
assertNotStreamError(event);
text += event.choices[0]?.delta?.content ?? class="tk-str">"";
}
}
class=class="tk-str">"tk-comment">// Flush anything the stream ended on — including a final error event.
for (const event of parser.end()) {
assertNotStreamError(event);
text += event.choices[0]?.delta?.content ?? class="tk-str">"";
}
Errors can arrive after the stream opens
Search provenance arrives in a delta, usually the last one
Tool callingPermalink to Tool calling
Supply tools to let the model request a function call. If you send a tool call without declaring any tools, the request is rejected as a client error rather than reported as an upstream failure.
"tk-cmd">curl https://api.relane.ai/v1/chat/completions \
"tk-flag">-H "Authorization: Bearer rl_sk_live_YOUR_KEY" \
"tk-flag">-H "Content-Type: application/json" \
"tk-flag">-d '{
"model": "MODEL_ID",
"messages": [{ "role": "user", "content": "What is the build status?" }],
"tools": [
{
"type": "function",
"function": {
"name": "get_build_status",
"description": "Return the status of the most recent build.",
"parameters": {
"type": "object",
"properties": { "branch": { "type": "string" } },
"required": ["branch"]
}
}
}
],
"tool_choice": "auto"
}'Nested request objectsPermalink to Nested request objects
Complete field lists for every object a request can nest. These are the shapes the server decodes; a field not listed here is not read.
| Field | Type | Notes |
|---|---|---|
| type | string | Required. "function" for a callable tool; "web_search" requests hosted search. |
| function | object | Required for a function tool. See below. |
| Field | Type | Notes |
|---|---|---|
| name | string | Required. The name the model calls. |
| description | string | What the tool does. Omitted when empty. |
| parameters | object | JSON Schema for the arguments. Omitted when the tool takes none. |
| Field | Type | Notes |
|---|---|---|
| id | string | Identifier this call is answered by, via tool_call_id. |
| type | string | Always "function". |
| function | object | name (string) and arguments (a JSON-encoded STRING, not an object). |
tool_calls[].function.arguments is a string
| Field | Type | Notes |
|---|---|---|
| type | string | Required. "text" or "image_url". |
| text | string | The text of a text part. |
| image_url | object | url (string) and optional detail (string). |
| cache_control | object | type (string). Marks the part for prompt caching. |
| Field | Type | Notes |
|---|---|---|
| max_tokens | integer | Bound on reasoning tokens. |
| effort | string | Reasoning-effort hint. Commonly low, medium or high; some entries accept a wider ladder and reject values outside it with a 400. |
| Field | Type | Notes |
|---|---|---|
| sort | string | Routing sort preference. |
| order | array | Ordered routing preference. |
| allow_fallbacks | boolean | Whether a fallback route may be used. |
| Field | Type | Notes |
|---|---|---|
| include_usage | boolean | Include a usage block in the stream. |
Routing and fallbackPermalink to Routing and fallback
The provider object expresses how you would like the request routed when more than one route could serve it. Every field in it is a preference, forwarded to the route that ends up serving the request and honoured only where that route implements it. None of them is a guarantee, and none of them changes which model you asked for.
| Field | Asks for |
|---|---|
| sort | A ranking preference among the routes that could serve the request. |
| order | An explicit order to try routes in, most preferred first. |
| allow_fallbacks | Set it to false to be refused rather than served by a route you did not rank. |
The response tells you what actually served it
Three outcomes are refusals rather than fallbacks, and the endpoint returns them instead of quietly answering from somewhere else:
| code | Status | What happened |
|---|---|---|
| model_not_allowed | 403 | The key is restricted to an explicit list of models and this one is not on it. It is never rerouted to a permitted model. |
| model_not_found | 403 | The model is in the catalog but has been deactivated. |
| model_unavailable | 503 | The model is known but cannot currently be served. Retry; do not substitute a different model automatically without telling your own users. |
No silent substitution across model families
Hosted web searchPermalink to Hosted web search
A request asks for hosted search in either of two ways: by sending web_search_options, or by including a bare {"type": "web_search"} entry in tools. Both are treated as a request for search.
| Field | Type | Accepted values |
|---|---|---|
| search_context_size | string | low, medium, or high. Omit it to let the endpoint apply its default; any other value is a 400. |
| user_location | object | type must be "approximate". Then EITHER a nested approximate object OR the flat fields — never both. |
| Field | Type | Notes |
|---|---|---|
| country | string | ISO-3166-1 alpha-2, exactly two letters. |
| city | string | Length-bounded. |
| region | string | Length-bounded. |
| timezone | string | A valid IANA zone, e.g. Europe/Istanbul. |
user_location is strict-decoded
"tk-cmd">curl https://api.relane.ai/v1/chat/completions \
"tk-flag">-H "Authorization: Bearer rl_sk_live_YOUR_KEY" \
"tk-flag">-H "Content-Type: application/json" \
"tk-flag">-d '{
"model": "MODEL_ID",
"messages": [
{ "role": "user", "content": "What changed in the TLS 1.3 spec most recently?" }
],
"web_search_options": {
"search_context_size": "medium",
"user_location": {
"type": "approximate",
"country": "TR",
"timezone": "Europe/Istanbul"
}
}
}'class=class="tk-str">"tk-comment">// content becomes an ARRAY of parts when a message carries more than text.
const res = await fetch(class="tk-str">"https:class="tk-commentclass="tk-str">">//api.relane.ai/v1/chat/completions", {
method: class="tk-str">"POST",
headers: {
Authorization: class="tk-str">`Bearer ${process.env.RELANE_API_KEY}`,
class="tk-str">"Content-Type": class="tk-str">"application/json",
},
body: JSON.stringify({
model: modelId, class=class="tk-str">"tk-comment">// must be an entry whose supports_images is true
messages: [
{
role: class="tk-str">"user",
content: [
{ type: class="tk-str">"text", text: class="tk-str">"Which library produced this error, and is it patched?" },
{ type: class="tk-str">"image_url", image_url: { url: class="tk-str">"https:class="tk-commentclass="tk-str">">//example.com/screenshot.png" } },
],
},
],
web_search_options: { search_context_size: class="tk-str">"high" },
}),
});Check supports_images before sending an image part
Some entries need room to search AND answer
| code | When | message |
|---|---|---|
| web_search_not_supported | The route cannot serve hosted search for this request. | hosted web search is unavailable for this request |
| invalid_web_search_options | search_context_size is not low, medium or high. | search_context_size must be one of: low, medium, high |
| invalid_web_search_options | user_location.type is not "approximate". | user_location.type must be "approximate" |
| invalid_web_search_options | Nested and flat location fields are mixed. | user_location must not mix nested approximate and flat fields |
| invalid_web_search_options | user_location carries an unknown or malformed field. | user_location has unknown or malformed fields |
| web_search_output_budget_too_small | The entry needs a larger output budget to search and still answer. | hosted web search on this model requires an output budget of at least N tokens; M was requested |
The budget refusal costs you nothing
Refusal is upfront, never partial
Reading sources and provenancePermalink to Reading sources and provenance
When a completion used hosted search, the assistant message carries provenance in two separate places. They are not interchangeable.
| Field | Contains |
|---|---|
| annotations | Standard url_citation entries: which span of the answer came from which page. |
| relane_web_search | The search actions performed, as a namespaced extension. Never mixed into annotations. |
| Field | Type | Notes |
|---|---|---|
| type | string | Always "url_citation". |
| url_citation | object | url, optional title, start_index and end_index. |
| Field | Type | Notes |
|---|---|---|
| url | string | HTTPS link to the cited page. |
| title | string | Page title. Omitted when unavailable. |
| start_index | integer | Start offset of the cited span in the final message text. |
| end_index | integer | End offset of that span. |
| Field | Type | Notes |
|---|---|---|
| type | string | search, open_page, or find_in_page. |
| query | string | The single query searched for. |
| queries | array | Several queries, when the action carried more than one. |
| url | string | The page opened. Absent on a search action, and absent on any entry whose source page cannot be attested. |
| pattern | string | The text pattern searched for, on find_in_page. |
A search action without a url is normal, not truncated data
const completion = await res.json();
const message = completion.choices[0].message;
class=class="tk-str">"tk-comment">// Citations index into the final message text, so slice with them rather than
class=class="tk-str">"tk-comment">// re-searching the string.
const body = typeof message.content === class="tk-str">"string" ? message.content : class="tk-str">"";
for (const a of message.annotations ?? []) {
if (a.type !== class="tk-str">"url_citation" || !a.url_citation) continue;
const { url, title, start_index, end_index } = a.url_citation;
const quoted = body.slice(start_index, end_index);
console.log(class="tk-str">`class="tk-str">"${quoted}" — ${title ?? url} (${url})`);
}
class=class="tk-str">"tk-comment">// What the model actually did, kept separate from the citations above.
for (const call of message.relane_web_search ?? []) {
if (call.type === class="tk-str">"search") console.log(class="tk-str">"searched:", call.query ?? call.queries?.join(class="tk-str">", "));
if (call.type === class="tk-str">"open_page") console.log(class="tk-str">"opened:", call.url);
if (call.type === class="tk-str">"find_in_page") console.log(class="tk-str">"looked for:", call.pattern, class="tk-str">"in", call.url);
}Indices are bounded to the final text
The responsePermalink to The response
| Field | Type | Notes |
|---|---|---|
| id | string | Identifier for this completion. |
| object | string | Object type. |
| created | integer | Unix timestamp. |
| model | string | The entry that produced the completion. |
| choices | array | One entry per returned completion. |
| usage | object | Token and cost accounting. Omitted when unavailable. |
| Field | Type | Notes |
|---|---|---|
| index | integer | Position of this choice. |
| message | object | The assistant message. |
| finish_reason | string | Why generation stopped. Omitted when not reported. |
| Field | Type | Notes |
|---|---|---|
| role | string | Always the assistant role. |
| content | string or array | The answer. |
| tool_calls | array | Tool calls the model wants performed. |
| reasoning_content | string | Reasoning text, when the entry exposes it. |
| annotations | array | url_citation entries. Response-only. |
| relane_web_search | array | Search actions performed. Response-only. |
annotations and relane_web_search are response-only
| Field | Type | Notes |
|---|---|---|
| prompt_tokens | integer | Input tokens. |
| completion_tokens | integer | Generated tokens. |
| total_tokens | integer | Sum of the two. |
| prompt_tokens_details | object | cached_tokens and cache_write_tokens. |
| cost | number | Cost for this call. Omitted when unavailable. |
| cost_details | object | Cost breakdown. Omitted when unavailable. |
| relane_web_search_calls | integer | Hosted-search actions that completed. Omitted when none. |
| Field | Type | Notes |
|---|---|---|
| cached_tokens | integer | Input tokens served from cache. Omitted when zero. |
| cache_write_tokens | integer | Cache-write tokens, additional to prompt_tokens. Omitted when zero. |
| Field | Type | Notes |
|---|---|---|
| upstream_inference_cost | number | Inference cost component. Omitted when unavailable. |
Count searches from usage, not from the annotations