A newer version of the Gradio SDK is available: 6.20.0
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:
- A cast can express at most four distinct models β one per tier. Two agents on the same tier always share a model.
- 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.
AgentManifestgainsmodel_endpoint: str | None = Noneβ amodal/catalogue.pyendpoint slug.Noneβ route bymodel_profile(unchanged). - Routing.
ManifestAgentroutes by a route key βself._route_key = model_endpoint or model_profileβ and callsrouter.for_profile(self._route_key). TheModelRouteralready accepts any key;_spec_fornow 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 thefasttier rather than crashing. Offline this path is never reached:_buildserves the deterministic stub for any key, with the key folded into the stub'svariantso a different pick still varies (reproducible) output. - Composed runs.
Registry.from_world(world)builds an in-memory registry from a validatedWorldConfig, so a UI- (or LLM-) composed run flows through the samebuild_scenario/build_router/governor_forpath as a config-file run. - Fishbowl Lab. The cast section is a
@gr.renderover the scenario: one modelgr.Dropdownper non-judge player (the Judge picks in Β§04), its choices sourced only frommodal_catalogue.entries(). Picks accumulate in acast_modelsstate;collect_world_configmaps each onto the agent'smodel_endpoint(re-checking the key against the catalogue), andSummonruns 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_endpointis purely additive, so every existing manifest, scenario, and test is byte-identical (defaults toNone). - 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_configre-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
ModelProfileto an arbitrarystr: would dissolve the tier contract that drives decoding defaults, theMODEL_<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_endpointsrc/agents/base.pyβManifestAgent._route_keysrc/models/router.pyβModelRouter._spec_for/_catalogue_specsrc/core/registry.pyβRegistry.from_worldsrc/ui/fishbowl/lab.pyβmodel_choices,_cast_defaults,_judge_manifest, the castgr.render,collect_world_configsrc/ui/fishbowl/app.pyβ_compose_session, theSummonwiring
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).