C.W.K.
Stream
Lesson 02 of 05 · published

POST /api/chat — The Conversational Endpoint

~26 min · api, chat, messages

Level 0Downloader
0 XP0/41 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

The shape

/api/chat takes a messages array (system / user / assistant / tool roles) and returns either streaming NDJSON or a single JSON object depending on stream. This is the endpoint you'll use for everything except raw completion / FIM.

Request fields

  • model (required) — the model name as it appears in ollama list.
  • messages — array of {role, content, images?, tool_calls?}. Roles: system, user, assistant, tool.
  • stream — boolean (default true).
  • format"json" or a JSON Schema object for structured outputs.
  • options — inference parameters (temperature, top_p, num_ctx, num_predict, ...).
  • tools — array of OpenAI-format tool definitions for function calling.
  • keep_alive — how long the model stays loaded after this request (default "5m"; set "-1" to pin).
  • think — boolean; enables thinking output for reasoning models that support it.

Response fields (non-streaming)

  • message{role, content, tool_calls?}.
  • done — boolean (always true when stream is off).
  • Timing: total_duration, load_duration, prompt_eval_count, prompt_eval_duration, eval_count, eval_duration — all in nanoseconds.

Why prefer /api/chat over /api/generate

The messages shape forces you to think about role boundaries (system instructions vs user turns vs assistant outputs vs tool results). That structure is what makes tool use, multi-turn context, and the adapter pattern coherent later. /api/generate looks simpler but pushes you toward concatenated-string prompts that are harder to compose.

Code

Python — non-streaming chat·python
import httpx

def chat(model: str, messages: list[dict]) -> dict:
    resp = httpx.post(
        "http://localhost:11434/api/chat",
        json={"model": model, "messages": messages, "stream": False},
        timeout=120.0,
    )
    resp.raise_for_status()
    return resp.json()

result = chat("qwen2.5:7b", [
    {"role": "system", "content": "You are concise."},
    {"role": "user", "content": "Define quantization in two sentences."},
])
print(result["message"]["content"])
print(f"Took {result['total_duration'] / 1e9:.2f}s")
print(f"Generated {result['eval_count']} tokens at "
      f"{result['eval_count'] / (result['eval_duration'] / 1e9):.1f} tok/s")
TypeScript — non-streaming chat·typescript
type Msg = { role: "system" | "user" | "assistant" | "tool"; content: string };

async function chat(model: string, messages: Msg[]) {
  const res = await fetch("http://localhost:11434/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ model, messages, stream: false }),
  });
  if (!res.ok) throw new Error(`Ollama ${res.status}: ${await res.text()}`);
  return res.json();
}

const out = await chat("qwen2.5:7b", [
  { role: "system", content: "You are concise." },
  { role: "user", content: "Define quantization in two sentences." },
]);
console.log(out.message.content);

External links

Exercise

Write a Python function chat(model, messages, **opts) that hits /api/chat non-streaming, returns the message content, and logs total duration in seconds and tokens-per-second. Test it with two different models and one identical prompt. Note the speed difference.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.