multi-agent-lab / docs /adr /0022-per-agent-explicit-model-binding.md
agharsallah
feat: Enhance model routing and deployment flexibility
c1656a8
|
Raw
History Blame Contribute Delete
5.2 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

ADR-0022: Per-Agent Explicit Model Binding (model_endpoint)

Status

Accepted

Context

ADR-0010 gave per-agent model selection through four logical tiers (tiny/fast/balanced/strong), and config/models.yaml binds each tier to one concrete catalogue model. That is the right default, but it has two limits:

  1. A cast can express at most four distinct models β€” one per tier. Two agents on the same tier always share a model.
  2. The unbound specialist models are unreachable. Several catalogue entries have profile=None (Nemotron Cascade 14B, Nemotron 30B, MiniCPM-o) precisely so they do not displace a tier default β€” but then no manifest could ever cast them.

Both bite the hackathon strategy directly: the unfair advantage is running different sponsor models in one cast (Judge β†’ Nemotron, a worker β†’ MiniCPM) to qualify for multiple tracks from a single submission. And the Fishbowl Lab needed to let a user pick concrete Modal-hosted models per cast member β€” with the pick actually driving the run (the Lab's model controls were previously cosmetic: on_summon ignored collect_world_config and always built the scenario's default cast).

Decision

Add an optional, additive per-agent override that names a specific catalogue model, leaving the tier system as the default and fallback.

  • Manifest. AgentManifest gains model_endpoint: str | None = None β€” a modal/catalogue.py endpoint slug. None β†’ route by model_profile (unchanged).
  • Routing. ManifestAgent routes by a route key β€” self._route_key = model_endpoint or model_profile β€” and calls router.for_profile(self._route_key). The ModelRouter already accepts any key; _spec_for now resolves a non-tier key against the catalogue (_catalogue_spec: modal_catalogue.binding_for(key) for the live model string / endpoint URL / api key, with decoding inherited from the model's tier β€” an unbound specialist β†’ balanced). An unknown non-tier key degrades to the fast tier rather than crashing. Offline this path is never reached: _build serves the deterministic stub for any key, with the key folded into the stub's variant so a different pick still varies (reproducible) output.
  • Composed runs. Registry.from_world(world) builds an in-memory registry from a validated WorldConfig, so a UI- (or LLM-) composed run flows through the same build_scenario / build_router / governor_for path as a config-file run.
  • Fishbowl Lab. The cast section is a @gr.render over the scenario: one model gr.Dropdown per non-judge player (the Judge picks in Β§04), its choices sourced only from modal_catalogue.entries(). Picks accumulate in a cast_models state; collect_world_config maps each onto the agent's model_endpoint (re-checking the key against the catalogue), and Summon runs the composed world. Only catalogue-hosted models are offerable, and the selection is load-bearing.

Consequences

  • A cast can pin any catalogue model per agent, including the unbound specialists β€” enabling genuine multi-sponsor-model casts from one engine, one submission.
  • The tier abstraction (ADR-0010) is untouched: it remains the default, the decoding source, the offline-variant tag, and the fallback. model_endpoint is purely additive, so every existing manifest, scenario, and test is byte-identical (defaults to None).
  • The Lab's model picker is now functional, not cosmetic: the model you choose is the model that speaks (offline β†’ the deterministic stub, demo still reproducible). A bad compose degrades to the scenario's default cast, so Summon never breaks the demo.
  • A run cannot point at an undeployed model: the UI offers only catalogue entries, and collect_world_config re-validates the key, dropping anything out-of-band or stale.
  • Offline determinism is preserved end-to-end (the route key, not just the tier, seeds the stub).

Alternatives considered

  • Per-tier rebinding (let the run choose which catalogue model backs each of the four tiers): zero engine change, but still capped at four distinct models and still cannot reach the unbound specialists. Rejected as too weak for the multi-sponsor goal.
  • Widening ModelProfile to an arbitrary str: would dissolve the tier contract that drives decoding defaults, the MODEL_<TIER> env overrides, and the offline variant. Rejected in favour of a separate, additive field that keeps both concepts crisp.

Code

  • src/core/manifest.py β€” AgentManifest.model_endpoint
  • src/agents/base.py β€” ManifestAgent._route_key
  • src/models/router.py β€” ModelRouter._spec_for / _catalogue_spec
  • src/core/registry.py β€” Registry.from_world
  • src/ui/fishbowl/lab.py β€” model_choices, _cast_defaults, _judge_manifest, the cast gr.render, collect_world_config
  • src/ui/fishbowl/app.py β€” _compose_session, the Summon wiring

See also: ADR-0010 (logical-profile routing), ADR-0011 (declarative validatable config), ADR-0015 (LiteLLM gateway to Modal models), ADR-0019 (single model catalogue), ADR-0021 (Fishbowl Gradio presenter).