Chat Completions API

/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 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 instead.

The request

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

Each message carries a role and content:

RoleUse
systemStanding instructions for the model. Put it first.
userWhat the person said.
assistantWhat the model said on a previous turn.
toolThe result of a tool call you executed. See 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:

1{
2 "model": "asi1",
3 "messages": [
4 { "role": "system", "content": "Be precise and concise." },
5 { "role": "user", "content": "What is agentic AI?" },
6 { "role": "assistant", "content": "Agentic AI describes systems that..." },
7 { "role": "user", "content": "How is that different from a chatbot?" }
8 ]
9}

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 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 reconstructs the thread server side from previous_response_id.

The reply

1{
2 "id": "9f41ab07c5d24e8fa6b2c1d70e5384bb",
3 "object": "chat.completion",
4 "created": 1768412905,
5 "model": "asi1",
6 "choices": [
7 {
8 "index": 0,
9 "finish_reason": "stop",
10 "message": {
11 "role": "assistant",
12 "content": "Agentic AI describes systems that...",
13 "tool_calls": null,
14 "reasoning_content": null
15 }
16 }
17 ],
18 "usage": {
19 "prompt_tokens": 2118,
20 "completion_tokens": 186,
21 "total_tokens": 2304,
22 "prompt_tokens_details": { "cached_tokens": 1920 },
23 "reasoning_tokens": 0
24 }
25}

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

ValueMeaning
stopThe model finished its answer.
tool_callsThe model wants you to run a tool and send the result back.
lengthGeneration 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, when you asked for it. See Reasoning.
  • choices[0].message.tool_calls - tools the model wants you to run. See 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.
  • usage.reasoning_tokens - tokens spent reasoning before answering.

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.

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

1import json
2import os
3
4import requests
5
6resp = requests.post(
7 "https://api.asi1.ai/v1/chat/completions",
8 headers={
9 "Authorization": f"Bearer {os.getenv('ASI_ONE_API_KEY')}",
10 "Content-Type": "application/json",
11 },
12 json={
13 "model": "asi1",
14 "messages": [{"role": "user", "content": "Explain blockchain simply"}],
15 "stream": True,
16 },
17 stream=True,
18)
19resp.raise_for_status()
20
21for line in resp.iter_lines(decode_unicode=True):
22 if not line or not line.startswith("data: "):
23 continue
24 payload = line[len("data: "):]
25 if payload == "[DONE]":
26 break
27 chunk = json.loads(payload)
28 if not chunk["choices"]:
29 continue
30 delta = chunk["choices"][0]["delta"]
31 if delta.get("content"):
32 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.

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 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. Image input is supported on asi1.

$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": "https://example.com/photo.jpg"}}
> ]
> }
> ]
> }'

The Python example reuses the client built in The request.

ASI:One fetches the image server side, so the URL has to be publicly reachable: a link behind authentication or on a private network will not resolve. Pass a hosted link rather than an inline data: URI.

The answer comes back as ordinary text, so nothing else about the request or the reply changes. Image input is specific to this endpoint; /v1/responses takes text input.

What else this endpoint takes

For which OpenAI parameters apply here and which belong on /v1/responses, see OpenAI Compatibility.

Next steps

  1. Responses API - The stateful alternative, with retrieve and chaining
  2. Tool Calling - Let the model call your own functions
  3. Planner Mode - Multi-step work against Agentverse agents
  4. Errors and Rate Limits - What each status code means and what to retry