Chat completions
POST /v1/chat/completions — the OpenAI chat shape, served by
Qwen3.8 27B as
qwen3.8-27b-nvfp4. 262,144-token context. €0.29 / 1M input tokens, €1.77 / 1M output tokens (launch pricing). Streaming, tool calls,
reasoning output and image input all work on this one endpoint.
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-nvfp4",
"messages": [
{"role": "system", "content": "You answer in one sentence."},
{"role": "user", "content": "What is an embedding?"}
],
"max_tokens": 2048
}'
PS> $body = @{
model = 'qwen3.8-27b-nvfp4'
messages = @(
@{ role = 'system'; content = 'You answer in one sentence.' },
@{ role = 'user'; content = 'What is an embedding?' }
)
max_tokens = 2048
} | ConvertTo-Json -Depth 5
PS> $r = Invoke-RestMethod https://api.axforge.ai/v1/chat/completions -Method Post `
-Headers @{ Authorization = "Bearer $env:AXFORGE_API_KEY" } `
-ContentType 'application/json' -Body $body
PS> $r.choices[0].message.content
REM cmd keeps single quotes, so the inner quotes are escaped instead.
C:\> 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-nvfp4\",\"messages\":[{\"role\":\"system\",\"content\":\"You answer in one sentence.\"},{\"role\":\"user\",\"content\":\"What is an embedding?\"}],\"max_tokens\":2048}"
from openai import OpenAI
client = OpenAI(
base_url="https://api.axforge.ai/v1",
api_key="YOUR_AXFORGE_KEY",
)
r = client.chat.completions.create(
model="qwen3.8-27b-nvfp4",
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-nvfp4",
"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-nvfp4", "stream": true,
"messages": [{"role": "user", "content": "Count to three."}]}'
# the wire format:
data: {"choices":[{"delta":{"content":"One"}}]}
data: {"choices":[{"delta":{"content":", two"}}]}
data: [DONE]
# 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:
PS> $body = '{"model":"qwen3.8-27b-nvfp4","stream":true,"messages":[{"role":"user","content":"Count to three."}]}'
PS> (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:
PS> $req = [System.Net.HttpWebRequest]::Create("https://api.axforge.ai/v1/chat/completions")
PS> $req.Method = "POST"; $req.ContentType = "application/json"
PS> $req.Headers.Add("Authorization", "Bearer $env:AXFORGE_API_KEY")
PS> $bytes = [Text.Encoding]::UTF8.GetBytes($body); $req.ContentLength = $bytes.Length
PS> $s = $req.GetRequestStream(); $s.Write($bytes,0,$bytes.Length); $s.Close()
PS> $rd = New-Object IO.StreamReader($req.GetResponse().GetResponseStream())
PS> while (-not $rd.EndOfStream) { $l = $rd.ReadLine(); if ($l -like "data: *") { $l } }
C:\> 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-nvfp4\",\"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]
r = client.chat.completions.create(
model="qwen3.8-27b-nvfp4",
stream=True,
messages=[{"role": "user", "content": "Count to three."}],
)
for chunk in r:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="")
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
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-nvfp4", 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": "2041", "total_eur": 1240.0}),
})
r = client.chat.completions.create(
model="qwen3.8-27b-nvfp4", 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.
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-nvfp4",
"messages": [{"role": "user", "content": "Classify: \"refund please\""}],
"chat_template_kwargs": {"enable_thinking": false}
}'
r = client.chat.completions.create(
model="qwen3.8-27b-nvfp4",
messages=[{"role": "user", "content": "Classify: \"refund please\""}],
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
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 disable thinking for short structured outputs.
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–1,500 tokens, a full A4 300-DPI page (2480×3508) about 8,500. Downscale documents to roughly 1,500–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-nvfp4",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "These are two invoice pages. What is the grand total?"},
{"type": "image_url", "image_url": {"url": "https://example.com/page-1.jpg"}},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."}}
]
}]
}'
The Python helper below resizes each file, encodes it as a JPEG data URL, and sends any number of images in one call.
import base64, io
from openai import OpenAI
from PIL import Image # pip install pillow openai
client = OpenAI(base_url="https://api.axforge.ai/v1", api_key="YOUR_AXFORGE_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-nvfp4",
messages=[{"role": "user", "content": parts}],
)
print(r.choices[0].message.content)
Context & pricing
| Context window | 262,144 tokens |
|---|---|
| Input | €0.29 / 1M tokens (launch pricing) |
| Output | €1.77 / 1M tokens (launch pricing) |
| Image input | ~500–9,000 prompt tokens per image by size (a full A4 scan ~8,500), billed as input |
| Reasoning | Billed as output tokens |
More on how prices are set: Models & pricing and the pricing page. Concurrency and error behavior: Errors & limits.