Resolve /model <provider-alias> to the alias's pinned model (#825) - #826
Merged
Conversation
…de#825) /model low reported success and renamed the session model to the alias string itself — an id no endpoint serves — because resolve_model_switch fell through to Keep for any unclassifiable id. Aliases that pin a model now resolve to that alias and its pinned model, via a new ModelSwitch::SwitchAlias variant that carries the model (Switch alone maps to a route whose model is the raw argument, which would have switched the client but kept the alias string as the model). Ranked below the exact-pin and gateway rules so a same-named model pin still wins, and only for aliases that PIN a model — everything else, including unknown non-alias ids, keeps today's permissive behaviour. The active provider's own alias flows through the route layer's already-live guard: no rebuild, no spurious switch note. cmd_model now echoes the model the session landed on rather than the raw argument.
yogthos
pushed a commit
that referenced
this pull request
Sep 2, 2026
`resolve_model_switch` ends in a deliberate permissive fallthrough: an id matching no configured alias and no known model family returns `Keep`, which is what lets `/model claude-opus-6` work on release day. #826 narrowed the trap to genuinely unknown strings rather than closing it, on purpose, and that stays. But `Keep` is also what `/model off` gets, and reaching for `off` is a natural slip — it is a valid argument to both `/effort off` and `/agent off`, and `/model` has no "go back". It reported success and left `session.model = "off"`, with nothing said until the next request 400s. Keep applying it, and say so. The unrecognised case now returns its own `ModelSwitch::KeepUnrecognized`, carried through to `ModelRoute::Active { model, recognized }`, and `/model` appends one clause: switched to model: off (unrecognised — your provider may not serve it) The clause never fires on a configured alias, an exact pin, the gateway-dialect rule, or an id whose family `model_family` knows, so in practice it fires on typos and on genuinely new models. It asserts RECOGNITION, not validity — only the provider knows whether an id is servable, and the two come apart for a new-but-valid id — so the consequence stays conditional. Same clause on the ACP `/model` path.
yogthos
added a commit
that referenced
this pull request
Sep 2, 2026
`resolve_model_switch` ends in a deliberate permissive fallthrough: an id matching no configured alias and no known model family returns `Keep`, which is what lets `/model claude-opus-6` work on release day. #826 narrowed the trap to genuinely unknown strings rather than closing it, on purpose, and that stays. But `Keep` is also what `/model off` gets, and reaching for `off` is a natural slip — it is a valid argument to both `/effort off` and `/agent off`, and `/model` has no "go back". It reported success and left `session.model = "off"`, with nothing said until the next request 400s. Keep applying it, and say so. The unrecognised case now returns its own `ModelSwitch::KeepUnrecognized`, carried through to `ModelRoute::Active { model, recognized }`, and `/model` appends one clause: switched to model: off (unrecognised — your provider may not serve it) The clause never fires on a configured alias, an exact pin, the gateway-dialect rule, or an id whose family `model_family` knows, so in practice it fires on typos and on genuinely new models. It asserts RECOGNITION, not validity — only the provider knows whether an id is servable, and the two come apart for a new-but-valid id — so the consequence stays conditional. Same clause on the ACP `/model` path. Co-authored-by: Yogthos <yogthos@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #825.
Problem
/model <provider-alias>reported success and left the session on a model string no provider can serve — no*in the listing, and the next message 400s. Any unrecognised id did the same (/model bananaincluded).Cause
resolve_model_switchfalls through toKeepwhen the id can't be classified:KeepbecomesModelRoute::Active(model)andapply_model_routerenames the session model on the current client with no validation.That permissiveness is deliberate and is kept: it is what lets
/model claude-opus-6work the day it ships, before dirge knows the id. This is not a whitelist — a non-alias id still returnsKeepexactly as before.Fix
A new rule in
resolve_model_switch: if the id names a configuredprovidersalias that pins amodel, resolve to that alias's pinned model.Placed after the exact-pin rules and the #711 vendor-prefixed gateway guard, and before family inference — so an alias whose name collides with a real model id never shadows that id's own pin.
Switch(alias)was not sufficient. It maps toModelRoute::Provider { alias, model: <raw arg> }, so the session would have switched provider while keepinglowas the model — the same bug wearing a different hat. Added an accretive variant carrying the resolved model:Switchis untouched and no existing match arm changed.Active provider's own alias flows through the route layer's existing already-live guard: no client rebuild, no
· aliasnote, provider untouched. It does restore the pinned model if the session had drifted off it, which seems the right reading of/model <own-alias>.Alias lookup is case-insensitive, matching
active_provider_kind's existing convention, and usesget_key_valueso the stored key is what reaches the note and the swap guard.An alias with no pinned model (relying on its provider default) falls through unchanged rather than inventing a model string.
One UX change that falls out
cmd_model's success line now printsctx.session.modelrather than the raw argument:Identical on every non-alias path, since there
session.modelis the argument.Tests
9 new — 7 in
resolve_model_switch_tests, 2 at the route layer:banana)session.model == "glm-5.2", not"glm"), client swapped, provider updatedThe four pre-existing vendor-prefix/gateway tests pin constraint 5 unchanged.
Full suite 5432 passed, 0 failed, 1 ignored (baseline on
upstream/mainwas 5423/0/1 — all 9 new).clippy --all-targets -- -D warningsclean,cargo fmt --checkclean.Note
/modelis TUI-only, so there is no headless end-to-end for this; the two route-layer tests exerciseresolve_model_route+apply_model_routeagainst real clients, which is the layer where a "switched provider but kept the alias as the model" regression would hide.Built and tested with
--no-default-features --features no-plugin(no janet toolchain on this host — #712); CI covers the plugin feature.