# Completions

A standalone chat completion endpoint for independent LLM calls. No agent config, no session, no tool-execution loop — just a direct call to the configured LLM provider.

Intended for apps built on Veil that need raw AI completions without setting up an agent workflow.

> **Calling from inside a custom tool?** Prefer the in-process helper instead of HTTP self-call — see [guide/06-tools.md → Calling the LLM from a custom tool](../guide/06-tools.md#calling-the-llm-from-a-custom-tool). Same provider chain, no extra hop, no auth header.

---

## POST /completions

**Request body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `messages` | array | ✅ | OpenAI-compatible messages array |
| `model` | string | — | Model ID to use. Overrides the default model from settings |
| `temperature` | number | — | Sampling temperature |
| `max_tokens` | number | — | Maximum output tokens |
| `tools` | array | — | OpenAI-compatible tool definitions |
| `reasoning` | object | — | Engine-blind reasoning config: `{ effort, max_tokens? }`. See [05-sessions.md](05-sessions.md) "Reasoning unification" for cross-engine semantics. |
| `sse` | boolean | — | Stream the response via SSE (default: `false`) |

The `base_url` and `api_key` are resolved through the provider chain via `callWithProviderFallback()`. The endpoint uses the resolved provider's credentials based on the configured `providers` and `routing` rules in settings. They cannot be overridden per-request.

**Standard JSON response**

```json
{
  "message": {
    "role": "assistant",
    "content": "The capital of France is Paris.",
    "tool_calls": []
  },
  "usage": {
    "input": 18,
    "output": 9,
    "cache": 0
  },
  "cost": 0.0000000432,
  "finish_reason": "stop"
}
```

**Response fields**

| Field | Type | Description |
|-------|------|-------------|
| `message.content` | string\|null | Assistant reply text |
| `message.tool_calls` | array | Tool calls requested by the model (may be empty) |
| `usage.input` | integer | Prompt tokens consumed |
| `usage.output` | integer | Completion tokens generated |
| `usage.cache` | integer | Cached prompt tokens (subset of `input`) |
| `cost` | number | Estimated USD cost. Uses the value returned by the API if present, otherwise calculated from `config/models.json` pricing |
| `finish_reason` | string | Why the model stopped (`stop`, `tool_calls`, `length`, etc.) |

**Example**

```bash
curl -X POST http://localhost:5050/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "messages": [
      { "role": "user", "content": "What is the capital of France?" }
    ]
  }'
```

With an explicit model override:

```bash
curl -X POST http://localhost:5050/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "google/gemini-2.5-flash",
    "messages": [
      { "role": "system", "content": "You are a concise assistant." },
      { "role": "user",   "content": "Summarise the water cycle in one sentence." }
    ],
    "temperature": 0.3,
    "max_tokens": 120
  }'
```

---

## SSE streaming

Set `"sse": true` in the request body to receive a streamed response.

The connection emits three event types:

| Event | Payload | Description |
|-------|---------|-------------|
| `chunk` | `{ "content": "..." }` | Incremental text delta |
| `done` | Full response object (same as JSON mode) | Final message, usage, and cost |
| `error` | `{ "error": "...", "code": "..." }` | Emitted if the LLM call fails |

**Example**

```bash
curl -X POST http://localhost:5050/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "messages": [{ "role": "user", "content": "Tell me a short story." }],
    "sse": true
  }'
```

**Sample stream output**

```
event: chunk
data: {"content":"Once"}

event: chunk
data: {"content":" upon"}

event: chunk
data: {"content":" a time..."}

event: done
data: {"message":{"role":"assistant","content":"Once upon a time...","tool_calls":[]},"usage":{"input":12,"output":5,"cache":0},"cost":0.0000000075,"finish_reason":"stop"}
```

---

## Error responses

| Code | Condition |
|------|-----------|
| `400 VALIDATION_ERROR` | `messages` is missing or not an array |
| `400 VALIDATION_ERROR` | No model specified and no default model configured in settings |
| `500 INTERNAL_ERROR` | LLM provider returned an error |
