The previous revision stripped the '@provider:' qualifier from model_value
in _session_model_state_from_request BEFORE _resolve_compatible_session_model_state
ran. That defeated two behaviors that rely on the qualified form:
1. provider-switch repair - a session pinned to a removed/unconfigured
provider (@removed:mistral-large) no longer reverted to the active
default; it returned (bare_model, removed) and targeted an unavailable
provider.
2. PR #6718's custom-provider normalizer, which needs the still-qualified
value to map @custom:<slug>:<model> to the canonical slug.
Keep the qualifier through session-state resolution (matching master) and
strip it to the bare model ONLY at the gateway request boundary via
_gateway_model_field(), which already runs on both gateway request sites.
Red-before-green: the rewritten test asserts @removed:mistral-large repairs
to the active default; it failed on the prior head and passes now.
The gateway-body assertions (TestGatewayRequestBodiesCarryBareModel) still
prove only the outbound payload is bare.
Verified: 275 tests pass across the issue-6722, provider-mismatch,
model-resolution-fast-path, gateway-approval (legacy+runs), gateway-routing,
custom-provider, session-model-switch and model-not-found suites.
Pins the provider/model pair for every qualified-model shape at all
three layers that reach the gateway: the routes splitter, the provider
normalizer, the request-state builder, and both gateway request bodies
(legacy chat completions + runs API).
The table-driven cases include the shapes a positional colon split gets
wrong -- @custom:backup:model-a, @custom:backup:model-a:free,
@custom:<host>:<port>:model and @ollama-cloud:model:tag -- and assert
the routes splitter agrees with config._parse_provider_qualified_model_id()
exactly, so re-introducing a local split heuristic fails loudly.
The gateway assertions read the JSON actually written to the request
body rather than mocking the helper under test.
All 12 fail on master before the fix and pass after it.
A provider-qualified model string (@provider:model) is deliberately kept
intact for internal routing (#1253), but three places on the way to the
Hermes gateway mishandled it:
* _clean_session_model_provider returned the whole "provider:model"
value as the provider, so the gateway saw a provider named
"ollama-cloud:deepseek-v4-flash" -> "Unknown provider".
* _session_model_state_from_request extracted the provider correctly
but left the full "@provider:model" string as the model value.
* both gateway request builders sent that string verbatim as
body["model"], which the upstream provider API 404'd on.
Rather than add positional colon-split heuristics, route all three
through config._parse_provider_qualified_model_id() -- the existing
shared grammar that already distinguishes a multi-segment custom
provider ID (custom:<slug>, custom:<host>:<port>) from a model segment
carrying its own colon tag (:free, :0731). A positional split cannot
tell those two shapes apart, so any single split direction breaks the
other; the shared parser is the only thing that gets both right.
_split_provider_qualified_model now delegates to that parser too, which
fixes the same class of truncation at all of its call sites in routes.py
instead of only the two reported ones, and guarantees the provider used
for the routing decision matches the model sent in the request.
Fixes#6722