omp-plugin-dynamic-system-prompt
v0.6.0
Published
Append a per-model tuning block to the omp system prompt, scaled by model family, version, and tier.
Maintainers
Readme
omp-plugin-dynamic-system-prompt
Scales system-prompt guidance to the model that will read it — less instruction for frontier models, more for the ones that need it.
An oh-my-pi plugin. It appends one ## Model tuning section to the system prompt on every turn, choosing the directives from the model actually in use.
Why
A prompt written for a weak model wastes tokens and credibility on a strong one. "Stop searching once you have the answer" is worth saying to a model that keeps searching; on a frontier reasoner it is noise that displaces something useful.
So the guidance is inverted from the obvious direction: capability buys silence. A frontier flagship gets a light prompt. An older or small-tier model in the same family gets more. An unrecognized model gets the heaviest budget, on the assumption that it needs the most help.
gpt-5.6 -> light (336 chars of 700)
claude-opus-4.5 -> medium (286 chars of 1600)
claude-haiku-4.5 -> heavy (286 chars of 3000)
glm-4.5-air -> heavy (298 chars of 3000)The block never names the model, its version, or the resolved profile. Nothing tells the model it is being profiled.
Install
omp plugin install omp-plugin-dynamic-system-promptIt works with no configuration. Every setting below has a default.
How it works
Three stages, one per module.
model id -> resolve axis -> select rules -> render block
(model-axis) (render) (render)1. Resolve the axis. The model id is normalized (bareModelId, lowercased, with suffixes like :thinking, :free, -high stripped) and classified into three independent dimensions:
| Dimension | Values |
| --------- | --------------------------------------------------------------------------------------- |
| family | openai anthropic gemini glm kimi deepseek grok qwen minimax unknown |
| version | numeric series — gpt-5.6 → 5.6, kimi-k2.7 → 2.7; null when unparseable |
| tier | flagship mid small |
Parsing is delegated to @oh-my-pi/pi-catalog/identity rather than reimplemented, so new model ids are recognized as the host catalog learns them.
2. Derive the weight. weight falls out of how far the version sits behind the family's frontier, plus a penalty for small-tier models. unknown always lands heavy.
| Distance from frontier | flagship / mid | small |
| ---------------------- | -------------- | -------- |
| current generation | light | medium |
| one generation back | medium | heavy |
| older, or unparseable | heavy | heavy |
3. Select and render. Rules whose applies(axis) holds and whose minWeight fits are sorted by priority, reduced to one rule per concern, then trimmed from the lowest priority end until the rendered block fits CHAR_BUDGET[weight] — 700, 1600, or 3000 characters.
When no rule applies, nothing is appended and the prompt is returned untouched. This is the normal outcome for an unrecognized model: it resolves heavy, but every rule gates on a known family, so the block is empty.
Worked examples
| model id | profile | weight | rules | block / budget |
| --------------------- | ---------------------- | ------ | ----- | -------------- |
| gpt-5.6 | openai-5.6-flagship | light | 2 | 336 / 700 |
| gpt-5.5 | openai-5.5-flagship | light | 1 | 175 / 700 |
| claude-opus-5 | anthropic-5-flagship | light | 3 | 498 / 700 |
| claude-sonnet-5 | anthropic-5-mid | light | 2 | 337 / 700 |
| kimi-k3 | kimi-3-flagship | light | 4 | 692 / 700 |
| grok-4.5 | grok-4.5-flagship | light | 4 | 653 / 700 |
| gemini-3-pro | gemini-3-flagship | medium | 1 | 206 / 1600 |
| claude-opus-4.5 | anthropic-4.5-flagship | medium | 2 | 286 / 1600 |
| claude-haiku-4.5 | anthropic-4.5-small | heavy | 2 | 286 / 3000 |
| deepseek-v4-pro | deepseek-4-flagship | light | 2 | 341 / 700 |
| deepseek-v4-flash | deepseek-4-small | medium | 5 | 786 / 1600 |
| glm-4.5-air | glm-4.5-mid | heavy | 2 | 298 / 3000 |
| grok-3-mini | grok-3-small | heavy | 1 | 184 / 3000 |
| claude-opus-6 | anthropic-6-flagship | light | 0 | 0 / 700 |
| some-unlisted-model | unknown-flagship | heavy | 0 | 0 / 3000 |
What glm-4.5-air actually receives:
## Model tuning
Settle one reading and act on it; reopen it only when a tool result
contradicts it. Re-deliberating the same evidence adds no information.
When the answer is already in context, return it. Skip filler verification
language and do not re-derive facts you settled earlier this turn.Settings
Configurable in omp's settings UI, in ~/.omp/plugins/omp-plugins.lock.json, or by environment variable. The lockfile wins over the environment.
| Setting | Env | Values | Default | Effect |
| ------------ | ------------------------- | ------------------------------------- | ------- | ------------------------------------------------------------- |
| enabled | OMP_MODEL_TUNING | boolean | true | Append the block at all. |
| weight | OMP_MODEL_TUNING_WEIGHT | auto light medium heavy off | auto | Override the derived weight. off disables the plugin. |
| showStatus | OMP_MODEL_TUNING_STATUS | boolean | true | Show the tuning weight and injection state in the omp footer. |
With showStatus on, omp's footer carries one line: ● tuning heavy when the block was appended this turn, ○ tuning heavy when the resolved model matched no rule and nothing was appended. omp strips colour from extension status text, so the symbol is the whole signal. Switching models with /model updates it on the next turn, not immediately — omp emits no model_select event, so the axis is resolved when the turn starts.
Rules
Sixteen rules across twelve concerns. Only one rule per concern ever ships, so priority decides which of several competing directives a given model sees.
| id | concern | priority | min weight |
| ------------------------------ | ---------------- | -------- | ---------- |
| declared-stop | completion | 8 | light |
| verify-once | verification | 9 | light |
| real-surface-verify | verification | 10 | light |
| settled-reading | deliberation | 11 | light |
| reasoning-aim | deliberation | 12 | light |
| effort-calibration | deliberation | 13 | medium |
| no-filler-verification | verification | 14 | medium |
| injected-directive-authority | harness-contract | 15 | medium |
| search-stop | retrieval | 16 | medium |
| reflect-then-ask | ambiguity | 20 | light |
| confirmation-executed | re-litigation | 21 | medium |
| todo-live | todo | 22 | medium |
| recommendation-not-survey | reporting | 25 | light |
| scope-literalism | scope | 30 | light |
| compaction-persistence | context-budget | 40 | medium |
| house-style-counter | aesthetics | 50 | medium |
The table is deliberately sparse. A rule earns its place only on evidence that a family needs the correction and that omp's base prompt does not already make it. Every light rule is also bounded to the series its evidence names, so a rule written for one generation cannot follow the family into the next — claude-opus-6 and gpt-6 resolve to an empty block for the same reason an unrecognized model does.
Development
bun install
bun test ./test # 74 tests across 6 files
bun x tsc --noEmit -p tsconfig.json # typecheckOne peer dependency: @oh-my-pi/pi-catalog at >=16.0.5 with no upper bound, satisfied by the host omp. Every other import is a Node builtin.
Two invariants worth knowing before changing anything:
- Append-only. The
before_agent_starthandler never replaces omp's system prompt, and it checks forTUNING_HEADERbefore appending so the block cannot accumulate across turns. - The block is anonymous. No model name, version, or profile string may appear in the rendered output.
AGENTS.md carries the full contract.
License
MIT
