Skip to content

Repository files navigation

Routing Matrix Bundle

Curated model routing matrices for Amplifier. Maps semantic roles (like coding, reasoning, fast) to ranked lists of provider/model candidates, so agents request what kind of model they need rather than hardcoding a specific one.

The routing hook tries candidates top-to-bottom and uses the first that matches an installed provider.

Matrices

Eight curated matrices ship with this bundle, plus one explicit-name alias (knob-consistent delegation):

Matrix When to use
balanced (default) Mixed workloads. Good quality/cost tradeoff for everyday development.
quality Maximum capability. Uses the strongest models for every role, regardless of cost.
economy Cost-optimized. Prefers free tiers, smaller models, and local providers like Ollama.
anthropic Anthropic Claude models exclusively. No knob-consistent delegation -- no measured win for this family yet (the "Anthropic guardrail").
openai OpenAI models exclusively. Ships knob-consistent delegation ON by default (2026-09-02, a measured win -- see below). Covers both OpenAI backends — the pay-per-use API (openai) and the ChatGPT subscription (openai-chatgpt) — with a single candidate per role: the resolver treats them as one family, API key first. The flagship sol tier is PAUSED as of 2026-09-07 pending further evals; roles that used it now run terra one effort notch higher, and ui-coding runs luna at max.
gemini Google Gemini models exclusively.
copilot GitHub Copilot-optimized. Balances multiplier costs, avoids the 30x fast-variant trap.
ollama A template, deliberately minimal: only the two required roles (general, fast), both model: "*". Every Ollama user has pulled a different set of models, so there is no useful curation to ship -- copy it and pin what you actually have.

openai-knob-consistent was removed on 2026-09-07. Once the preset: block became openai's default on 2026-09-02, the two files were the same matrix under two names. Select openai instead -- it is byte-for-byte what openai-knob-consistent used to give you.

Browse the matrix files directly in the routing/ directory.

Including the Bundle

Foundation already includes this bundle — no extra configuration needed if you use Foundation.

To include it in a custom bundle:

includes:
  - routing-matrix:behaviors/routing.yaml

How Agents Use model_role

Agents declare what kind of model they need via the model_role frontmatter field. The routing hook resolves this to a concrete provider/model at session start.

String shorthand — request a single role:

meta:
  name: my-agent
  description: "..."
  model_role: coding

List form with fallbacks — try roles in order:

meta:
  name: my-agent
  description: "..."
  model_role: [vision, coding, general]

The system tries vision first. If no installed provider matches any candidate for that role, it falls back to coding, then general.

Available Roles

Role Description
general Versatile catch-all, no specialization needed
fast Quick parsing, classification, file ops, bulk work
coding Code generation, implementation, debugging
ui-coding Frontend/UI code — components, layouts, styling, spatial reasoning
security-audit Vulnerability assessment, attack surface analysis, code auditing
reasoning Deep architectural reasoning, system design, complex multi-step analysis
critique Analytical evaluation — finding flaws in existing work
creative Design direction, aesthetic judgment, high-quality creative output
writing Long-form content — documentation, marketing, case studies, storytelling
research Deep investigation, information synthesis across multiple sources
vision Understanding visual input — screenshots, diagrams, UI mockups
image-gen Image generation, visual mockup creation, visual ideation
critical-ops High-reliability operational tasks — infrastructure, orchestration

Every matrix must define at least general and fast. All other roles are optional — agents fall back through their model_role list if a role isn't defined.

How model_role and provider_preferences interact (matrix strategy)

The behavior documented in this section is matrix-strategy policy. Alternative routing-strategy bundles that register the model_role_resolver capability MAY choose different semantics.

amplifier-foundation agents/skills/recipes typically declare both model_role: and provider_preferences: in their frontmatter. This is by design, not redundant:

  • provider_preferences: is the bundle-portable, always-works fallback. It functions for every AmplifierSession regardless of which bundles are installed — including sessions that don't include any routing bundle at all.
  • model_role: is the opt-in enhancement that activates only when a routing bundle (such as this one) is installed. It tells the routing bundle which semantic role to resolve against the active matrix.

What this bundle does when both fields are declared

When this bundle's hooks-routing is mounted, its session:start hook reads each agent's model_role:. For every agent that declares one, the hook resolves it against the active matrix and overwrites agent_cfg["provider_preferences"] with the matrix-resolved candidates. The hard-pinned frontmatter provider_preferences: is replaced at runtime.

When this bundle is NOT mounted (the session has no routing bundle), the hook never runs, and frontmatter provider_preferences: flows through unchanged. The agent operates on its hard-pinned fallback.

The net effect:

Configuration Routing bundle installed Routing bundle NOT installed
model_role: + provider_preferences: (typical foundation agent) Matrix resolves model_role → preferences Frontmatter provider_preferences flows through
model_role: only Matrix resolves model_role → preferences Agent gets parent's mount-plan defaults (no per-agent override)
provider_preferences: only (rare; only when matrix resolution is undesirable) Frontmatter flows through (no override) Frontmatter flows through

Author guidance

Declare both. That's the supported and recommended pattern. Authors get:

  • Per-agent provider preferences that work in any bundle composition (provider_preferences:)
  • Smart matrix resolution that activates automatically when a routing bundle is loaded (model_role:)

Per-delegate model_role overrides (e.g. delegate(agent="...", model_role="research")) take precedence over BOTH the agent's frontmatter and the matrix-resolved agent-config preferences. That precedence is the spawner's policy, not this bundle's — see amplifier-app-cli/docs/SPAWN_PRECEDENCE.md.

Knob-consistent delegation

Status (2026-09-02): PROVEN WIN on OpenAI roots, and now DEFAULT ON for the openai matrix. Measured (lane l1-knob-consistent-routing, DONE.json + ai-notes/w2-s3-three-knob-presets/ROUTING-PROPOSAL.md; S3 n=3/arm): gpt-5.6-sol call share 27.8% → 0.0%, cost −29.7% (S3 median) / −55.9% (S1), wall −16.2%, quality flat. This is scoped to OpenAI-family roots: every other shipped matrix (anthropic, balanced, quality, economy, gemini, copilot, ollama) is untouched and resolves byte-identically to before — there is no equivalent measurement yet for other provider families (the Anthropic guardrail: no evidence, no default change).

The problem this solves. A session pins its model and effort for the root only. Sub-agents are routed by the matrix, so the dial you chose governs only the root's own work. Measured on real runs: a gpt-5.6-terra tree sent 85–96% of its LLM calls to gpt-5.6-sol; a claude-haiku-4-5 cell billed $2.225 with 26 of 49 calls on sol; gpt-5.6-luna costs $1.5 when it delegates to sol versus $0.48–0.78 when it does not. Choosing a cheap tier does not buy a cheap tree.

The fix. An optional top-level preset: block adds one step to resolution — inherited caller intent — between the agent's explicit pin and the matrix candidate:

# Level Where it lives
1 delegate(provider_preferences=[…]) tool-delegate (unchanged)
2 agent frontmatter provider_preferences tool-delegate / this bundle (unchanged)
3 inherited caller tier + effort preset: in the matrix file
4 agent model_role → matrix candidate this bundle (unchanged)
5 matrix general fallback this bundle (unchanged)

Levels 1 and 2 stay strictly above level 3, so an author who deliberately pinned a specialist model still gets one. Inheritance is a default, never a ceiling on explicit intent.

Scoped by matrix, off unless the resolver can determine the caller's own model. A matrix must carry a preset: block and the resolution must be able to determine the caller's own model. Only openai.yaml carries one. Every OTHER matrix shipped with this bundle still has no preset: key and resolves byte-identically — asserted, not asserted-to, by tests/test_default_resolution_unchanged.py, which replays a recording taken from the commit immediately before the feature landed, plus modules/hooks-routing/tests/test_knob_consistent_routing.py::TestDefaultBehaviourUnchanged::test_anthropic_matrix_has_no_preset_block and ::test_anthropic_root_resolution_unchanged_vs_pre_50_matrix naming the Anthropic guardrail directly.

# routing/openai.yaml -- shipped, DEFAULT ON
preset:
  tier_ladder:                       # cheapest -> most expensive, declared
    openai:
      - ["gpt-?.?-luna*", "gpt-?.?-mini*", "gpt-?.?-nano*"]
      - ["gpt-?.?-terra*"]
      - ["gpt-?.?-sol*", "gpt-[0-9].[0-9]"]
  delegation:
    inherit: strict                  # none | effort | tier-and-effort | strict
    report_unhonored: true
inherit Behaviour
none Unclamped. What an absent preset: means, and what every non-openai matrix still does.
effort Keep the matrix's model; carry the caller's effort.
tier-and-effort Clamp the model to at most the caller's rung; carry the caller's effort; allow declared escalations.
strict As above, and escalation is denied outright for every role. This is what openai.yaml ships.

Under strict with a gpt-5.6-terra @ medium root, model_role: reasoning resolves to gpt-5.6-terra @ medium instead of gpt-5.6-sol @ xhigh — with no settings.yaml change required when your session is already routed through the openai matrix.

When intent cannot be honoured, you are told. Every clamp, denial, substitution and no-op emits a routing:intent-clamped event carrying {role, mode, honored, requested, granted, reason, escalations_remaining}. It goes to the event log — never injected into the conversation, which would mutate the cached prefix.

It is already on if you use the openai matrix. No action needed — this is the default:

# ~/.amplifier/settings.yaml
routing:
  matrix: openai

If your settings still say matrix: openai-knob-consistent, change it to openai. That file was removed on 2026-09-07; the two had been identical since the preset became the default.

Opt out, and restore legacy behaviour. If you need the pre-2026-09-02 unclamped openai matrix (root's dial governs only the root), set disable_delegation_preset: true in this hook's own mount config — the same config: block default_matrix already lives in (see behaviors/routing.yaml):

hooks:
  - module: hooks-routing
    source: git+https://github.com/microsoft/amplifier-bundle-routing-matrix@main#subdirectory=modules/hooks-routing
    config:
      default_matrix: openai
      disable_delegation_preset: true   # restores pre-2026-09-02 behaviour

This is a no-op on any matrix that never carried a preset: block in the first place — it can only ever remove behaviour, never add any.

Cross-vendor safety. A delegate spawns a new session with its own request prefix, so setting a child's effort at creation is not a mid-session effort change and cannot invalidate a parent's cached message blocks. Nothing here re-configures a running session.

Not included, on purpose. The preset: schema also accepts fan_out_max, delegate_timeout_s, on_timeout, session_scoped and context. Those are parsed and validated but never applied — bounding the delegation tree is a separate treatment from varying effort within it, and mixing them makes neither measurable. See modules/hooks-routing/amplifier_module_hooks_routing/knob_consistency.py.

Selecting a Matrix

Via CLI command:

amplifier routing use balanced   # or: quality, economy
amplifier routing list           # show available matrices
amplifier routing show           # show resolved roles for current matrix

Via settings.yaml:

# ~/.amplifier/settings.yaml (global) or .amplifier/settings.yaml (project)
routing:
  matrix: quality

Overriding Specific Roles

Users can override individual role assignments without replacing the entire matrix. Use the base keyword in settings.yaml to reference the active matrix and selectively replace roles:

# ~/.amplifier/settings.yaml
routing:
  matrix: balanced
  overrides:
    coding:
      - provider: ollama
        model: codellama:70b
    fast:
      - provider: ollama
        model: llama3:8b
      - base  # fall back to the matrix's "fast" candidates after Ollama

With base in the list, the matrix's original candidates for that role are appended after your overrides. Without base, your override completely replaces the matrix's candidates.

Creating a Custom Matrix

Create a YAML file following this schema:

name: my-matrix
description: "Short description of this matrix's philosophy."
updated: "2026-02-28"

roles:
  general:                          # REQUIRED
    description: "Balanced catch-all"
    candidates:
      - provider: anthropic         # Module type name (not "provider-anthropic")
        model: claude-sonnet-4-6    # Exact model name
      - provider: ollama
        model: "*"                  # Glob: any model from this provider

  fast:                             # REQUIRED
    description: "Quick utility work"
    candidates:
      - provider: openai
        model: gpt-5-mini

  coding:                           # Optional
    description: "Code generation"
    candidates:
      - provider: anthropic
        model: claude-sonnet-4-6
        config:                     # Optional: passed to provider session config
          reasoning_effort: high

Schema Reference

Top-level fields:

Field Required Description
name Yes Matrix identifier
description Yes Human-readable description
updated Yes Last update date (YYYY-MM-DD)
roles Yes Map of role name to role definition

Role definition:

Field Required Description
description Yes What this role is for
candidates Yes Ordered list of provider/model candidates

Candidate fields:

Field Required Description
provider Yes Module type name (e.g., anthropic, openai, ollama)
model Yes Exact model name or glob pattern (e.g., claude-sonnet-*, *)
config No Model parameters passed to provider (e.g., reasoning_effort: high)

Place custom matrix files in routing/ within this bundle, or reference them from your own bundle.

Where a matrix is loaded from (and what shadows what)

hooks-routing resolves default_matrix: <name> to the first existing <name>.yaml in this order:

  1. each configured user routing dir, in order (custom_routing_dirs, which the CLI populates with ~/.amplifier/routing/)
  2. this bundle's own routing/ dir

First hit wins; nothing is merged. A user file therefore shadows a shipped matrix of the same name — deliberately, so a local override beats the default — which also means every change shipped in this bundle's copy of that matrix has no effect on that host. amplifier routing create / edit write into ~/.amplifier/routing/, so this is easy to end up in without noticing.

Because that outcome used to be invisible, the loader now reports it:

  • the winning file is logged at INFO on every load: [ROUTING] matrix 'openai' loaded from <path> (source=user|bundle)
  • when a same-named file is shadowed, a WARNING names the winner and every file it suppressed
  • the registered model_role_resolver capability exposes matrix_path, matrix_source ("user" / "bundle") and shadowed_paths
  • a routing:matrix-loaded event carries the same fields once per session

To make a shipped matrix take effect again, remove or rename the shadowing file (or point default_matrix at a name the user dir does not define).

See docs/MATRIX_CURATOR_GUIDE.md for detailed authoring guidance.

Mount-time resolution fan-out

At session:start this hook resolves the model_role of every agent in the composed bundle, and each candidate whose model: is a glob needs the provider's model list — an HTTPS list_models() call. On a 41-agent bundle that used to be up to 41 simultaneous TLS handshakes, on every session mount: the CLI's own, and every spawned agent's.

Two bounds now apply, and they compose:

  • at most max_concurrent_role_resolutions resolutions in flight at once (default 4)
  • one list_models() call per provider per burst, not one per agent — concurrent resolutions of the same provider share a single in-flight fetch

Measured on a 41-agent bundle whose agents all resolve globs against one provider: 41 concurrent list_models() calls before, 1 after. Resolution results and warnings are unchanged; only the arrival rate and the count of identical calls.

This is not only traffic reduction. That unbounded burst is the documented trigger for a native abort: it put 20–37 threads at a time inside truststore 0.10.4's wrap_bio, which calls _configure_context without taking self._ctx_lock (its own wrap_socket does), so tens of threads ran ctx.set_default_verify_paths() on one shared ssl.SSLContext and glibc aborted the process — double free or corruption, exit 134 (sometimes SIGSEGV, exit 139), with no Python traceback and no result envelope. The missing lock is the defect and belongs upstream; bounding the fan-out removes the trigger regardless of which truststore version is installed.

Raise the ceiling if a bundle's agents span many distinct providers and mount latency matters more than the margin — setting it at or above the agent count restores the previous unbounded behaviour exactly:

hooks:
  - module: hooks-routing
    source: git+https://github.com/microsoft/amplifier-bundle-routing-matrix@main#subdirectory=modules/hooks-routing
    config:
      default_matrix: balanced
      max_concurrent_role_resolutions: 8   # default: 4; must be an integer >= 1

An invalid value is rejected at mount with a ValueError rather than silently ignored — a typo here would quietly remove the protection.

Contributing

Note

This project is not currently accepting external contributions, but we're actively working toward opening this up. We value community input and look forward to collaborating in the future. For now, feel free to fork and experiment!

Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit Contributor License Agreements.

When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

Trademarks

This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.

About

Routing Matrix bundle for the Amplifier project

Resources

Code of conduct

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages