Chat completions
POST /v1/chat/completions: the OpenAI chat shape, served by
Qwen3.8 27B as
qwen3.8-27b. 262,144-token context. €0.12 / 1M input tokens, €0.62 / 1M output tokens (launch pricing). Streaming, tool calls,
reasoning output and image input all work on this one endpoint.
Two more chat models on the same endpoint and key, both text only with a 262,144-token context:
GLM-5.3-Flash as glm-5.3-flash
(€0.12 / 1M input, €0.62 / 1M output), which always thinks before it answers, and
MiMo-V2.6-Flash as mimo-v2.6-flash
(€0.12 / 1M input, €0.62 / 1M output), which thinks when you ask it to.
Both take tool calls and streaming; a request with an image is refused.
For GLM's effort settings and output budget, see GLM long answers.
And Qwen3.8 Flash Next as qwen3.8-flash-next
(€0.12 / 1M input, €0.62 / 1M output): a 500,000-token
context, tool calls, streaming and image input, and it thinks when you ask it to.
Request & response
curl https://api.axforge.ai/v1/chat/completions \
-H "Authorization: Bearer $AXFORGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3.8-27b",
"messages": [
{"role": "system", "content": "You answer in one sentence."},
{"role": "user", "content": "What is an embedding?"}
],
"max_tokens": 2048
}'
$body = @{
model = 'qwen3.8-27b'
messages = @(
@{ role = 'system'; content = 'You answer in one sentence.' },
@{ role = 'user'; content = 'What is an embedding?' }
)
max_tokens = 2048
} | ConvertTo-Json -Depth 5
$r = Invoke-RestMethod https://api.axforge.ai/v1/chat/completions -Method Post `
-Headers @{ Authorization = "Bearer $env:AXFORGE_API_KEY" } `
-ContentType 'application/json' -Body $body
$r.choices[0].message.content
REM cmd keeps single quotes, so the inner quotes are escaped instead.
curl -s https://api.axforge.ai/v1/chat/completions -H "Authorization: Bearer %AXFORGE_API_KEY%" -H "Content-Type: application/json" -d "{\"model\":\"qwen3.8-27b\",\"messages\":[{\"role\":\"system\",\"content\":\"You answer in one sentence.\"},{\"role\":\"user\",\"content\":\"What is an embedding?\"}],\"max_tokens\":2048}"
# pip install openai · the key is read from AXFORGE_API_KEY
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.axforge.ai/v1", api_key=os.environ["AXFORGE_API_KEY"])
r = client.chat.completions.create(
model="qwen3.8-27b",
messages=[{"role": "user", "content": "What is an embedding?"}],
)
print(r.choices[0].message.content)
// npm i openai · save as chat.mjs, run: node chat.mjs
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.axforge.ai/v1", apiKey: process.env.AXFORGE_API_KEY });
const r = await client.chat.completions.create({
model: "qwen3.8-27b",
messages: [{ role: "user", content: "What is an embedding?" }],
});
console.log(r.choices[0].message.content);
The response is the standard OpenAI shape. Note
reasoning_content, the model's thinking, separate from the
answer (details below).
{
"id": "chatcmpl-...",
"object": "chat.completion",
"model": "qwen3.8-27b",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "An embedding is a vector that encodes meaning...",
"reasoning_content": "The user wants a one-line definition..."
},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 21, "completion_tokens": 96, "total_tokens": 117}
}Streaming
Set "stream": true and the response arrives as server-sent
events, one JSON chunk per data: line. Usage is included in
streaming mode too. The final chunk before [DONE] carries it.
curl -N https://api.axforge.ai/v1/chat/completions \
-H "Authorization: Bearer $AXFORGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "qwen3.8-27b", "stream": true,
"messages": [{"role": "user", "content": "Count to three."}]}'
# Streaming is the one place Windows PowerShell needs more than a different quote.
# Invoke-RestMethod waits for the whole body, and plain Invoke-WebRequest throws on an
# SSE response in 5.1 ("Object reference not set to an instance of an object").
# -UseBasicParsing gets you the full text once it is finished:
$body = '{"model":"qwen3.8-27b","stream":true,"messages":[{"role":"user","content":"Count to three."}]}'
(Invoke-WebRequest https://api.axforge.ai/v1/chat/completions -Method Post `
-Headers @{ Authorization = "Bearer $env:AXFORGE_API_KEY" } `
-ContentType 'application/json' -Body $body -UseBasicParsing).Content
# For tokens as they arrive, read the stream yourself:
$req = [System.Net.HttpWebRequest]::Create("https://api.axforge.ai/v1/chat/completions")
$req.Method = "POST"; $req.ContentType = "application/json"
$req.Headers.Add("Authorization", "Bearer $env:AXFORGE_API_KEY")
$bytes = [Text.Encoding]::UTF8.GetBytes($body); $req.ContentLength = $bytes.Length
$s = $req.GetRequestStream(); $s.Write($bytes,0,$bytes.Length); $s.Close()
$rd = New-Object IO.StreamReader($req.GetResponse().GetResponseStream())
while (-not $rd.EndOfStream) { $l = $rd.ReadLine(); if ($l -like "data: *") { $l } }
curl -N -s https://api.axforge.ai/v1/chat/completions -H "Authorization: Bearer %AXFORGE_API_KEY%" -H "Content-Type: application/json" -d "{\"model\":\"qwen3.8-27b\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"Count to three.\"}]}"
The wire format, whichever shell you used:
# the wire format:
data: {"choices":[{"delta":{"content":"One"}}]}
data: {"choices":[{"delta":{"content":", two"}}]}
data: {"choices":[{"delta":{},"finish_reason":"stop"}]}
data: {"choices":[],"usage":{"prompt_tokens":13,"completion_tokens":9,"total_tokens":22}}
data: [DONE]import os
from openai import OpenAI
client = OpenAI(base_url="https://api.axforge.ai/v1", api_key=os.environ["AXFORGE_API_KEY"])
stream = client.chat.completions.create(
model="qwen3.8-27b",
stream=True,
messages=[{"role": "user", "content": "Count to three."}],
)
for chunk in stream:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="", flush=True)
if chunk.usage:
print("\ntokens:", chunk.usage.total_tokens)
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.axforge.ai/v1", apiKey: process.env.AXFORGE_API_KEY });
const stream = await client.chat.completions.create({
model: "qwen3.8-27b",
stream: true,
messages: [{ role: "user", content: "Count to three." }],
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
if (chunk.usage) console.log("\ntokens:", chunk.usage.total_tokens);
}
Tool calls
The endpoint accepts the OpenAI tools schema. Declare
functions, let the model decide when to call one, run it yourself, and send
the result back as a tool message. One complete round trip:
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.axforge.ai/v1", api_key=os.environ["AXFORGE_API_KEY"])
tools = [{
"type": "function",
"function": {
"name": "get_invoice",
"description": "Look up an invoice by its number",
"parameters": {
"type": "object",
"properties": {"number": {"type": "string"}},
"required": ["number"],
},
},
}]
messages = [{"role": "user", "content": "What is the total on invoice 2041?"}]
r = client.chat.completions.create(model="qwen3.8-27b", messages=messages, tools=tools)
# the model asked for the tool instead of answering
call = r.choices[0].message.tool_calls[0]
args = json.loads(call.function.arguments) # {"number": "2041"}
# run it yourself, then send the result back
messages.append(r.choices[0].message)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps({"number": args["number"], "total_eur": 1240.0}),
})
r = client.chat.completions.create(model="qwen3.8-27b", messages=messages, tools=tools)
print(r.choices[0].message.content) # "Invoice 2041 totals EUR 1,240.00."
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.axforge.ai/v1", apiKey: process.env.AXFORGE_API_KEY });
const tools = [{
type: "function",
function: {
name: "get_invoice",
description: "Look up an invoice by its number",
parameters: {
type: "object",
properties: { number: { type: "string" } },
required: ["number"],
},
},
}];
const messages = [{ role: "user", content: "What is the total on invoice 2041?" }];
let r = await client.chat.completions.create({ model: "qwen3.8-27b", messages, tools });
// the model asked for the tool instead of answering
const call = r.choices[0].message.tool_calls[0];
const args = JSON.parse(call.function.arguments); // { number: "2041" }
// run it yourself, then send the result back
messages.push(r.choices[0].message);
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify({ number: args.number, total_eur: 1240.0 }),
});
r = await client.chat.completions.create({ model: "qwen3.8-27b", messages, tools });
console.log(r.choices[0].message.content); // "Invoice 2041 totals EUR 1,240.00."
On the wire, the model's tool request looks like this. Pass the same
tools array in a curl body to get it:
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_...",
"type": "function",
"function": {"name": "get_invoice", "arguments": "{\"number\": \"2041\"}"}
}]
},
"finish_reason": "tool_calls"Reasoning output
The model thinks before it answers. The thinking arrives in
reasoning_content on the message (and in the delta when
streaming); content holds only the answer. Reasoning tokens are
output tokens and are billed as such.
GLM-5.3-Flash always thinks: the switch below does not apply to it. MiMo-V2.6-Flash thinks only when you pass
"enable_thinking": true the same way. Qwen3.8 Flash Next thinks when you pass that, or when you send an
effort level (reasoning_effort); its levels are low, medium and
xhigh, and high is taken as xhigh.
To disable thinking, pass chat_template_kwargs:
curl https://api.axforge.ai/v1/chat/completions \
-H "Authorization: Bearer $AXFORGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3.8-27b","messages":[{"role":"user","content":"Classify as refund, question or other: refund please"}],"chat_template_kwargs":{"enable_thinking":false}}'
$call = @{
Method = "Post"
Uri = "https://api.axforge.ai/v1/chat/completions"
Headers = @{ Authorization = "Bearer $env:AXFORGE_API_KEY" }
ContentType = "application/json"
Body = '{"model":"qwen3.8-27b","messages":[{"role":"user","content":"Classify as refund, question or other: refund please"}],"chat_template_kwargs":{"enable_thinking":false}}'
}
(Invoke-RestMethod @call).choices[0].message.content
curl.exe https://api.axforge.ai/v1/chat/completions ^
-H "Authorization: Bearer %AXFORGE_API_KEY%" ^
-H "Content-Type: application/json" ^
-d "{\"model\":\"qwen3.8-27b\",\"messages\":[{\"role\":\"user\",\"content\":\"Classify as refund, question or other: refund please\"}],\"chat_template_kwargs\":{\"enable_thinking\":false}}"
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.axforge.ai/v1", api_key=os.environ["AXFORGE_API_KEY"])
r = client.chat.completions.create(
model="qwen3.8-27b",
messages=[{"role": "user", "content": "Classify as refund, question or other: refund please"}],
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
print(r.choices[0].message.content)
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.axforge.ai/v1", apiKey: process.env.AXFORGE_API_KEY });
const r = await client.chat.completions.create({
model: "qwen3.8-27b",
messages: [{ role: "user", content: "Classify as refund, question or other: refund please" }],
chat_template_kwargs: { enable_thinking: false }, // passed through as is
});
console.log(r.choices[0].message.content);
Tip: give thinking room. With thinking enabled, the
model spends output tokens on reasoning_content before it
writes the answer, so a small max_tokens can be used up by
reasoning and return an empty content. Raise
max_tokens, or, for models that support it, disable thinking for short structured outputs.
GLM's thinking cannot be disabled.
GLM-5.3-Flash: effort and long answers
GLM's current default effort is max. You can request high or
low for lighter reasoning. These settings change how much reasoning the model attempts;
they do not reserve a fixed number of tokens or turn thinking off.
max_tokens is the combined allowance for reasoning and the final answer.
A small allowance can end at finish_reason: "length" while the model is still thinking,
leaving content empty. Give a long extraction or analysis enough room for both parts.
Streaming lets you follow delta.reasoning_content while waiting for
delta.content; the first reasoning token is not the start of the final answer.
For example, send this JSON body to POST /v1/chat/completions with your usual
Bearer key. Replace the document placeholder with your text:
{
"model": "glm-5.3-flash",
"messages": [{"role": "user", "content": "Extract every section and table from this document: [your document text]"}],
"stream": true,
"max_tokens": 32768,
"temperature": 1,
"top_p": 0.95,
"chat_template_kwargs": {"reasoning_effort": "max"}
}To try a lighter effort, change "max" to "high" or "low"
in chat_template_kwargs. The top-level form "reasoning_effort": "low"
is also accepted; use one form per request. If an SDK exposes extra JSON through extra_body,
put {"chat_template_kwargs": {"reasoning_effort": "high"}} there.
Z.ai recommends
temperature: 1, top_p: 0.95 and max effort.
Temperature zero is not a way to turn reasoning off or guarantee a faster answer.
What we measured
On 27 October 2026, we sent the same OCR text from one synthetic 12-page annual report to the
AxForge API, once per setting, sequentially. With max_tokens: 32768,
temperature: 1 and top_p: 0.95:
| Effort | Total time | Output tokens, including reasoning |
|---|---|---|
| max | 359.8 seconds | 12,547 |
| high | 124.8 seconds | 4,682 |
| low | 114.7 seconds | 4,313 |
All three produced answers, but this is one example, not a quality ranking or latency promise. They preserved an incorrect opening-cash figure already present in the supplied OCR; low also lost some unit context and contents-page references. All three wrapped JSON in Markdown fences despite a JSON-only instruction. Validate the answer's format, figures and units separately.
A max-effort run at temperature zero, with top_p omitted and the same output allowance,
also answered: 677.2 seconds and 23,873 output tokens, with strict JSON. A longer reasoning trace
does not by itself establish a loop. The response did not report separate reasoning-token counts.
The 32,768 allowance in the example is room to work, not a guaranteed answer length or a promise that every document fits. A separate request allowing 65,536 tokens was accepted, but the model stopped voluntarily well before that number; it did not establish the longest possible output or the service timeout.
An experimental thinking_token_budget: 4000 request ended reasoning mid-row and then
answered. We have not verified the exact threshold or its compatibility with tools. Do not rely on
it to guarantee an answer. Effort, total output allowance and a reasoning cap are different controls.
Image input (vision)
Qwen3.8 27B reads images. Make content an array that mixes
text and image_url parts. An image is either an
https:// URL we can fetch, or a base64 data: URL you
build from the file. You can send several images in one message, up
to 8 per request, and the model reads them in the order you list them.
Send them the best way:
- Resize big scans. Cost scales with pixels: a phone photo is about 500 to 1,500 tokens, a full A4 300-DPI page (2480×3508) about 8,500. Downscale documents to roughly 1,500 to 2,000 px on the long side unless the fine print matters. It is faster and cheaper, and just as accurate for invoices and forms.
- Use JPEG for photos and scans (a fraction of a PNG's size). Keep PNG only for screenshots and line art.
- One document per image. For a multi-page file, send one image per page, in order, and say so in the text.
- Put the images first, then your question. The model answers what the text asks about the images above it.
curl https://api.axforge.ai/v1/chat/completions \
-H "Authorization: Bearer $AXFORGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3.8-27b","messages":[{"role":"user","content":[{"type":"text","text":"Describe each image in one sentence, in order."},{"type":"image_url","image_url":{"url":"https://axforge.ai/assets/og-default.png"}},{"type":"image_url","image_url":{"url":"https://axforge.ai/assets/avatars.webp"}}]}]}'
$call = @{
Method = "Post"
Uri = "https://api.axforge.ai/v1/chat/completions"
Headers = @{ Authorization = "Bearer $env:AXFORGE_API_KEY" }
ContentType = "application/json"
Body = '{"model":"qwen3.8-27b","messages":[{"role":"user","content":[{"type":"text","text":"Describe each image in one sentence, in order."},{"type":"image_url","image_url":{"url":"https://axforge.ai/assets/og-default.png"}},{"type":"image_url","image_url":{"url":"https://axforge.ai/assets/avatars.webp"}}]}]}'
}
(Invoke-RestMethod @call).choices[0].message.content
curl.exe https://api.axforge.ai/v1/chat/completions ^
-H "Authorization: Bearer %AXFORGE_API_KEY%" ^
-H "Content-Type: application/json" ^
-d "{\"model\":\"qwen3.8-27b\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Describe each image in one sentence, in order.\"},{\"type\":\"image_url\",\"image_url\":{\"url\":\"https://axforge.ai/assets/og-default.png\"}},{\"type\":\"image_url\",\"image_url\":{\"url\":\"https://axforge.ai/assets/avatars.webp\"}}]}]}"
From your own files: the helper below resizes each one, encodes it as a JPEG data URL, and sends any number of images in one call.
# pip install openai pillow · the key is read from AXFORGE_API_KEY
import base64, io, os
from openai import OpenAI
from PIL import Image
client = OpenAI(base_url="https://api.axforge.ai/v1", api_key=os.environ["AXFORGE_API_KEY"])
def image_part(path, max_side=2000):
# open, shrink the long side to max_side, re-encode as JPEG, return a data-URL part
im = Image.open(path).convert("RGB")
scale = min(1.0, max_side / max(im.size))
if scale < 1.0:
im = im.resize((round(im.width * scale), round(im.height * scale)))
buf = io.BytesIO()
im.save(buf, "JPEG", quality=85)
b64 = base64.b64encode(buf.getvalue()).decode()
return {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
files = ["page-1.jpg", "page-2.jpg", "page-3.png"] # up to 8
parts = [{"type": "text",
"text": "These are the pages of one invoice, in order. "
"Return supplier, invoice number, date and total as JSON."}]
parts += [image_part(f) for f in files]
r = client.chat.completions.create(
model="qwen3.8-27b",
messages=[{"role": "user", "content": parts}],
)
print(r.choices[0].message.content)
// npm i openai sharp · save as pages.mjs, run: node pages.mjs
import OpenAI from "openai";
import sharp from "sharp";
const client = new OpenAI({ baseURL: "https://api.axforge.ai/v1", apiKey: process.env.AXFORGE_API_KEY });
// shrink the long side to maxSide, re-encode as JPEG, return a data-URL part
async function imagePart(path, maxSide = 2000) {
const jpeg = await sharp(path)
.resize({ width: maxSide, height: maxSide, fit: "inside", withoutEnlargement: true })
.jpeg({ quality: 85 })
.toBuffer();
return { type: "image_url", image_url: { url: `data:image/jpeg;base64,${jpeg.toString("base64")}` } };
}
const files = ["page-1.jpg", "page-2.jpg", "page-3.png"]; // up to 8
const parts = [{ type: "text", text: "These are the pages of one invoice, in order. " +
"Return supplier, invoice number, date and total as JSON." }];
for (const f of files) parts.push(await imagePart(f));
const r = await client.chat.completions.create({
model: "qwen3.8-27b",
messages: [{ role: "user", content: parts }],
});
console.log(r.choices[0].message.content);
Context & pricing
| Context window | 262,144 tokens |
|---|---|
| Input | €0.12 / 1M tokens (launch pricing) |
| Output | €0.62 / 1M tokens (launch pricing) |
| Image input | ~500 to 9,000 prompt tokens per image by size (a full A4 scan ~8,500), billed as input |
| Reasoning | Billed as output tokens |
| GLM-5.3-Flash | €0.12 / 1M input · €0.62 / 1M output, 262,144 tokens, text only |
| MiMo-V2.6-Flash | €0.12 / 1M input · €0.62 / 1M output, 262,144 tokens, text only |
| Qwen3.8 Flash Next | €0.12 / 1M input · €0.62 / 1M output, 500,000 tokens, text and images |
More on how prices are set: Models & pricing and the pricing page. Concurrency and error behavior: Errors & limits.