---
title: AI
description: An optional language model, Claude or one on your own machine, for chat, drafting and agents.
section: Server
order: 8
---

# AI

<p class="lead">AI is optional and off until a superuser configures a model. Every feature that uses it works without one, and model output is always a suggestion that a person accepts or rejects.</p>

## Setting one up

In the admin UI, under **AI**:

| | |
|---|---|
| Provider | `anthropic` for Claude, or `openai` for any OpenAI-compatible API: Ollama, LM Studio, llama.cpp server |
| Model | Defaults to `claude-opus-5` for Claude. Required for `openai` |
| Address | For `openai`, the base URL, e.g. `http://localhost:11434/v1` for Ollama |
| Key | Stored on the server; never returned by the API |
| Effort | Claude's reasoning effort: `low`, `medium` (default) or `high` |
| Per person, per day | Request quota per user; `0` for unlimited. Admins aren't counted |

With a local model, no data leaves your machine.

## In the browser

```ts title="app.ts"
const { text } = await sluurp.ai.chat([{ role: "user", text: "Summarise this week's notices" }]);

// Streaming:
await sluurp.ai.stream(turns, { onText: (piece, all) => (out.textContent = all) });
```

Both accept `{ system, signal }`; aborting via the signal keeps what arrived so far. When a user hits their daily quota, the response says so instead of throwing.

## In server code

`ai(system, prompt, schema)` returns JSON matching the schema, or `null` when no model is configured:

```ts title="functions/tag-notice.ts"
const tags = await ai(
  "You tag school notices.",
  notice.body,
  { type: "object", properties: { tags: { type: "array", items: { type: "string" } } } },
);
if (tags) await collection("notices").update(notice.id, tags);
```

For multi-turn tool use, see [Agents](/docs/agents).

## Where Sluurp uses it

[Pages](/docs/pages) shows its AI tools, and `sluurp/ai` provides an assistant for any app, only when a model is configured.
