What is missing
AgentDefinition has a model: Option<String> documented as "preferred model id, when the agent pins one". ModelResolveRequest has three fields — agent_id, role, is_team_lead — and no model field.
So there is no defined route for a definition's exact pin (claude-3-5-sonnet, a BYOK id, a local model name) to reach ModelResolver::resolve. The two seams are adjacent and never connected.
Why it matters now rather than later
Nothing in the crate constructs a ModelResolveRequest today, so nothing is currently broken. The risk is what a wiring author does when they need a pin honoured: role is the only string field available, so the natural move is to put the model id there.
That misroutes, and quietly. A host resolver reasonably treats role as a workload-role vocabulary, so an arbitrary model id becomes an unrecognised role and falls back to a default. In OpenHuman's adapter the id would additionally have been mistaken for a tier if it ended in -v1 (role_for_model_tier answers "chat" for anything it does not recognise, with no diagnostic). I tightened that suffix heuristic in tinyhumansai/openhuman#5396, but the tightening only converts a silent misroute into a warned one — the pin is still dropped, because there is nowhere for it to go.
Two possible shapes
A. Carry the pin on the request.
pub struct ModelResolveRequest {
pub agent_id: String,
pub role: Option<String>,
pub is_team_lead: bool,
/// Exact model id the definition pinned, if any. Distinct from `role`:
/// a role is a host taxonomy, this is a concrete model.
pub model_pin: Option<String>,
}
Keeps one resolution site and lets the host decide whether it can honour the pin (credentials, provider availability) rather than the runner deciding for it.
B. Document that the runner consults definition.model first and only calls the resolver when it is None.
Simpler, but it splits model selection across two places and means a host cannot apply its own policy to a pin — OpenHuman would want to validate the id against configured providers rather than pass it through blind.
I would favour A, but either is fine as long as it is written down. The thing to avoid is leaving role as the only channel, because "role may also be a model id" is exactly the ambiguity that produces the bug.
Impact
Phase 4 repointing blocker on the OpenHuman side (docs/specs/plan-agents.md). Per-agent model pins in agent.toml and user-authored custom agents would silently resolve to the workload default until this is settled.
Found by review on tinyhumansai/openhuman#5396.
What is missing
AgentDefinitionhas amodel: Option<String>documented as "preferred model id, when the agent pins one".ModelResolveRequesthas three fields —agent_id,role,is_team_lead— and no model field.So there is no defined route for a definition's exact pin (
claude-3-5-sonnet, a BYOK id, a local model name) to reachModelResolver::resolve. The two seams are adjacent and never connected.Why it matters now rather than later
Nothing in the crate constructs a
ModelResolveRequesttoday, so nothing is currently broken. The risk is what a wiring author does when they need a pin honoured:roleis the only string field available, so the natural move is to put the model id there.That misroutes, and quietly. A host resolver reasonably treats
roleas a workload-role vocabulary, so an arbitrary model id becomes an unrecognised role and falls back to a default. In OpenHuman's adapter the id would additionally have been mistaken for a tier if it ended in-v1(role_for_model_tieranswers"chat"for anything it does not recognise, with no diagnostic). I tightened that suffix heuristic in tinyhumansai/openhuman#5396, but the tightening only converts a silent misroute into a warned one — the pin is still dropped, because there is nowhere for it to go.Two possible shapes
A. Carry the pin on the request.
Keeps one resolution site and lets the host decide whether it can honour the pin (credentials, provider availability) rather than the runner deciding for it.
B. Document that the runner consults
definition.modelfirst and only calls the resolver when it isNone.Simpler, but it splits model selection across two places and means a host cannot apply its own policy to a pin — OpenHuman would want to validate the id against configured providers rather than pass it through blind.
I would favour A, but either is fine as long as it is written down. The thing to avoid is leaving
roleas the only channel, because "role may also be a model id" is exactly the ambiguity that produces the bug.Impact
Phase 4 repointing blocker on the OpenHuman side (
docs/specs/plan-agents.md). Per-agent model pins inagent.tomland user-authored custom agents would silently resolve to the workload default until this is settled.Found by review on tinyhumansai/openhuman#5396.