# Model addressing

> The model field is an address: prefixes pick who answers (Synapse, a full model, a local model, one of your agents) and +flags add or remove capabilities.

Every AI client lets you change one thing: the model name. Subsidia uses that single field as an **address**. The text before the first `+` selects which capability serves the call; each `+flag` after it adds or removes something. Your agents, your knowledge base and local-only processing therefore become reachable from tools that know nothing about Subsidia: Cursor, Continue, n8n, LangChain, Claude Code, or a script you wrote against the OpenAI SDK.

The address is parsed identically on `POST /v1/chat/completions` and `POST /v1/messages` (see [Gateway](https://dev.subsidia.protypa.fr/docs/gateway.md)), and the original string is recorded verbatim in the [proof certificate](https://dev.subsidia.protypa.fr/docs/proofs.md).

> **INFO: The red line: no prefix, no flag, no change**
> A model name that carries no Subsidia prefix and no flag (`gpt-4o`, `claude-sonnet-4-6`, anything) is a **faithful passthrough**: the call is still masked, routed, metered and signed, but nothing is added to your prompt and no agent or knowledge base is involved. Capabilities are opt-in through the address, never imposed. An empty or missing `model` is read as `pulse-auto`.

## Grammar

```
<base>[+flag][+flag]...

base  = pulse-auto | pulse-agent | pulse-sensitive | agent:<slug> | debate:<slug> | any other string
flag  = cortex | sensitive | agent | nocortex | noknowledge | nomemory
```

Parsing rules, all taken from the parser:

- The string is split on `+`. The first part is the base, the rest are flags.
- Whitespace around each part is trimmed. **Flags are case-insensitive** (`+Cortex` works); the **base is case-sensitive** (`Pulse-Auto` is just an unknown name, so a passthrough).
- **Flag order does not matter** and flags compose freely.
- An **unknown flag is silently ignored**, with no error. Check spelling: `pulse-auto+cortx` is a plain `pulse-auto` call.
- A base that starts with `agent:` or `debate:` is followed by a slug. The slug is normalised the same way agent names are: accents removed, lowercased, every run of non-alphanumeric characters turned into one dash, edge dashes trimmed. `agent:Demo CFO` and `agent:demo-cfo` are the same address.

## Prefixes

| Base | Who answers | Notes |
| --- | --- | --- |
| `pulse-auto` | Synapse routes the call by task complexity and sensitivity. | The default, and what any unknown or missing model name behaves like. See [Synapse](https://dev.subsidia.protypa.fr/docs/synapse.md). |
| `pulse-agent` | The configured full-quality model, always. | Marks the call as agentic so it is never handed to the small model. For harnesses and tool loops. The Gateway also detects agentic traffic on its own (see below). |
| `pulse-sensitive` | Local processing, always. | Same as `pulse-auto+sensitive`. Where a local model exists the request never leaves the machine and the certificate shows the provider. See the caveat in [Sensitive](#flag-sensitive). |
| `agent:<slug>` | One of your workspace agents. | Its prompt, memory, documents and tools. Unknown slug is a 404 `model_not_found`. See [Agents](https://dev.subsidia.protypa.fr/docs/agents.md). |
| `debate:<slug>` | Reserved. | Parsed but not served: `501 address_not_supported` (OpenAI dialect) or `501 invalid_request_error` (Anthropic dialect). Use the [Conversations API](https://dev.subsidia.protypa.fr/docs/conversations.md). |
| Any other name | Passthrough, routed like `pulse-auto`. | `gpt-4o`, `claude-sonnet-4-6`, `my-model`. Accepted, recorded in the certificate as `request.model`, and routed by Synapse regardless of the name. |

## Flags

| Flag | Applies to | Effect |
| --- | --- | --- |
| `+cortex` | Non-agent addresses | Retrieves the five most relevant passages of your knowledge base for the last user message, adds them to the prompt as cited context, and commits their hashes to the certificate (`grounding`). If nothing matches, the call proceeds ungrounded. |
| `+sensitive` | Non-agent addresses | Forces local routing. Equivalent to the `pulse-sensitive` base, and composable with any other base. |
| `+agent` | Non-agent addresses | Forces the full-quality model. Equivalent to the `pulse-agent` base. |
| `+nocortex` | `agent:` only | The agent answers from its persona alone, without searching the knowledge base. Recorded in the certificate as `address.knowledge: false` (OpenAI dialect). |
| `+noknowledge` | `agent:` only | Alias of `+nocortex`. |
| `+nomemory` | `agent:` only | The agent neither recalls nor writes persistent memory. The call inherits nothing and leaves nothing behind. Recorded as `address.memory: false` (OpenAI dialect). |

The negative flags exist because an address that could only add capability gave callers running an isolated experiment no way to stop an agent from inheriting, and leaving, state across calls. `agent:cfo+nocortex+nomemory` is a clean-room run of the persona.

**Which flags are read where.** The parser accepts every flag on every address, but the Gateway only acts on the ones that make sense for the kind of call. `+nocortex`, `+noknowledge` and `+nomemory` are used by `agent:` calls only. For `+cortex`, an `agent:` call already grounds itself in the agent's own knowledge unless you add `+nocortex`, so the flag adds nothing there. For `+sensitive` on an `agent:` address, the Gateway does not pass the flag to the agent pipeline today; use `pulse-sensitive` when local processing is a hard requirement.

### Sensitive

> **WARNING: Local means local only where a local model exists**
> `pulse-sensitive` and `+sensitive` guarantee that the call stays on a local model on an installation that has one: a local or on-premise Subsidia, or a cloud deployment whose active provider is local. On a hosted cloud deployment without a local lane, the engine cannot route to a machine it does not have; the PII shield masks personal data before any external provider sees it, as for every call. Read `x-pulse-provider` or the certificate's `egress.provider` to see what actually answered. Details in [Synapse](https://dev.subsidia.protypa.fr/docs/synapse.md).

## Composition

| Address | Parsed as |
| --- | --- |
| `pulse-auto` | Synapse routing, nothing added. |
| `gpt-4o` | Passthrough, Synapse routing, nothing added. The name is only recorded. |
| `gpt-4o+cortex` | Passthrough plus knowledge grounding. |
| `pulse-auto+cortex` | Synapse routing plus knowledge grounding. |
| `pulse-agent+cortex` | Full model, always, grounded in your knowledge base. |
| `pulse-sensitive+cortex` | Local model, grounded. The passages never leave the machine either. |
| `pulse-auto+sensitive+cortex` | Same as the line above, in a different spelling. Flag order is irrelevant. |
| `agent:demo-cfo` | The agent, with its knowledge base and memory on. |
| `agent:demo-cfo+nocortex` | The agent, persona only. |
| `agent:demo-cfo+nomemory` | The agent, with knowledge, no memory read or written. |
| `agent:demo-cfo+nocortex+nomemory` | The agent as a stateless persona. |
| `agent:Demo CFO` | Same as `agent:demo-cfo` (slug normalised). |
| `debate:board` | Reserved. 501. |
| `pulse-auto+cortx` | Typo in a flag: ignored, so a plain `pulse-auto`. |

## Examples

The request body is identical in every case; only `model` changes.

**Ground a call in your knowledge base**

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/chat/completions \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "pulse-auto+cortex",
    "messages": [{"role": "user", "content": "What notice period does the Martin lease require?"}]
  }'
```

_TypeScript_

```typescript
import OpenAI from 'openai'

const client = new OpenAI({
  baseURL: 'https://api.subsidia.protypa.fr/v1',
  apiKey: process.env.SUBSIDIA_API_KEY,
})

const res = await client.chat.completions.create({
  model: 'pulse-auto+cortex',
  messages: [{ role: 'user', content: 'What notice period does the Martin lease require?' }],
})
// res.pulse.grounded_sources is the number of passages the certificate commits to
console.log(res.choices[0].message.content, (res as any).pulse)
```

_Python_

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.subsidia.protypa.fr/v1",
    api_key=os.environ["SUBSIDIA_API_KEY"],
)

res = client.chat.completions.create(
    model="pulse-auto+cortex",
    messages=[{"role": "user", "content": "What notice period does the Martin lease require?"}],
)
print(res.choices[0].message.content)
```

**Talk to an agent, then continue the same thread**

_curl_

```bash
# First turn: read the conversation id from the response headers
curl -i https://api.subsidia.protypa.fr/v1/chat/completions \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"agent:demo-cfo","messages":[{"role":"user","content":"Summarise our cash position."}]}'

# Second turn: send it back to continue the same persona-side conversation
curl https://api.subsidia.protypa.fr/v1/chat/completions \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "x-pulse-conversation: $CONVERSATION_ID" \
  -H "Content-Type: application/json" \
  -d '{"model":"agent:demo-cfo","messages":[{"role":"user","content":"And next quarter?"}]}'
```

_TypeScript_

```typescript
const base = 'https://api.subsidia.protypa.fr/v1/chat/completions'
const headers = {
  Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
  'Content-Type': 'application/json',
}

const first = await fetch(base, {
  method: 'POST',
  headers,
  body: JSON.stringify({ model: 'agent:demo-cfo', messages: [{ role: 'user', content: 'Summarise our cash position.' }] }),
})
const conversation = first.headers.get('x-pulse-conversation')!

const second = await fetch(base, {
  method: 'POST',
  headers: { ...headers, 'x-pulse-conversation': conversation },
  body: JSON.stringify({ model: 'agent:demo-cfo', messages: [{ role: 'user', content: 'And next quarter?' }] }),
})
console.log((await second.json()).choices[0].message.content)
```

_Python_

```python
import os, requests

url = "https://api.subsidia.protypa.fr/v1/chat/completions"
headers = {"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]}

first = requests.post(url, headers=headers, json={
    "model": "agent:demo-cfo",
    "messages": [{"role": "user", "content": "Summarise our cash position."}],
})
conversation = first.headers["x-pulse-conversation"]

second = requests.post(url, headers={**headers, "x-pulse-conversation": conversation}, json={
    "model": "agent:demo-cfo",
    "messages": [{"role": "user", "content": "And next quarter?"}],
})
print(second.json()["choices"][0]["message"]["content"])
```

**Other addresses, same call**

_Python_

```python
# Hard requirement: stay on a local model, grounded in the knowledge base
client.chat.completions.create(model="pulse-sensitive+cortex", messages=msgs)

# Always the full model, for an agent harness or a long tool loop
client.chat.completions.create(model="pulse-agent", messages=msgs)

# An agent with no memory and no knowledge base: a reproducible bench
client.chat.completions.create(model="agent:demo-cfo+nocortex+nomemory", messages=msgs)

# A name your tool insists on: still masked, routed, metered and signed
client.chat.completions.create(model="gpt-4o", messages=msgs)
```

_Claude Code_

```bash
export ANTHROPIC_BASE_URL="https://api.subsidia.protypa.fr"
export ANTHROPIC_AUTH_TOKEN="sk_live_..."
export ANTHROPIC_MODEL="pulse-agent+cortex"   # full model, grounded in your documents
claude
```

1. **Pick the base**

   Do you need a specific agent (`agent:<slug>`), a guaranteed-local call (`pulse-sensitive`), a guaranteed-full model (`pulse-agent`), or simply the sensible default (`pulse-auto`)?

2. **Add flags only for what you need**

   `+cortex` when the answer must come from your documents. `+nocortex` or `+nomemory` on an agent when you want an isolated, repeatable run.

3. **Check what happened**

   Read `x-pulse-provider` and the `pulse` block of the response, or fetch the [certificate](https://dev.subsidia.protypa.fr/docs/proofs.md): it records your raw address, the parsed kind, the provider that answered and the grounding hashes.

## Where the address shows up in a certificate

`payload.request.model` is the exact string you sent. `payload.address` records what was parsed:

- `address.kind`: `auto` or `agent`.
- `address.agent`: the agent slug for plain gateway calls. For `agent:` calls the certificate is signed by the agent pipeline itself, which records the agent identifier it holds internally rather than the slug, so match certificates to agents through the response's `pulse.agent` field or your own bookkeeping, not by string comparison with the slug.
- `address.cortex`: `true` when `+cortex` was on a non-agent call.
- `address.knowledge` / `address.memory`: `false` only when `+nocortex` / `+nomemory` switched them off on the OpenAI dialect, so an auditor can see that an answer ran without the knowledge base or without memory.
- `grounding`: one entry per passage used, with source name, chunk id, page and SHA-256 of the text.

The Anthropic dialect records `kind`, `agent` and `cortex` only.

## Discover addresses with GET /v1/models

### GET /v1/models

List the aliases and your workspace agents as addresses.

The listing is the discovery surface of the addressing scheme. It always contains `pulse-auto`, `pulse-sensitive` and `pulse-agent`, then the full model currently configured on the installation (for clients that insist on a real model id), then one `agent:<slug>` entry per agent visible to the key's workspace, oldest agent first. Because model pickers are filled from this route, your own agents appear in the dropdown of Cursor, Continue or n8n with no integration work.

The flags are not listed: they are suffixes you append to any entry.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `engine`

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/models \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/models', {
  headers: { Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY },
})
const { data } = await res.json()
const agents = data.filter((m: { id: string }) => m.id.startsWith('agent:'))
console.log(agents.map((m: { id: string }) => m.id))
```

_Python_

```python
import os, requests

data = requests.get(
    "https://api.subsidia.protypa.fr/v1/models",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
).json()["data"]

agents = [m["id"] for m in data if m["id"].startswith("agent:")]
print(agents)
```

#### Responses

**200**: An OpenAI-style list. Gateway aliases are owned by `pulse-gateway`, agents by `pulse-agents`.

```json
{
  "object": "list",
  "data": [
    { "id": "pulse-auto", "object": "model", "created": 1790000000, "owned_by": "pulse-gateway" },
    { "id": "pulse-sensitive", "object": "model", "created": 1790000000, "owned_by": "pulse-gateway" },
    { "id": "pulse-agent", "object": "model", "created": 1790000000, "owned_by": "pulse-gateway" },
    { "id": "gpt-4o", "object": "model", "created": 1790000000, "owned_by": "pulse-gateway" },
    { "id": "agent:demo-cfo", "object": "model", "created": 1790000000, "owned_by": "pulse-agents" }
  ]
}
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 401 |  | No key, wrong key, revoked or expired key. |
| 403 | `scope_denied` | The key does not carry the `engine` scope. |

#### Notes

The entry after the three aliases is whatever full model the installation is configured with; its id changes when an administrator changes the provider. Do not hard-code it.

## Errors specific to addressing

| Status | Code | Cause | What to do |
| --- | --- | --- | --- |
| 404 | `model_not_found` (OpenAI) or `not_found_error` (Anthropic) | No agent in the key's workspace has that slug. | Call `GET /v1/models` and copy the id. Renaming an agent changes its slug. |
| 501 | `address_not_supported` (OpenAI) or `invalid_request_error` (Anthropic) | A `debate:<slug>` address. | Use the [Conversations API](https://dev.subsidia.protypa.fr/docs/conversations.md). |
| 404 | `conversation_not_found` | The `x-pulse-conversation` header names a conversation that does not exist in this workspace. | Omit the header to start a new conversation. |

## Frequently asked

**My tool only lets me pick from a fixed list of model names. Can I still use Subsidia?**

If the list is editable or the tool fetches it from `GET /v1/models`, yes. If it is truly fixed, any real model name works as a passthrough (masked, routed, metered, signed), but you lose the capabilities that need an address: agents, `+cortex`, `pulse-sensitive`.

**Does an unknown model name cause an error?**

No. It is accepted, recorded in the certificate, and routed exactly like `pulse-auto`. The name does not choose the provider or the model, Synapse does. Only the `agent:` and `debate:` prefixes can fail.

**Can I combine pulse-sensitive with an agent?**

Not in one address: the base is either a `pulse-*` alias or `agent:<slug>`. `agent:<slug>+sensitive` parses, but the Gateway does not forward the flag to the agent pipeline today. If an agent must work on data that stays local, restrict the installation's providers instead (see [Synapse](https://dev.subsidia.protypa.fr/docs/synapse.md)).

**Why did +cortex not change my answer?**

Retrieval found no passage for the last user message, so the call ran ungrounded and the certificate has no `grounding`. The flag also requires a workspace on the key; and on `agent:` addresses it is redundant, because agents search their knowledge by default. See [Querying knowledge](https://dev.subsidia.protypa.fr/docs/knowledge-query.md) to test what your question retrieves.

**Is the address case-sensitive?**

The base is, the flags are not. Write `pulse-auto`, not `Pulse-Auto`. The agent slug is normalised, so `agent:Demo CFO` finds `demo-cfo`.

**Can I use the + syntax inside a SDK that rejects model names with a plus sign?**

A few UIs validate model ids against a pattern. Add the flagged name to the tool's custom models list, or fetch it from your own code. The plus sign is the only separator, there is no alternative spelling.
