> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.asi1.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.asi1.ai/_mcp/server.

# Chat Completions API

> Send a conversation to /v1/chat/completions, read the reply, stream it, and carry history across turns

`/v1/chat/completions` implements OpenAI's Chat Completions format. You send the
whole conversation as a `messages` array and get one reply back. It is the
default endpoint, what most OpenAI code already speaks, and where
[planner mode](/documentation/build-with-asi-one/planner) runs.

If you would rather the server keep the thread for you, and you want to retrieve
or chain responses by id, use the
[Responses API](/documentation/build-with-asi-one/responses) instead.

## The request

#### cURL

```bash
curl -X POST https://api.asi1.ai/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ASI_ONE_API_KEY" \
  -d '{
    "model": "asi1",
    "messages": [
      {"role": "system", "content": "Be precise and concise."},
      {"role": "user", "content": "What is agentic AI?"}
    ]
  }'
```

#### Python

```python
import os

from openai import OpenAI

client = OpenAI(
    api_key=os.getenv("ASI_ONE_API_KEY"),
    base_url="https://api.asi1.ai/v1",
)

completion = client.chat.completions.create(
    model="asi1",
    messages=[
        {"role": "system", "content": "Be precise and concise."},
        {"role": "user", "content": "What is agentic AI?"},
    ],
)

print(completion.choices[0].message.content)
```

Each message carries a `role` and `content`:

| Role        | Use                                                                                                         |
| ----------- | ----------------------------------------------------------------------------------------------------------- |
| `system`    | Standing instructions for the model. Put it first.                                                          |
| `user`      | What the person said.                                                                                       |
| `assistant` | What the model said on a previous turn.                                                                     |
| `tool`      | The result of a tool call you executed. See [Tool Calling](/documentation/build-with-asi-one/tool-calling). |

## Carrying a conversation

This endpoint does not keep your conversation. Each request is independent, so a
follow-up turn means sending the earlier turns again, with the model's own
replies as `assistant` messages:

```json
{
  "model": "asi1",
  "messages": [
    { "role": "system", "content": "Be precise and concise." },
    { "role": "user", "content": "What is agentic AI?" },
    { "role": "assistant", "content": "Agentic AI describes systems that..." },
    { "role": "user", "content": "How is that different from a chatbot?" }
  ]
}
```

That is the same as OpenAI's Chat Completions API, so existing conversation
handling carries over unchanged.

Because you resend the history every turn, it grows, and the whole array counts
against the model's context window along with the reply. Keep an eye on the
[context window](/documentation/models#context-windows) for the model you are
using and drop the oldest turns as the conversation gets long. If you would
rather not manage this yourself, the [Responses API](/documentation/build-with-asi-one/responses) reconstructs the thread server
side from `previous_response_id`.

## The reply

```json
{
  "id": "chatcmpl-3e9a4c27-6f1b-4d85-b0e2-7a5c9d1f8b64",
  "object": "chat.completion",
  "created": 1768412905,
  "model": "asi1",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "Agentic AI describes systems that...",
        "tool_calls": null,
        "reasoning_content": "The user wants a precise, concise definition of agentic AI. ..."
      }
    }
  ],
  "usage": {
    "prompt_tokens": 265,
    "completion_tokens": 274,
    "total_tokens": 539,
    "prompt_tokens_details": { "cached_tokens": 0, "created_cache_tokens": 0 },
    "completion_tokens_details": { "reasoning_tokens": 93 }
  }
}
```

`finish_reason` tells you why generation stopped, and is the field to branch on:

| Value        | Meaning                                                         |
| ------------ | --------------------------------------------------------------- |
| `stop`       | The model finished its answer.                                  |
| `tool_calls` | The model wants you to run a tool and send the result back.     |
| `length`     | Generation stopped at a token limit, so the reply is cut short. |

Handle an unrecognized value by treating the reply as finished, so a new value
does not break your client.

The rest of the fields:

* **`choices[0].message.content`** - the assistant's reply.
* **`choices[0].message.reasoning_content`** - the model's reasoning: `null` when reasoning is off, an empty string when the model reasoned without producing text. See [Reasoning](/documentation/build-with-asi-one/reasoning).
* **`choices[0].message.tool_calls`** - tools the model wants you to run. See [Tool Calling](/documentation/build-with-asi-one/tool-calling).
* **`usage.prompt_tokens` / `completion_tokens` / `total_tokens`** - token accounting for cost and limits.
* **`usage.prompt_tokens_details.cached_tokens`** - how much of your prompt was served from cache. Cached tokens still count toward `prompt_tokens`. See [Prompt caching](/documentation/build-with-asi-one/prompt-caching) for what keeps a prefix cacheable.
* **`usage.prompt_tokens_details.created_cache_tokens`** - how much of your prompt was written to the cache. It is non-zero on the first request with a new prefix and drops to zero once that prefix is cached, which is what tells a cold cache apart from one that is not being used.
* **`usage.completion_tokens_details.reasoning_tokens`** - tokens spent reasoning before answering. They are part of `completion_tokens`.

The response may gain fields over time. Read the ones you need and ignore any you
do not recognize, rather than depending on the exact shape.

## Streaming

Set `"stream": true` and the reply arrives as server-sent events instead of one
JSON body. Use it for anything a person is waiting on, so they see text appear
rather than a blank screen.

```bash
curl -X POST https://api.asi1.ai/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ASI_ONE_API_KEY" \
  -d '{
    "model": "asi1",
    "messages": [{"role": "user", "content": "Explain blockchain simply"}],
    "stream": true
  }'
```

Each frame is a `data:` line holding one chunk, and the stream ends with a
literal `[DONE]` sentinel:

```
data: {"choices":[{"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"choices":[{"delta":{"content":"Block"},"finish_reason":null}]}

data: {"choices":[{"delta":{"content":"chain is"},"finish_reason":null}]}

data: {"choices":[{"delta":{},"finish_reason":"stop"}]}

data: {"choices":[],"usage":{"prompt_tokens":18,"completion_tokens":112,"total_tokens":130}}

data: [DONE]
```

Four things to handle:

* **Check `choices` is non-empty before indexing it.** The final chunk before the
  sentinel reports `usage` and carries `choices: []`, so code that reaches
  straight for `choices[0]` on every chunk will fail on the last one.
* **Concatenate `choices[0].delta.content`** across chunks to build the reply.
  Any given chunk may carry no content.
* **Read `finish_reason` on every chunk.** It arrives on a chunk whose `delta` is
  empty, so do not wait for one that also carries content.
* **Stop on `data: [DONE]`.** It is a literal string, not JSON, so parse it as a
  sentinel before attempting to decode the payload.

All four, in a loop over the raw HTTP response:

```python
import json
import os

import requests

resp = requests.post(
    "https://api.asi1.ai/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {os.getenv('ASI_ONE_API_KEY')}",
        "Content-Type": "application/json",
    },
    json={
        "model": "asi1",
        "messages": [{"role": "user", "content": "Explain blockchain simply"}],
        "stream": True,
    },
    stream=True,
)
resp.raise_for_status()

for line in resp.iter_lines(decode_unicode=True):
    if not line or not line.startswith("data: "):
        continue
    payload = line[len("data: "):]
    if payload == "[DONE]":
        break
    chunk = json.loads(payload)
    if not chunk["choices"]:
        continue
    delta = chunk["choices"][0]["delta"]
    if delta.get("content"):
        print(delta["content"], end="", flush=True)
```

Passing `stream=True` to `requests.post` matters as much as sending it in the
body: without it the client buffers the whole response and you get the text in
one piece at the end.

When reasoning is on, it streams in its own field and generally arrives before
any `content`:

```
data: {"choices":[{"delta":{"reasoning_content":"The question asks..."}}]}
```

If you use the OpenAI SDK, its streaming iterator handles the framing and the
sentinel for you.

> **Info**
>
> Tool calls also arrive in the delta when streaming. Each call comes complete in
> a single chunk, keyed by its `index` and carrying the whole `arguments` string,
> on a chunk with `finish_reason: "tool_calls"`. You do not need to accumulate
> argument fragments. See [Tool Calling](/documentation/build-with-asi-one/tool-calling) for what to do with the
> call once you have it.

## Image input

Ask about an image by sending an `image_url` content part alongside your text,
in OpenAI's format. Every model takes image input, under the same rules: the
same formats, up to **ten images** per request, and up to **20 MB** per image
once decoded. Send the image inline as a `data:` URI.

#### cURL

```bash
curl -X POST https://api.asi1.ai/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ASI_ONE_API_KEY" \
  -d '{
    "model": "asi1",
    "messages": [
      {
        "role": "user",
        "content": [
          {"type": "text", "text": "What is in this image?"},
          {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,<BASE64_ENCODED_IMAGE>"}}
        ]
      }
    ]
  }'
```

#### Python

```python
import base64

with open("photo.jpg", "rb") as image_file:
    encoded = base64.b64encode(image_file.read()).decode("utf-8")

completion = client.chat.completions.create(
    model="asi1",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "What is in this image?"},
                {
                    "type": "image_url",
                    "image_url": {"url": f"data:image/jpeg;base64,{encoded}"},
                },
            ],
        }
    ],
)

print(completion.choices[0].message.content)
```

The Python example reuses the `client` built in [The request](#the-request).

Send images inline as a `data:` URI. ASI:One does not fetch images from
arbitrary hosts, so a hosted `https` link is not a substitute. That includes
any link behind authentication or on a private network.

An image that cannot be read is rejected rather than quietly left out, so a
`200` means every image you sent was used. The request returns `400` with
`code: "unsupported_content_part"` and `param` naming the part, for an
`image_url` carrying no URL, base64 that does not decode, an image over 20 MB
once decoded, more images than the model accepts, a host ASI:One does not fetch
from, and any content part that is not `text` or `image_url`. Correct the part and send it
again; see [Errors](/documentation/build-with-asi-one/errors).

The answer comes back as ordinary text, so nothing else about the request or the
reply changes. [`/v1/responses`](/documentation/build-with-asi-one/responses#image-input)
takes images too, under the same limits as here, with OpenAI's `input_image` part.

## What else this endpoint takes

* **[Tool calling](/documentation/build-with-asi-one/tool-calling)** with `tools` and `tool_choice`.
* **[Structured data](/documentation/build-with-asi-one/structured-data)** with `response_format`.
* **[Reasoning](/documentation/build-with-asi-one/reasoning)** with `reasoning_effort`, plus `enable_thinking` and `thinking_budget`.
* **[Planner mode](/documentation/build-with-asi-one/planner)** with `planner_mode`, which runs here and not on `/v1/responses`.
* **[Image input](#image-input)** with OpenAI's `image_url` content parts, on every model. Send the image inline as a `data:` URI.

For which OpenAI parameters apply here and which belong on `/v1/responses`, see
[OpenAI Compatibility](/documentation/build-with-asi-one/openai-compatibility).

## Next steps

1. **[Responses API](/documentation/build-with-asi-one/responses)** - The stateful alternative, with retrieve and chaining
2. **[Tool Calling](/documentation/build-with-asi-one/tool-calling)** - Let the model call your own functions
3. **[Planner Mode](/documentation/build-with-asi-one/planner)** - Multi-step work against Agentverse agents
4. **[Errors and Rate Limits](/documentation/build-with-asi-one/errors)** - What each status code means and what to retry