# Chat completions


# 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}"
```

    Python

```
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]
```

    Python

```
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:

    Python

```
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

```
$ 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}
  }'
```

    Python

```
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** &mdash; up
    to **8** per request &mdash; 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&ndash;1,500 tokens, a full A4 300-DPI page (2480&times;3508) about 8,500. Downscale
      documents to roughly 1,500&ndash;2,000 px on the long side unless the fine print
      matters &mdash; 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** &mdash; the model answers what the
      text asks about the images above it.

    curl &middot; two images

```
$ 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.

    Python &middot; several files, resized

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

      &larr; Models & pricing
      Embeddings &rarr;



Source: https://axforge.ai/docs/chat/
