Skip to content

OMP: accept role aliases (@role) in historian/dreamer model config, including fallback_models #526

Description

@Qiiks

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:

// magic-context.jsonc
"historian": { "omp": { "model": "experiantallabs/gpt-5.6-luna", "fallback_models": [
  { "model": "Atria/Atria-Dawn-Preview", "thinking_level": "max" },
  { "model": "opencode-zen/mimo-v2.6-flash-free", "thinking_level": "high" }
]}}

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:

  1. 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.
  2. 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.
  3. 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:

"historian": { "omp": { "model": "@historian", "fallback_models": ["@historian-cheap"] }}

Behavior

  • 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.
  • Related: the OMP-side counterpart is Two modelRoles on the identical selector cannot have independent fallbackChains; the first YAML key wins can1357/oh-my-pi#13187 (two roles on the identical selector cannot carry independent chains) — different repo, but the same underlying idea: a role's fallback policy is a property of the role, not of the model.
  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions