Documentation / API

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
  }'
# 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)

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."}]}'

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)

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."

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}}'
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)

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:

EffortTotal timeOutput tokens, including reasoning
max359.8 seconds12,547
high124.8 seconds4,682
low114.7 seconds4,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"}}]}]}'

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)

Context & pricing

Context window262,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
ReasoningBilled 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.

Ask on the forum Markdown For AI Updated 2026-11-06

Anything unclear on this page?

Ask on the forum. The answer helps the next person too.

Ask about this page