Injection Modes & Budget
The `full` and `metadata` modes, the 20,000-character budget, which endpoints it runs on, and why `allowed-tools` is not enforced.
Matching skills are written as a single `<skills>` block at the END of the system message. System prompts go first (admin policy first), and the procedure packs come AFTER the policy and after the user's own system text.
<skills>
## incident-postmortem
Writes a blameless postmortem from an incident timeline. Use when an incident is resolved and a written record is needed.
# Incident postmortem
...body...
## commit-message
Writes a commit message ... # metadata mode: no body
</skills>If there is no system message at all, the block is inserted at the front as one. If the system content is not text (multimodal), its parts are left alone — the block goes in as a separate system message immediately AFTER it. On the `/api/generate` path it is appended to Ollama's native `system` field.
#The two modes
| Mode | What is injected | When to use it |
|---|---|---|
| `full` (default) | `## name` + description + the WHOLE body, on EVERY matching request. | The real working mode. If you want the model to actually follow the procedure, this is it. |
| `metadata` | ONLY `## name` + the description. The body is never sent. | Cheap discovery: the model sees that the skill exists. Do not use it before reading the limit below. |
#The budget: 20,000 characters
There is a hard ceiling on the total characters of the injected block: **20,000**. What is counted is NOT the raw body but the section actually written into the block — `## name` + description + (only in `full` mode) the trimmed body. This is why a `metadata` binding also takes budget: back when only bodies were counted, N metadata bindings could grow without limit.
- 1The bindings are already in priority order; the budget is spent in that order.
- 2A skill that does not fit is dropped WHOLE. A half body is NEVER injected — a procedure cut off at step 3 shows the model a world where step 4 does not exist; half a procedure is the wrong procedure.
- 3After a dropped skill is skipped, the scan CONTINUES: a big skill does not evict the small, lower-priority one behind it.
- 4The dropped skill's name is written to the server log with a `logger.warning` — and the panel shows it BEFORE you even save (see below).
The panel does this arithmetic for you: every skill shows its own character cost, the Marketplace compares the catalog total against the limit, the Installed tab gives, for EACH target, at least what one request hitting that target would carry (`global` bindings match every request, so they are counted into every target's total), and the binding editor recomputes the budget live as you change the mode or the priority.
#Where it runs, and where it does not
Skill injection runs ONLY on the text-generation endpoints: `/v1/chat/completions`, `/api/chat` and `/api/generate`. Other endpoints (embeddings, model listings, …) never reach this step. The step sits immediately AFTER the system-prompt injection and carries the same endpoint list.
The gateway's own self-calls receive NO skills
The gateway also calls models internally. All four of those internal calls are EXEMPT from skill injection: every turn of the MCP agent loop, the judge's own call, the Second Brain distiller, and the content-routing classifier. The reason differs per case: the body entering the loop and the judge has already been through this step (applying it again would duplicate the `<skills>` block on every turn); and the distiller and the classifier are not user requests — writing a 20,000-character procedure pack into a call that just asks for one category name is not merely wasteful, it risks breaking the ROUTING itself.
Injection never drops the request
Every error in this step (the DB could not be read, a condition could not be evaluated, a trigger pattern is broken) is written with a `logger.warning` and the injection is SKIPPED; the request continues upstream unchanged. This is exactly the same behaviour as the system-prompt seam. The practical consequence: when a skill was never injected there is NO client-side error to see — the panel's budget indicators and the server log are the places to look.
When a change takes effect
Active bindings are cached under a single Redis key, and the cache is invalidated on every admin change (editing a skill, installing, deleting, adding or changing a binding) — the next request reads fresh. Without Redis it reads the database directly; the behaviour is identical, there is simply no cache.
#`allowed-tools`: stored, not enforced
The same holds for `license`, `compatibility` and `metadata`: they are documentation fields, not behaviour. The only text injected is `## name`, the description and (depending on the mode) the body.