Files
majordomo/docs/adr/0003-parse-grammar.md
steve dcd004289f feat: foundations — canonical types, Parse grammar, env DSNs, health, chains
Phase 1 of the majordomo build:
- llm/ canonical contract (messages, parts, tools, capabilities, streaming,
  Model/Provider, error classification)
- health/ clock-injected tracker (threshold bench, exponential capped
  cooldown, reset-on-success)
- root Registry + Parse (verbatim model ids, inline recursive alias
  expansion with cycle detection, chain dedup), LLM_* env-DSN providers
  (go-llm parity: lazy fallback + eager LoadEnv), health-aware chain
  executor behind the Model interface
- provider/fake scriptable test provider; hermetic test suite incl. the
  trailing-thinking chain and foreman:// env loading
- ADRs 0001-0008, CLAUDE.md, README (honest matrix), CI workflow,
  docs/phase-1-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:35:34 +02:00

2.6 KiB

ADR-0003: Parse grammar — verbatim model ids, inline alias expansion, chains

Status: Accepted — 2026-06-10

Context

Callers (mort first) address models by string: single targets, tier aliases, and comma-separated failover chains, with custom and env-defined providers as first-class elements. go-llm's grammar is close but nests alias-chains as composite Models and strips :low/:medium/:high reasoning suffixes, which collides with Ollama-style tags (minimax-m3:cloud) and Google-style ids.

Decision

Grammar (binding, from the kickoff):

spec    := element ("," element)*
element := target | alias
target  := provider "/" model      # model = everything after the FIRST "/",
                                   # up to the next comma, passed VERBATIM
alias   := bare token, no slash
  • Provider resolution order per target: registered providers (built-ins, RegisterProvider, eagerly env-loaded) → lazy LLM_{UPPER(name)} env DSN (ADR-0004) → error naming both places checked.
  • Aliases expand inline wherever they appear (head/middle/tail), recursively, into the flat element list. Cycles are detected via the expansion stack and return ErrAliasCycle — never a hang. Inline (not nested-Model, as in go-llm) expansion keeps one flat chain so health skipping and error reporting see every element uniformly.
  • Duplicate elements after expansion are dropped (first occurrence wins): retrying an already-failed target in the same pass is never useful.
  • A single element and a multi-element chain return the same Model (a chain of one) — identical retry/health semantics, callers never branch.
  • No reasoning-suffix stripping. mort's :high dialect is handled by mort's spec layer during migration; majordomo will expose reasoning effort as an explicit request option instead.
  • The package-level Default() registry (lazy, loads process env) backs majordomo.Parse for go-llm-style one-call ergonomics; New() builds isolated registries for tests/multi-tenant use.

Consequences

  • m1/richardyoung/qwen3-14b-abliterated:q4_K_M (a real mort tier value) parses as provider m1, model richardyoung/qwen3-14b-abliterated:q4_K_M.
  • A bare token that is a provider name yields a targeted error ("use openai/").
  • Alias updates after Parse don't affect already-built Models (expansion is at Parse time). mort re-parses per request, so DB-tier edits still apply.

Alternatives considered

  • Nested alias expansion (go-llm): opaque chains inside chains; health skipping can't see the elements. Rejected.
  • Reasoning suffixes in the grammar: breaks verbatim ids. Rejected.