---
title: BrainAPI
emoji: π§
colorFrom: blue
colorTo: green
sdk: docker
app_port: 7860
pinned: false
---
π§ BrainAPI
Turn raw text into a living knowledge graph β automatically.
You send text. A swarm of agents reads it, takes notes, and draws the graph for you.
Then you ask questions and get answers backed by a traceable path β not a similarity guess.
BrainAPI Cloud β’
Quickstart β’
What it is β’
How it works β’
Use cases β’
Docs β
---
## π What is BrainAPI?
**BrainAPI reads your text and builds a knowledge graph out of it β by itself.**
You feed it documents, notes, messages or events. Behind the scenes a group of AI agents reads everything, understands what happened, writes down the facts, and connects them into a queryable graph. You then ask questions in plain language and get answers grounded in the actual connections it found.
The trick is that BrainAPI is **event-centric**. Instead of just storing "A is related to B," it captures _who did what, to whom, when, and in what context_. That's what lets it answer multi-step questions and always show you the path it used to get there.
Feed it one sentence:
```
"Emily organized the AI Ethics Meetup in London on March 8, 2024."
```
Ask something it was never explicitly told:
```python
result = client.retrieveContext("Who organized AI events in London in Q1 2024?")
# β "Emily organized the AI Ethics Meetup in London on 2024-03-08."
# β result.triples shows the full graph path used to derive this
```
That trace is the difference. Not a nearest-neighbour guess β a **reasoned, walkable path through a graph it built for you.**
> **[βΆ Watch the full demo β](https://youtu.be/ECOleTRjl64?si=fBUALoYvsiUl-BPC)**
---
## Authentication and durability
`SYSTEM_PAT` is the one administrator credential for HTTP, console, Agent API,
and MCP. A legacy `BRAINPAT_TOKEN` may temporarily alias it; different values
fail startup. Brain/workspace creation does not issue credentials. Administrators
create and revoke named scoped tokens through `/system/access-tokens`; plaintext
is shown once and only hashes are persisted. Existing Brain PATs remain scoped
compatibility credentials pending rotation.
The console uses `/auth/session`, `/auth/me`, and `/auth/logout`, with an opaque
HttpOnly/Secure/SameSite cookie and CSRF protection. It stores no submitted
access token. System login has `brain_id=null` until workspace selection.
`/health` reports durable or ephemeral storage. Without a persistent `/data`
mount, production startup requires the explicit staging/recovery override
`ALLOW_EPHEMERAL_STORAGE=true` and health remains degraded. This never means
production GO. See [credential migration and runtime gates](docs/security/credentials.md).
## π Quickstart (fastest way to run it)
The quickest way to get BrainAPI running is the **`brainapi` TUI** β a CLI that clones the project, sets up a Python environment, configures services, starts backing services, and launches everything for you. Run the interactive wizard, or pass flags to skip prompts (hybrid / non-interactive).
```sh
npm install -g brainapi-tui
brainapi init # clone, install deps, setup wizard (or pass flags to skip prompts)
brainapi start # backing services + API + MCP + worker + console
```
When it's up:
- **API** β `http://localhost:8000`
- **Console (web UI)** β `http://localhost:8000/console`
- **MCP server** β `http://localhost:8001/mcp`
Set `PUBLIC_BASE_URL` to the externally visible, scheme-qualified deployment URL
(without a trailing slash) when BrainAPI is served behind a proxy. BrainAPI uses it
to publish stable workspace API and Console URLs such as
`$PUBLIC_BASE_URL/brains/company/api` and
`$PUBLIC_BASE_URL/console/w/company/`. If it is unset, discovery responses use the
request base URL.
### Workspace API base
Every active workspace publishes a stable API base on the same BrainAPI
deployment:
```text
https://HOST/brains/aux-research/api
```
Authenticate with either `Authorization: Bearer $BRAIN_PAT` or the `BrainPAT`
header. The workspace path selects the brain, so `X-Brain-ID` is not required.
If a caller supplies `X-Brain-ID` or a query/body `brain_id` that conflicts with
the workspace path, BrainAPI returns `409 BRAIN_SCOPE_CONFLICT`. Existing clients
using legacy endpoints plus `X-Brain-ID` remain supported.
The system PAT may access every active workspace. A per-brain PAT may access only
the workspace backed by that brain; using it with another workspace returns
`403`. Unknown credentials return `401`, unknown workspace slugs return `404`,
and archived workspaces return `410`. These rules apply consistently to the
native API, Agent API, and workspace MCP endpoint.
Useful discovery URLs are:
```text
GET /brains/aux-research/api
GET /brains/aux-research/api/openapi.json
```
The workspace facade includes ingest, retrieval/data, graph model, task, and
brain-specific metadata routes. System management routes are never mirrored.
| Hosted route family | Methods | Scope/behavior |
| --- | --- | --- |
| `/ingest/`, `/ingest/structured`, `/ingest/file` | `POST` | Queues the existing ingestion workers with the resolved workspace `brain_id`. |
| `/retrieve/*` | `GET`, `POST` | Reuses the existing context, search, graph, observation, data, recommendation, and vector handlers. |
| `/model/entity`, `/model/relationship` | `POST`, `PUT` | Reuses the existing graph mutation handlers. |
| `/tasks/`, `/tasks/{task_id}` | `GET` | Reads only the task namespace for the resolved workspace; foreign task IDs return `404`. |
| `/meta/relationships-properties`, `/meta/entity-labels`, `/meta/entity-properties` | `GET` | Exposes brain-specific metadata only. `/meta/login-info` is intentionally excluded. |
All paths in this table are relative to
`/brains/{workspace_slug}/api`. Management (`/system/*`), health, demo, and
plugin routes are not implicitly exposed. The discovery endpoint describes the
active workspace, while its OpenAPI document contains only this allowlisted
facade.
### Using a Brain Workspace from an Agent
An external agent only needs a deployment URL, workspace slug, and existing
BrainPAT:
```ini
BRAINAPI_BASE_URL=https://HOST
BRAINAPI_WORKSPACE=aux-research
BRAINAPI_PAT=...
```
These values derive three stable surfaces:
```text
Native API: https://HOST/brains/aux-research/api
Agent API: https://HOST/brains/aux-research/agent
MCP: https://HOST/brains/aux-research/mcp
```
Retrieve compact agent context without knowing BrainAPI's internal route tree:
```bash
curl \
-H "Authorization: Bearer $BRAINAPI_PAT" \
-H "Content-Type: application/json" \
https://HOST/brains/aux-research/agent/context \
-d '{"query":"What do we know about X?"}'
```
Queue a memory write through the existing asynchronous ingestion pipeline:
```bash
curl \
-H "Authorization: Bearer $BRAINAPI_PAT" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: hermes-session-42-message-7" \
https://HOST/brains/aux-research/agent/memory \
-d '{"text":"The customer prefers annual billing.","metadata":{"source":"hermes","session_id":"42"}}'
```
Agent memory metadata is forwarded through the ingestion model's existing
`meta_keys` support. Arbitrary metadata is retained as source metadata where the
configured ingestion backend already supports it; no separate metadata store is
created. Specifically, standard text ingestion persists it on the source
`TextChunk`, copies it into the data-vector metadata, and attaches it to generated
observations. Stored chunks can be filtered through the existing
`/retrieve/text-chunks?metadata_eq=...` API. `Idempotency-Key` stabilizes the
returned task identifier, but it is not a distributed duplicate-content
guaranteeβretries after a successfully queued request may still process the
content again.
Agent memory writes are asynchronous: a successful request returns `202`,
`status: "queued"`, and the actual ingestion `task_id`. `Task-Identifier` and
`Idempotency-Key` use their value as that task identifier, so repeated requests
with the same key return the same identifier; they do not provide an exactly-once
persistence guarantee.
The agent schema is available at
`/brains/aux-research/agent/openapi.json`, and machine-readable capabilities at
`/brains/aux-research/agent/capabilities`.
BrainAPI's existing stateless Streamable HTTP MCP server remains one process on
the MCP port. The bundled reverse proxy maps the workspace-aware transport at
`https://HOST/brains/aux-research/mcp` to that server. The tools are
`memory_search`, `memory_context`, `memory_store`, `memory_observe`,
`memory_neighbors`, `memory_entity`, and `memory_task_status`; workspace scope is
derived from the URL and is not a tool argument. Direct development connections
without the reverse proxy use the MCP server port with the same path, for example
`http://localhost:8001/brains/aux-research/mcp`.
The workspace endpoint uses a semantic-only MCP registry. Legacy raw MCP tools
remain available at `/mcp` for backward compatibility, but they are not listed
or callable through `/brains/{workspace_slug}/mcp`. Plugins likewise appear in
the workspace registry only when they explicitly use the workspace-safe MCP
registration contract.
Log in over HTTPS with the operator-configured `SYSTEM_PAT` or an explicitly issued workspace access token. That's it β you have a working brain.
### TUI commands
| Command | What it does |
| ----------------- | ------------------------------------------------------------------------------------------- |
| `brainapi init` | Clone into `~/.brainapi/source/`, create a venv, run setup (flags skip individual prompts) |
| `brainapi start` | Bring up backing services and run the API, MCP server, and Celery worker (Ctrl-C stops all) |
| `brainapi config` | Re-open the setup wizard to change one area (databases, models, pipeline, plugins, β¦) |
| `brainapi doctor` | Check Python, Docker, Ollama, cloud credentials, and configured services |
| `brainapi update` | `git pull` the install and reinstall Python dependencies |
Running any command (except `help`) before `init` will run `init` first automatically.
**Useful `brainapi init` flags:** `--defaults`, `--vector-db`, `--models-mode`, provider keys/models, `--brainpat-token`, `--no-plugins`, `--no-start-services` (any passed value skips that wizard prompt). **`brainapi start` flags:** `--pipeline accurate|lightweight`, `--no-services`, `--no-api` / `--no-mcp` / `--no-worker`, `--only api,mcp,worker`. Full reference β [`tui/README.md`](tui/README.md).
> The TUI installs into `~/.brainapi/` (source, venv, `.env`, and `state.json`) β separate from any git clone. Override with `BRAINAPI_HOME`, `BRAINAPI_REPO_URL`, or `BRAINAPI_BRANCH`.
---
## π οΈ Run it locally (dev mode)
If you're contributing or hacking on BrainAPI, run it straight from the repo with `make`:
```sh
git clone https://github.com/lumen-labs/brainapi2.git && cd brainapi2
poetry install
make start-all # backing services + API (:8000) + MCP (:8001) + Celery worker
```
`make start-all` runs everything from source with hot reload enabled, so code changes are picked up live.
**Console in dev mode:**
```sh
make build-console && make start-api # serve console at http://localhost:8000/console
# β or hot-reload the console UI separately β
make start-api # terminal 1
make start-console-dev # terminal 2 β http://localhost:5173/console/
```
**Other handy targets:** `make start-api`, `make start-mcp`, `make stop-all`, and `make install-extras` (auto-installs heavy ML extras like local OCR based on your `.env`).
**Docker only** (pick a [deploy profile](deploy/README.md)):
```sh
# light β Postgres + DeepSeek + OpenAI embeddings
cp deploy/env.light.example .env # fill in secrets, then:
docker compose -f deploy/docker-compose.light.yaml up -d
# heavy β Neo4j + Milvus + Mongo + GCP/Azure (full resources)
cp deploy/env.heavy.example .env
docker compose -f deploy/docker-compose.heavy.yaml up -d
```
Then ingest your first data point:
```sh
curl -X POST http://localhost:8000/ingest/ \
-H "Authorization: Bearer $BRAINPAT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"data": {"data_type": "text", "text_data": "Emily organized the AI Ethics Meetup in London on 2024-03-08."}, "brain_id": "default"}'
```
---
## π₯οΈ The Console
The Console is a web UI for exploring everything inside your brain: the **graph**, **text chunks**, **vectors**, **observations**, **tasks**, and an **ingest** page to push new data. It's served on the same port as the API at `http://localhost:8000/console` and you exchange an administrator or workspace access token for an opaque browser session over HTTPS.
> Set `CONSOLE_ENABLED=false` to disable static serving in environments where you don't want the UI exposed.
---
## βοΈ How It Works: The Agent Swarm
Every piece of text you send is read and discussed by a **swarm of specialized agents**. After they understand it, they write down notes and draw the graph. Each agent has one narrow job, which keeps the pipeline reliable and auditable end to end.
```mermaid
flowchart TD
Text["Raw text"] --> Scout["π Scout
extract ALL entities"]
Text --> Observer["π Observations Agent
write notes about facts
using prior context"]
Scout -->|entities| Architect["ποΈ Architect
build event-centric
relationships"]
Architect -->|relationships + entities| Janitor["π§Ή Janitor
verify correctness
+ check for duplicates"]
Janitor -->|"errors + instructions"| Architect
Janitor -->|"OK"| Graph["π Knowledge Graph
(append-only)"]
Observer --> Graph
```
1. **π Scout** reads the text and extracts **all** the entities it contains β people, things, events, quantities.
2. **π Observations Agent** runs alongside and writes down notes about the facts in the text, taking the previously known context into account.
3. **ποΈ Architect** takes the Scout's entities, re-reads the text, and creates the connections between them as **event-centric relationships** β `subject βrelβ event βrelβ object`. Actions become first-class event hubs rather than flat edges.
4. **π§Ή Janitor** checks the Architect's work: are the relationships correct, and do similar nodes (the same entity) or triplets (the same fact) already exist in the graph? If something is wrong, the Janitor tells the Architect exactly what to fix and expects a corrected output, then checks again β looping until it's clean.
### The Triangle of Attribution
Every action is modeled as a central **Event Hub** connecting three points, which is what makes multi-hop, traceable answers possible:
```
βββββββββββββββββββ
β EVENT HUB β
β (Action Node) β
ββββββββββ¬βββββββββ
β
βββββββββββββββββββΌββββββββββββββββββ
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β ACTOR β β TARGET β β CONTEXT β
β (Source) β β (Recipient) β β (Anchor) β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
:MADE :TARGETED :OCCURRED_WITHIN
```
So `"Emily organized the AI Ethics Meetup in London"` becomes:
```
(Emily)-[:MADE]->(Organizing Event)-[:TARGETED]->(AI Ethics Meetup)
\-[:OCCURRED_WITHIN]->(London)
```
### Append-only by design
BrainAPI follows an **append-only philosophy**: nothing is deleted from the graph. Every new action is appended as its own event node rather than overwriting what was already there. This keeps the full history intact and lets you **reconstruct how facts evolved over time** β events are never merged away, so the provenance trail always survives.
### Two pipeline modes
- **`accurate`** (default) β the full swarm runs: Scout extracts every entity, the Observations agent writes notes, and the Janitor validates and deduplicates the Architect's relationships.
- **`lightweight`** β a faster, cheaper path that extracts only the most important entities and skips the heavier validation steps. Switch with `--pipeline lightweight` or `PIPELINE_MODE` in `.env`.
Search (`GET|POST /retrieve/search`) is **off by default**. Set `SEARCH_ENABLED=true` to register the hit-list API. BM25 requires `DATA_DB=postgresql`; dense-only search (`SEARCH_USE_BM25=false`) is supported by either production profile. PostgreSQL writes BM25 `tsvector` indexes and uses halfvec HNSW when embeddings are >2000-d. Default light-profile retrieval is both channels fused (`SEARCH_FUSION=rrf`). The search p50 SLO is **< 200 ms excluding `embed.query`** β ask for `profile_stages` to split embed RTT from retrieve. `/retrieve/context` stays dense βͺ ILIKE unless search is on and `CONTEXT_PASSAGE_MODE` is `hybrid` / `bm25` / `dense`.
---
## π Retrieving Knowledge
Once your data is in, BrainAPI exposes purpose-built retrieval endpoints (REST, on port `8000`):
| Endpoint | Method | What you get |
| ---------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `/retrieve/context` | `POST` | **Relevant information for a piece of text** β one-shot graph triples, passages, historical context, and optional provenance; p50 target < 1000 ms |
| `/retrieve/search` | `GET`/`POST` | **Ranked search hits** (opt-in: `SEARCH_ENABLED=true`) β passages (BM25 βͺ dense) by default; optional `entities` / `events` / `communities` channels and `expand=neighbors`; p50 < 200 ms excluding embed |
| `/retrieve/entity/status` | `GET` | **Existence check** for a specific entity β returns whether it exists, its node, relationships, and observations |
| `/retrieve/entity/synergies` | `GET` | **Sibling entities (Preview)** β shared event hubs + embedding similarity (`top_k`, `labels`, `polarity`); quality thresholds are not yet release-stable |
| `/retrieve/recommend` | `GET`/`POST` | **Train-free recommendations (Preview)** β synergies plus asymmetric walks, multi-interest, and optional attribute preferences; endpoint compatibility is retained while the quality gate is defined |
**Get context for a question:**
```sh
curl -X POST http://localhost:8000/retrieve/context \
-H "Authorization: Bearer $BRAINPAT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Who organized AI events in London in March 2024?", "brain_id": "default"}'
# β text_context + triples + historical_context + source_passages
# (optional: graph_session_ids, topics, paths; tune with max_facts / max_passages)
```
**Check if an entity exists:**
```sh
curl "http://localhost:8000/retrieve/entity/status?target=Emily" \
-H "Authorization: Bearer $BRAINPAT_TOKEN"
# β { "exists": true, "node": {...}, "relationships": [...], "observations": [...] }
```
**Find synergies (same-type siblings, Preview):**
```sh
curl "http://localhost:8000/retrieve/entity/synergies?target=Neural%20Networks%20101&top_k=20" \
-H "Authorization: Bearer $BRAINPAT_TOKEN"
# β similar entities ranked by association_score
```
**Rank recommendations (product / next-item style):**
```sh
curl "http://localhost:8000/retrieve/recommend?target=u01&top_k=20&labels=PRODUCT&exclude_seen=true" \
-H "Authorization: Bearer $BRAINPAT_TOKEN"
# β ranked recommendations with channel provenance (synergy / asymmetric / multi_interest / β¦)
```
These endpoints cover the most common needs, but there are more (`/retrieve/hops`, `/retrieve/entities/neighbors`, `/retrieve/text-chunks`, β¦). Start with [Context retrieval](https://brainapi.lumen-labs.ai/docs/v2/retrieval/context) and the [docs hub](https://brainapi.lumen-labs.ai/docs/v2).
---
## π§© Plugins: Make BrainAPI Fit Your Use Case
Out of the box BrainAPI is a general-purpose understanding engine. **Plugins** let you reshape it into something far more specific β without forking the core. Because plugins can add routes, swap agent prompts, and register MCP tools, the same engine can become:
- **A cheap semantic search engine** β extract entities once, then serve fast graph-backed search instead of paying for vector queries every time.
- **A recommendation system** β use `/retrieve/recommend` (and `/retrieve/entity/synergies` for same-type siblings) or plugins like `features-rec` / `recsys-gnn`.
- **A domain-specific extractor** β override the Scout/Architect prompts so the swarm understands your ontology (CRM, legal, medical, β¦).
- **An agent toolset** β register custom MCP tools so any LLM runtime can read and write your brain.
A plugin can:
- Add custom REST routes to the BrainAPI server
- Override or extend the agents' prompts for domain-specific extraction
- Register new MCP tools for agent runtimes
- Ship its own background (Celery) tasks
### Install & publish
```sh
poetry run brainapi plugins install @community/crm-entities
poetry run brainapi plugins list --remote
poetry run brainapi plugins register # become a publisher
poetry run brainapi plugins publish ./my-plugin.tar.gz
```
Browse and publish at the [Plugin Registry β](https://registry.brain-api.dev/app)
---
## π Where You Can Use It
BrainAPI shines wherever messy, ever-changing text needs to become structured, queryable understanding:
- **AI memory for agents** β persistent, structured memory across sessions and documents, with provenance.
- **Recommendation systems** β grounded in real behavioural paths, not click co-occurrence.
- **Semantic search** β retrieve by meaning and multi-hop reasoning across docs, tickets and chat.
- **Expert & community mapping** β "Who in our org has worked on Kubernetes and ML pipelines together?"
- **Business intelligence from qualitative data** β turn feedback and support conversations into queryable signals.
- **Investigation & compliance** β connect people, events, documents and timestamps into a coherent, traceable graph.
β
**Reach for BrainAPI when** you need structured knowledge from messy sources, long-term explainable memory, or multi-hop queries with a traceable answer.
β οΈ **Skip it when** your data already fits a fixed schema you control, or a single SQL query solves the problem.
---
## π Connecting Claude Desktop via MCP
The MCP server runs as a separate process on port **8001** (`http://localhost:8001/mcp`).
1. Start it: `make start-mcp` (or it's already running via `brainapi start`).
2. In Claude Desktop β **Settings β Developer β Edit Config**, add:
```json
"brainapi-local": {
"command": "/path/to/node/bin/npx",
"args": ["-y", "@pyroprompts/mcp-stdio-to-streamable-http-adapter"],
"env": {
"URI": "http://localhost:8001/mcp",
"MCP_NAME": "brainapi-local",
"PATH": "/path/to/node/bin:/usr/local/bin:/usr/bin:/bin",
"BEARER_TOKEN": "your-pat-here"
}
}
```
3. Make sure URL-based MCP servers are enabled in Claude Desktop settings.
---
## π¦ SDKs
| Platform | Package | Status |
| ----------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **Python** | [`lumen_brain`](https://pypi.org/project/lumen_brain/) |  β οΈ Pre-release |
| **Node.js** | [`lumen-brain`](https://www.npmjs.com/package/@lumenlabs/lumen-brain) |  β οΈ Pre-release |
```python
from lumen_brain import BrainAPI
client = BrainAPI(api_key="your-key")
client.ingest.text("Emily organized the AI Ethics Meetup in London on 2024-03-08.")
result = client.retrieveContext("Who organized AI events in London in March 2024?")
print(result.text_context) # the answer
print(result.triples) # the graph path used to derive it
```
> Both SDKs are pre-1.0 and under active development. For production, use the [REST API](https://brainapi.lumen-labs.ai/docs/v2) directly until v1.0 ships. You can mix modes freely β ingest over REST, retrieve via MCP inside an agent runtime, or use the SDKs for everything.
---
βΆ Watch: BrainAPI (non-technical) Overview Video
---
## π Resources
| Resource | Link |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| π₯οΈ Local CLI (TUI) | [`tui/README.md`](tui/README.md) β `npm install -g brainapi-tui` |
| π Documentation | [brainapi.lumen-labs.ai/docs/v2](https://brainapi.lumen-labs.ai/docs/v2) |
| β‘ Quick Start Guide | [brainapi.lumen-labs.ai/docs/v2](https://brainapi.lumen-labs.ai/docs/v2) |
| π Plugin Registry | [registry.brain-api.dev/app](https://registry.brain-api.dev/app) |
| π οΈ Context API | [brainapi.lumen-labs.ai/docs/v2/retrieval/context](https://brainapi.lumen-labs.ai/docs/v2/retrieval/context) |
| π Python SDK (PyPI) | [pypi.org/project/lumen_brain](https://pypi.org/project/lumen_brain/) |
| π¦ Node.js SDK (npm) | [npmjs.com/package/@lumenlabs/lumen-brain](https://www.npmjs.com/package/@lumenlabs/lumen-brain) |
| π¬ Community & Support | [Discord](https://discord.gg/VTngQTaeDf) |
---
## π€ Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
---
## π License
This project is licensed under the **[Business Source License 1.1](LICENSE)**.
- **Free for non-production use** β copy, modify, redistribute, and run BrainAPI for development, evaluation, and other non-production purposes.
- **Production use requires a commercial license** β including self-host, sidecar, and running BrainAPI on your own cloud as part of a live product or service.
- **This version becomes Apache 2.0 on 13 August 2030.** After that date, this release is under the [Apache License, Version 2.0](licenses/Apache-2.0.txt) and production use of *this* version is free. Later releases keep their own BSL terms until their Change Date.
Contact [hello@lumen-labs.ai](mailto:hello@lumen-labs.ai) for a commercial license.
---
Built with β€οΈ by Lumen Labs
### Hugging Face CPU embeddings
The default CPU image uses `embeddinggemma-2:270m` through Ollama, with validated
768-dimensional native vectors truncated and normalized to 512 for pgvector.
See [the CPU deployment contract and qualification steps](deploy/README.ollama.md).