MeridiansMeridians

Adding work — the rule

Source path: knowledge-base/knowledge/conventions/ai-models/06-adding-work-the-rule.md

# Adding work — the rule

A new model call site first picks its **system** ([two systems](00-two-systems.md)): a typed decision goes
through `callDecide(state, questions, caller, decisionModelFor('<category>'))`; text goes through
`callGenerate(..., modelFor('<category>'))`. Never both in one call.

Either way it **must** pick a category. If none of
the twelve fit, add a category to `AI_CATEGORIES` (+ `CATEGORY_META`, + a tier in each profile's
allocation) rather than reaching for a raw tier constant or a transport default. There is no default
LLM model: an uncategorised call is uncategorised thinking, and the call boundary rejects it.

Orthogonality is the guardrail: a category names the work, while the active profile assigns its capability.
Do not create a catch-all category to make a call fit; either use the existing semantic category or add the
missing modality and allocate it deliberately.

`callGenerate*` terminates server-side at `executeGenerate` / `executeGenerateText`; HTTP and headless
adapters that already hold an explicit provider key reuse that service rather than rebuilding a provider
request. `callDecide` terminates the same way at `executeDecide` (`decide-service.ts`) over
`openrouterDecide`, the only file that may hold the decisions endpoint. Pilot is the only request-shape exception because its assistant → tool → result loop cannot be
represented as one prompt. It uses the shared `openrouterChat` transport, whose pre-dispatch context guard
also protects every standard generation call. The raw chat-completions endpoint belongs only there.

Jev does not read decision question keys semantically. Every question's instructions must name its own
subject; the validator rejects duplicate instructions within one request. Alice evidence: identical
instructions produced `0.40 ± 0.01` across all 16 pairs, while named subjects produced `0.02–0.56`.

## Image context is fitted, not capped

Image generation admits context up to the route's transport ceiling (`AI_IMAGE_PROMPT_MAX_CHARS ===
AI_JSON_BODY_MAX_BYTES`, 8 MB) rather than the ordinary `AI_PROMPT_MAX_CHARS`. This is a decision, not an
omission: `compactImageGenerationContext` owns fitting — it chunks, compacts hierarchically, and always
formulates one bounded visual prompt — so the image provider never sees caller-truncated continuity. The
trade is that spend scales with admitted context: every compaction chunk is a text-model call (bounded per
request by `MAX_IMAGE_COMPACTION_ROUNDS` × chunk count, not by a fixed budget), and each one is logged
through `logApiCall` into the API log and the spend lanes. Cost is therefore *visible* rather than *capped*
here; the owner-only route admission and the spend kill-switch remain the ceilings. Do not reintroduce a
character cap for symmetry — if spend needs bounding, bound compaction calls per request, not context.
Open on GitHub

Raw Markdown source