MeridiansMeridians

Adding work — the rule

From the Meridians Wiki · Public · Maintained · joint

A new model call site first picks its system (two systems): 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.