Skip to content
Back to skills

Ollama Api

ASecurity

The Ollama REST API as it is on this machine - /api/generate and /api/chat with streaming NDJSON, /api/embed vs the older /api/embeddings, /api/tags, /api/ps, /api/show, pull progress, create/copy/delete, the OpenAI-compatible /v1 endpoints, the options map, keep_alive, the error shapes, and the environment variables that change what the daemon does.

  • 10 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentsjavascriptjavabashdebuggingapi

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumUses curl or wget to download content

Pro shows the line behind each finding and how to fix it

Scanned October 20, 2026

npx -y skills add vecnode/vn-code --skill ollama-api --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ollama Api?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ollama Api
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vecnode-ollama-api/badge)](https://www.skillsdirectory.com/skills/vecnode-ollama-api)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ollama-api
description: The Ollama REST API as it is on this machine - /api/generate and /api/chat with streaming NDJSON, /api/embed vs the older /api/embeddings, /api/tags, /api/ps, /api/show, pull progress, create/copy/delete, the OpenAI-compatible /v1 endpoints, the options map, keep_alive, the error shapes, and the environment variables that change what the daemon does.
whenToUse: Use before writing any code that calls Ollama over HTTP, before debugging a request that returns 404 / 500 / an empty body, when a stream seems to end early, when structured output or tool calling is wanted, or when choosing between the native API and the OpenAI-compatible one.
---

# The Ollama REST API

The daemon is a plain HTTP server on the loopback interface. Everything below is
the shape this install answers with - not a client library's wrapper.

## The address

Ollama binds `127.0.0.1:11434` unless `OLLAMA_HOST` says otherwise. The value is
`host:port` (a scheme is accepted). Two different readers use it:

- the **daemon** reads `OLLAMA_HOST` as the address to BIND (`0.0.0.0:11434` to
  serve the whole network);
- a **client** reads it as the address to CONNECT to.

That asymmetry is the classic mistake: setting `OLLAMA_HOST=0.0.0.0:11434` for
the server and then pointing a client at `http://0.0.0.0:11434` works on Linux
and fails on Windows. Clients should default to `http://127.0.0.1:11434`.

The first request of any session should be the version, because every other
behaviour is version-dependent:

```bash
curl -s http://127.0.0.1:11434/api/version
```

```json
{"version":"0.40.3"}
```

`dsh-ollama`'s `ollama_status` answers this and the model inventory in one call;
when you are writing a program, this is the endpoint to probe.

## The three request shapes

### One prompt: `/api/generate`

```bash
curl -s http://127.0.0.1:11434/api/generate -d '{"model":"qwen2.5:7b","prompt":"Why is the sky blue?","stream":false}'
```

`stream: false` answers ONE JSON object carrying the whole completion and every
timing field. `stream: true` (the daemon's default) answers NDJSON - see below.

### A conversation: `/api/chat`

```bash
curl -s http://127.0.0.1:11434/api/chat -d '{"model":"qwen2.5:7b","messages":[{"role":"system","content":"Be brief."},{"role":"user","content":"Why is the sky blue?"}],"stream":false}'
```

Roles are `system`, `user`, `assistant` and `tool`. The same model, template and
options apply; the difference is that the daemon applies the chat template to
the whole array instead of wrapping one prompt.

### Vectors: `/api/embed`

```bash
curl -s http://127.0.0.1:11434/api/embed -d '{"model":"nomic-embed-text","input":["the cat sat","a cat was sitting"]}'
```

`input` is a string or an array. The answer is `{"embeddings": [[...], [...]],
"model": "...", ...}` - one vector per input, in order. Older daemons only have
the single-prompt form:

```bash
curl -s http://127.0.0.1:11434/api/embeddings -d '{"model":"nomic-embed-text","prompt":"the cat sat"}'
```

That answers `{"embedding": [...]}`. Prefer `/api/embed`: one request, many
inputs, and a dimension you can rely on. If `/api/embed` returns 404, fall back.

## Streaming is NDJSON

A streaming answer is one JSON object per line, each line terminated by `\n`,
with `done: false` on every partial object and `done: true` on the last one. The
partial objects carry `response` (generate) or `message.content` (chat); the
FINAL object carries the token counts and the durations. Nothing before the last
line has a measurement in it.

```bash
curl -s http://127.0.0.1:11434/api/generate -d '{"model":"qwen2.5:7b","prompt":"Say hello"}'
```

Three properties follow from that shape, and each one is a bug waiting to
happen:

1. **A line is not a document.** Parse per line; do not buffer the whole body
   and `JSON.parse` it.
2. **The last line is the measurement.** If your reader stops at the first
   `done: true` it has the numbers; if it stops at end-of-stream it may have
   them too, but the `done` object is the contract.
3. **A stream can be cut.** A cancelled request, a killed daemon or a timeout
   ends the stream with no `done` object at all. Treat "no done object" as a
   failed measurement, not as an empty answer.

A reader that keeps the text and the final object:

```javascript
const response = await fetch('http://127.0.0.1:11434/api/generate', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ model: 'qwen2.5:7b', prompt: 'Say hello', stream: true }),
})
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let text = ''
let final = null
for (;;) {
  const { value, done } = await reader.read()
  if (done) break
  buffer += decoder.decode(value, { stream: true })
  let newline = buffer.indexOf('\n')
  while (newline >= 0) {
    const line = buffer.slice(0, newline).trim()
    buffer = buffer.slice(newline + 1)
    newline = buffer.indexOf('\n')
    if (line === '') continue
    const item = JSON.parse(line)
    if (item.response) text += item.response
    if (item.done === true) final = item
  }
}
if (final === null) throw new Error('the stream ended without a done object')
console.log(text, final.eval_count, final.eval_duration)
```

(The arithmetic that turns those two numbers into tok/s is the
`ollama-inference-metrics` skill's subject.)

## The final object

`/api/generate` with `stream: false` and the last line of a streaming call have
the same fields:

| field | unit | what it covers |
|---|---|---|
| `response` | string | the generated text (chat: `message.content`) |
| `done` | bool | `true` on the final object |
| `done_reason` | string | `stop` (the model stopped) or `length` (it hit `num_predict`/context) |
| `context` | int[] | the token ids of prompt+answer - pass it back as `context` to continue |
| `total_duration` | ns | the whole daemon-side request |
| `load_duration` | ns | loading the weights (0 when they were resident) |
| `prompt_eval_count` | int | prompt tokens evaluated |
| `prompt_eval_duration` | ns | evaluating the prompt |
| `eval_count` | int | tokens generated |
| `eval_duration` | ns | generating them |

`done_reason: "length"` is the one field a reader must not ignore: the answer
was CUT by your `num_predict` or the context window, not finished by the model.

## Options and `keep_alive`

`options` is the sampling/runtime map. The ones that change the measurement:

| option | range | effect |
|---|---|---|
| `num_predict` | -1..N | max tokens to generate; `-1` (the default) means "until the model stops or the context fills" |
| `temperature` | 0..2 | 0 = greedy; the only defensible setting for a reproducible benchmark |
| `seed` | int | fixes sampling when temperature > 0; set it anyway so a rerun is comparable |
| `num_ctx` | 128.. | context window in tokens; the single biggest memory/speed knob |
| `top_k`, `top_p`, `min_p` | | sampling cutoffs |
| `repeat_penalty`, `repeat_last_n` | | repetition control |
| `stop` | string[] | stop sequences |
| `num_gpu` | int | layers offloaded to the GPU (0 = CPU only) |
| `num_thread` | int | CPU threads |
| `use_mmap`, `use_mlock` | bool | weight paging |

Anything the daemon does not recognise in `options` is **silently ignored** -
which is why a typo is worse than an error: the run reports numbers taken at a
setting nobody chose. `dsh-ollama` closes the map for exactly that reason.

`keep_alive` is a TOP-LEVEL sibling of `options`, not an option:

- `"5m"`, `"30s"`, `"1h"` - keep the weights loaded that long after the request;
- `0` - unload as soon as this request finishes;
- `-1` - never unload.

```bash
curl -s http://127.0.0.1:11434/api/generate -d '{"model":"qwen2.5:7b","prompt":"hi","keep_alive":"10m"}'
```

Unloading is the documented use of an empty request:

```bash
curl -s http://127.0.0.1:11434/api/generate -d '{"model":"qwen2.5:7b","keep_alive":0}'
```

That is the call that makes a "cold" benchmark honest: without it, run 1 is
as warm as run 2 and the load time you meant to measure never appears.

## Models: list, inspect, pull, delete

```bash
curl -s http://127.0.0.1:11434/api/tags
```

`/api/tags` is the inventory: `name`, `size` (bytes), `digest` (`sha256:...`),
`modified_at`, and `details` (`family`, `families`, `parameter_size`,
`quantization_level`, `format`).

```bash
curl -s http://127.0.0.1:11434/api/ps
```

`/api/ps` is what is LOADED right now: `size`, `size_vram`, `context_length`,
`expires_at`, and the same `details`. `size_vram` is 0 for a CPU-resident model;
`size_vram / size` is the offload share.

```bash
curl -s http://127.0.0.1:11434/api/show -d '{"model":"qwen2.5:7b"}'
```

`/api/show` is the model's own metadata: `details`, `parameters` (the Modelfile
defaults - read them before assuming `temperature`), `template`, `license`,
`model_info` (`general.architecture`, `*.context_length`, `*.embedding_length`,
`*.block_count`) and `capabilities` (`completion`, `tools`, `embedding`,
`vision`, `thinking` depending on the model). `capabilities` is what tells you
whether a tag can embed at all.

```bash
curl -s http://127.0.0.1:11434/api/pull -d '{"model":"qwen2.5:7b","stream":true}'
```

A pull is NDJSON progress, not a completion: `{"status":"pulling manifest"}`,
then one line per layer with `digest`, `total` and `completed`, then
`{"status":"verifying sha256 digest"}`, then `{"status":"success"}`. It is
measured in gigabytes and minutes; there is no partial-success state - a cut
pull leaves layers cached and the next pull resumes them.

```bash
curl -s -X DELETE http://127.0.0.1:11434/api/delete -d '{"model":"llama3.2:1b"}'
```

Delete answers `200` with an empty body. It frees the bytes immediately; the
model comes back only by pulling it again. `dsh-ollama` requires the tag
repeated as `confirm` for this reason.

The other model verbs, for completeness:

```bash
curl -s http://127.0.0.1:11434/api/copy -d '{"source":"qwen2.5:7b","destination":"qwen2.5:7b-mine"}'
curl -s http://127.0.0.1:11434/api/create -d '{"model":"my-model","from":"qwen2.5:7b","system":"You answer in one sentence."}'
```

`copy` and `create` are not tools in this pack - they mutate the model store in
ways an agent should not do implicitly - but they are part of the surface and
worth knowing when a user asks why a tag exists.

## Errors, and what they mean

Ollama reports a problem as a non-2xx status with a JSON body `{"error": "..."}`.

| status | body | meaning | the fix |
|---|---|---|---|
| 404 | `model "x" not found, try pulling it first` | the tag is not on disk | `ollama pull x` |
| 404 | `404 page not found` | the route does not exist on this daemon (an older build, or a typo) | check `/api/version`, use the older shape |
| 400 | `invalid options` / `json: cannot unmarshal ...` | the body's shape is wrong | compare with this page |
| 500 | `...` | the model failed while loading or running | read the message; often memory (`num_ctx` too large) |
| (none) | `fetch failed` / `ECONNREFUSED` | the DAEMON is not running | `ollama serve`, or the desktop app |

A 500 whose message mentions memory is not a bug in your code: `num_ctx` sizes
the KV cache, and a context the machine cannot hold fails at load. Try a smaller
`num_ctx` or a smaller quantization.

## Structured output

`format` is a top-level field. `"json"` asks for any valid JSON; a JSON Schema
asks for that shape and is the reliable form:

```bash
curl -s http://127.0.0.1:11434/api/generate -d '{"model":"qwen2.5:7b","prompt":"Give me one user: name and age.","format":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"]},"stream":false}'
```

The answer's text is a JSON document (not a Markdown fence). Parse it and fail
loudly when it does not parse - a schema is a strong request, not a guarantee.

## Tool calling

`/api/chat` accepts `tools` (a JSON-Schema function list) in the same shape as
the OpenAI tools array. The model answers with
`message.tool_calls: [{ function: { name, arguments } }]` and no content; you run
the function, append a `{role: "tool", content: "...", tool_name: "..."}`
message, and call again.

```bash
curl -s http://127.0.0.1:11434/api/chat -d '{"model":"qwen2.5:7b","messages":[{"role":"user","content":"What is the weather in Lisbon?"}],"tools":[{"type":"function","function":{"name":"weather","description":"Current weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],"stream":false}'
```

Not every model supports tools: check `capabilities` from `/api/show` first, or
you will get a plain-text answer that looks like a refusal.

## The OpenAI-compatible endpoints

Ollama also serves `/v1`: the same daemon, the OpenAI wire format. Use it when a
client library or SDK already speaks OpenAI and you cannot change it.

```bash
curl -s http://127.0.0.1:11434/v1/chat/completions -d '{"model":"qwen2.5:7b","messages":[{"role":"user","content":"hi"}]}'
curl -s http://127.0.0.1:11434/v1/completions -d '{"model":"qwen2.5:7b","prompt":"hi"}'
curl -s http://127.0.0.1:11434/v1/embeddings -d '{"model":"nomic-embed-text","input":"hi"}'
curl -s http://127.0.0.1:11434/v1/models
```

Two cost-saving facts about this surface:

- the **timings are not in the OpenAI body**; the `usage` object carries token
  counts only. If you need the durations, use the native `/api` route;
- `stream: true` here is Server-Sent Events (`data: {...}` lines ending
  `data: [DONE]`), not NDJSON. A reader written for `/api` will not parse it.

## The CLI, for the same operations

| task | API | CLI |
|---|---|---|
| list models | `GET /api/tags` | `ollama list` |
| loaded models | `GET /api/ps` | `ollama ps` |
| inspect | `POST /api/show` | `ollama show qwen2.5:7b` |
| pull | `POST /api/pull` | `ollama pull qwen2.5:7b` |
| delete | `DELETE /api/delete` | `ollama rm qwen2.5:7b` |
| one prompt | `POST /api/generate` | `ollama run qwen2.5:7b "hi"` |
| daemon | - | `ollama serve` |

`ollama run` starts an interactive session when given no prompt; it is not a
drop-in for a scripted call, and it prints the finished text without the timing
fields.

## Environment variables that change the daemon

These are read by the SERVER at start, not per request:

| variable | default | what it does |
|---|---|---|
| `OLLAMA_HOST` | `127.0.0.1:11434` | the bind address |
| `OLLAMA_MODELS` | `~/.ollama/models` | where the weights live |
| `OLLAMA_KEEP_ALIVE` | `5m` | default residency for a loaded model |
| `OLLAMA_NUM_PARALLEL` | 1 (auto) | concurrent requests one model serves; each gets its own KV cache slot |
| `OLLAMA_MAX_LOADED_MODELS` | 3 x GPUs (auto) | how many models may be resident at once |
| `OLLAMA_MAX_QUEUE` | 512 | queued requests before the daemon answers 503 |
| `OLLAMA_FLASH_ATTENTION` | off | a speed/memory trade at long context |
| `OLLAMA_KV_CACHE_TYPE` | `f16` | `q8_0` / `q4_0` shrink the KV cache |
| `OLLAMA_ORIGINS` | localhost only | CORS origins the daemon accepts from a browser |
| `OLLAMA_DEBUG` | off | verbose server logging |

Two settings interact in a way that ruins benchmarks: with
`OLLAMA_NUM_PARALLEL=2`, one model serves two requests at once, so two
"sequential" runs can overlap and both report a lower tok/s. Measure with
parallelism at its default, or state it in the result.

## What not to do

- Do not assume the daemon is running. Probe `/api/version` and say the address
  you tried.
- Do not buffer a streaming response and parse it as one JSON document.
- Do not invent an `options` key: an unknown one is ignored, and the numbers
  become a claim about a setting the daemon never applied.
- Do not quote `total_duration` as latency: it is the daemon's own span, and it
  excludes the transport.
- Do not treat a 404 for a model as "the daemon is broken": the fix is
  `ollama pull`, and the error body says so.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.