You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Discuss the design here before implementing it. A maintainer's design-approved label is what unblocks a linked PR; keep the PR in draft until approval.
What problem does this solve?
On OMP, model selection is expressed as roles (modelRoles.<name>, referenced as @<name>), and a role can carry its own ordered retry.fallbackChains.<name>. That is the natural place to describe "when the historian model is unavailable, degrade through these, in this order" — including thinking level per hop, since OMP selectors are provider/model:level.
Magic Context has its own parallel mechanism: historian.model + historian.fallback_models per harness, with thinking_level per entry. Today the two cannot be composed, so OMP users have to duplicate the policy:
and keep config.yml's retry.fallbackChains.historian in sync by hand. Change the role in one place, the other goes stale.
A role alias already works for the primary today, by accident rather than by design."model": "@historian" passes resolvePrimaryEntry (no validation) and reaches the OMP CLI as --model @historian, which OMP expands through modelRoleAliasPrefixLength. Verified live against OMP 18.x: --model @Oracle resolves to opencode-zen/space-bunny-free, --model @Mimo26 to opencode-zen/mimo-v2.6-flash-free, and --model @tiny reaches the configured free model's own rate limit. So the primary already follows modelRoles, and changing config.yml changes the historian model with no MC edit.
What does not work is the rest of the contract:
fallback_models silently drops role aliases.resolveFallbackEntries → isValidModelReference (packages/plugin/src/shared/model-resolution.ts:63-66, 85) requires a / with non-empty content on both sides, so "@SomeRole" fails and the entry is continued. There is no error and no warning: the historian simply has fewer fallbacks than configured. parseProviderModel (shared/resolve-fallbacks.ts:61-64) has the same rule and returns {}, so the failure surfaces as an empty model rather than an invalid one.
The role's own retry.fallbackChains are not consulted. MC spawns its subagent with a single --model; the fallback walk inside OMP belongs to the session's active turn, and an MC-spawned child is a fresh omp --print invocation, so the role's chain is never entered.
Thinking level has two homes and no precedence rule.modelRoles.historian: "provider/model:xhigh" carries the level for the primary, while thinking_level in the MC block carries it independently. When a role alias is used the two can disagree, and nothing documents which wins.
Net effect: the one field that does accept a role is the only one that cannot also use it consistently, and the failure mode is silence.
Proposed solution
Accept OMP role references everywhere a model is accepted, and let OMP own the chain.
Config surface — unchanged shape, model and fallback_models entries may be a role alias:
isValidModelReference (and parseProviderModel for the same inputs) accept a leading @ / pi/ role prefix, so a role entry is a first-class attempt rather than a silent drop.
The entry is passed through to the child as-is (--model @role); OMP already expands it, so no provider mapping is needed on MC's side.
Ordering. MC should preserve declaration order and its own per-attempt retry/fresh-child semantics. OMP's role chain is the one thing MC should not silently merge in, because MC already owns ordered attempts and two ladders would double-step. If a maintainer prefers delegation instead, the alternative is: MC passes only the role, and the child session resolves the chain from retry.fallbackChains.<role> — which requires the child to be a real retrying session rather than one --model invocation. That is a bigger change, so the concrete ask is the smaller one first.
Thinking level. When the entry is a role alias, the role's selector suffix is authoritative and the entry's own thinking_level is ignored, with the MC-side value used only when the role does not declare one. Documenting that precedence is the part that matters most, because today it is undefined.
A warning when an alias cannot resolve, instead of dropping it. A typo'd role and an invalid selector should both be visible in the existing status/log surface.
Why this is small. The primary path already works end to end. What is missing is (a) the validator accepting @, (b) not discarding alias entries in the fallback list, and (c) a documented level precedence. The role-expansion logic already lives in OMP.
Alternatives considered
Keep concrete model ids in MC and let config.yml be the source of truth for order. This is what I am doing today; it works but duplicates the policy and drifts the moment a role changes.
MC reads modelRoles itself. Rejected: MC is harness-agnostic and would need an OMP-specific settings reader plus a refresh path for runtime /model changes. Passing the alias and letting the host resolve it keeps the host's model registry authoritative — which is the same reason MC already defers provider naming to the harness.
Add a new role: key rather than overloading the string. Rejected as heavier for no gain; @name is already the host's own spelling, and reusing it means no new vocabulary.
Additional context
OMP role/chain config: modelRoles.<name>, retry.fallbackChains.<name>, selectors as provider/model[:level].
The same alias shape would serve dreamer (including per-task dreamer.omp.tasks.<task>.model), and mural.model where it is currently a bare string.
If the maintainer prefers, a one-line acknowledgement that role aliases are unsupported in fallback_models (today's silent behavior) would also prevent the trap, even if full support is deferred.
Discuss the design here before implementing it. A maintainer's
design-approvedlabel is what unblocks a linked PR; keep the PR in draft until approval.What problem does this solve?
On OMP, model selection is expressed as roles (
modelRoles.<name>, referenced as@<name>), and a role can carry its own orderedretry.fallbackChains.<name>. That is the natural place to describe "when the historian model is unavailable, degrade through these, in this order" — including thinking level per hop, since OMP selectors areprovider/model:level.Magic Context has its own parallel mechanism:
historian.model+historian.fallback_modelsper harness, withthinking_levelper entry. Today the two cannot be composed, so OMP users have to duplicate the policy:and keep
config.yml'sretry.fallbackChains.historianin sync by hand. Change the role in one place, the other goes stale.A role alias already works for the primary today, by accident rather than by design.
"model": "@historian"passesresolvePrimaryEntry(no validation) and reaches the OMP CLI as--model @historian, which OMP expands throughmodelRoleAliasPrefixLength. Verified live against OMP 18.x:--model @Oracleresolves toopencode-zen/space-bunny-free,--model @Mimo26toopencode-zen/mimo-v2.6-flash-free, and--model @tinyreaches the configured free model's own rate limit. So the primary already followsmodelRoles, and changingconfig.ymlchanges the historian model with no MC edit.What does not work is the rest of the contract:
fallback_modelssilently drops role aliases.resolveFallbackEntries→isValidModelReference(packages/plugin/src/shared/model-resolution.ts:63-66, 85) requires a/with non-empty content on both sides, so"@SomeRole"fails and the entry iscontinued. There is no error and no warning: the historian simply has fewer fallbacks than configured.parseProviderModel(shared/resolve-fallbacks.ts:61-64) has the same rule and returns{}, so the failure surfaces as an empty model rather than an invalid one.retry.fallbackChainsare not consulted. MC spawns its subagent with a single--model; the fallback walk inside OMP belongs to the session's active turn, and an MC-spawned child is a freshomp --printinvocation, so the role's chain is never entered.modelRoles.historian: "provider/model:xhigh"carries the level for the primary, whilethinking_levelin the MC block carries it independently. When a role alias is used the two can disagree, and nothing documents which wins.Net effect: the one field that does accept a role is the only one that cannot also use it consistently, and the failure mode is silence.
Proposed solution
Accept OMP role references everywhere a model is accepted, and let OMP own the chain.
Config surface — unchanged shape,
modelandfallback_modelsentries may be a role alias:Behavior
isValidModelReference(andparseProviderModelfor the same inputs) accept a leading@/pi/role prefix, so a role entry is a first-class attempt rather than a silent drop.--model @role); OMP already expands it, so no provider mapping is needed on MC's side.retry.fallbackChains.<role>— which requires the child to be a real retrying session rather than one--modelinvocation. That is a bigger change, so the concrete ask is the smaller one first.thinking_levelis ignored, with the MC-side value used only when the role does not declare one. Documenting that precedence is the part that matters most, because today it is undefined.Why this is small. The primary path already works end to end. What is missing is (a) the validator accepting
@, (b) not discarding alias entries in the fallback list, and (c) a documented level precedence. The role-expansion logic already lives in OMP.Alternatives considered
config.ymlbe the source of truth for order. This is what I am doing today; it works but duplicates the policy and drifts the moment a role changes.modelRolesitself. Rejected: MC is harness-agnostic and would need an OMP-specific settings reader plus a refresh path for runtime/modelchanges. Passing the alias and letting the host resolve it keeps the host's model registry authoritative — which is the same reason MC already defers provider naming to the harness.role:key rather than overloading the string. Rejected as heavier for no gain;@nameis already the host's own spelling, and reusing it means no new vocabulary.Additional context
modelRoles.<name>,retry.fallbackChains.<name>, selectors asprovider/model[:level].dreamer(including per-taskdreamer.omp.tasks.<task>.model), andmural.modelwhere it is currently a bare string.fallback_models(today's silent behavior) would also prevent the trap, even if full support is deferred.