fix(profiles): resolve @provider:model through the shared parser (#7183)

_split_webui_provider_model_value() and _strip_webui_provider_prefix()
split provider-qualified model values at the last colon. That amputates
any model name carrying its own tag: @ollama:qwen3.8:27b-mtp-q8_0
resolved to model 27b-mtp-q8_0 with provider ollama:qwen3.8, and the
truncated name was sent upstream, which 404'd. The splitter also cannot
distinguish a tagged model from a multi-segment custom provider ID, so
@custom:mykey:model-a:free was affected the same way.

Both sites now delegate to config._parse_provider_qualified_model_id(),
the grammar routes.py and the gateway adopted in #6723. The import is
function-local, matching the existing api.config imports in this module.

_split_webui_provider_model_value() persists its result into profile
config, so the mangled name outlived the session that produced it.

Fixes #7182.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-08-21 06:45:52 +00:00
1 parent c54d3faca5
commit 05ab4173cf
2 files changed
+141 -5

No files matched your search

+26 -5
View File
@@ -2393,20 +2393,41 @@ def _clean_profile_config_value(value: Optional[str], field: str) -> Optional[st
def _split_webui_provider_model_value(default_model: Optional[str], model_provider: Optional[str]) -> tuple[Optional[str], Optional[str]]:
"""Normalize WebUI-internal @provider:model picker values for config.yaml."""
"""Normalize WebUI-internal @provider:model picker values for config.yaml.
Parsing is delegated to ``config._parse_provider_qualified_model_id()`` so
this agrees with the grammar every other call site uses (#6722, #6723). A
positional ``rsplit(":", 1)`` cannot tell a colon-tagged model
(``@ollama:qwen3.8:27b-mtp-q8_0``) from a multi-segment custom provider ID
(``@custom:backup:model-a``); it truncated the model name and persisted the
fragment into profile config, which the provider API then 404'd on (#7182).
"""
model = _clean_profile_config_value(default_model, "default_model")
provider = _clean_profile_config_value(model_provider, "model_provider")
if model and model.startswith("@") and ":" in model:
provider_part, model_part = model[1:].rsplit(":", 1)
provider = provider or _clean_profile_config_value(provider_part, "model_provider")
model = _clean_profile_config_value(model_part, "default_model")
from api.config import _parse_provider_qualified_model_id
parsed = _parse_provider_qualified_model_id(model)
if parsed:
model_part, provider_part = parsed
provider = provider or _clean_profile_config_value(provider_part, "model_provider")
model = _clean_profile_config_value(model_part, "default_model")
return model, provider
def _strip_webui_provider_prefix(model_id: object) -> str:
"""Return the bare model name from a WebUI ``@provider:model`` value.
Uses the same shared grammar as ``_split_webui_provider_model_value()`` so a
tagged model name survives the round trip (#7182).
"""
value = str(model_id or "").strip()
if value.startswith("@") and ":" in value:
return value.rsplit(":", 1)[1]
from api.config import _parse_provider_qualified_model_id
parsed = _parse_provider_qualified_model_id(value)
if parsed:
return str(parsed[0] or "").strip()
return value
@@ -0,0 +1,115 @@
"""Regression tests for issue #7182 — profile parsing truncated tagged models.
``api/profiles.py`` kept a positional ``rsplit(":", 1)`` long after the shared
grammar in ``config._parse_provider_qualified_model_id()`` became the single
parser for ``@provider:model`` values (#1776, #1948, #2047, #6722, #6723).
Splitting at the LAST colon amputates any model name that carries its own tag.
An Ollama model is the common shape: ``@ollama:qwen3.8:27b-mtp-q8_0`` resolved
to model ``27b-mtp-q8_0`` with provider ``ollama:qwen3.8``, and the truncated
name was then sent upstream, which 404'd. ``_split_webui_provider_model_value``
also PERSISTS its result into profile config, so the mangled name outlived the
session that produced it.
The two shapes below are the ones a positional split cannot separate:
``@ollama:qwen3.8:27b-mtp-q8_0`` is a single-segment provider with a tagged
model, while ``@custom:backup:model-a`` is a two-segment provider ID with a
plain model. Guessing either way breaks the other, which is why both live here.
"""
import pytest
from api.config import _parse_provider_qualified_model_id
from api.profiles import (
_split_webui_provider_model_value,
_strip_webui_provider_prefix,
)
# (qualified value, expected bare model, expected provider)
QUALIFIED_MODEL_CASES = [
# The reported #7182 case: Ollama name:tag behind a single-segment provider.
("@ollama:qwen3.8:27b-mtp-q8_0", "qwen3.8:27b-mtp-q8_0", "ollama"),
# Untagged model through the same provider still resolves.
("@ollama:llama4", "llama4", "ollama"),
# Named custom provider — provider keeps both segments.
("@custom:backup:model-a", "model-a", "custom:backup"),
# Named custom provider AND a tagged model.
("@custom:mykey:model-a:free", "model-a:free", "custom:mykey"),
# host:port custom provider IDs.
("@custom:192.168.1.5:11434:llama4", "llama4", "custom:192.168.1.5:11434"),
("@openrouter:meta/llama-4:free", "meta/llama-4:free", "openrouter"),
]
class TestIssue7182ReportedShape:
"""The exact assertions the issue pinned, kept verbatim."""
def test_split_preserves_ollama_tag(self):
assert _split_webui_provider_model_value(
"@ollama:qwen3.8:27b-mtp-q8_0", None
) == ("qwen3.8:27b-mtp-q8_0", "ollama")
def test_strip_preserves_ollama_tag(self):
assert (
_strip_webui_provider_prefix("@ollama:qwen3.8:27b-mtp-q8_0")
== "qwen3.8:27b-mtp-q8_0"
)
def test_split_preserves_multi_segment_custom_provider(self):
assert _split_webui_provider_model_value(
"@custom:mykey:model-a:free", None
) == ("model-a:free", "custom:mykey")
class TestProfilesUseTheSharedGrammar:
"""profiles' helpers must agree with the shared config parser, exactly."""
@pytest.mark.parametrize(
"value,expected_model,expected_provider", QUALIFIED_MODEL_CASES
)
def test_split_matches_shared_parser(self, value, expected_model, expected_provider):
assert _parse_provider_qualified_model_id(value) == (
expected_model,
expected_provider,
), f"shared parser disagrees for {value!r}"
assert _split_webui_provider_model_value(value, None) == (
expected_model,
expected_provider,
), f"profiles splitter disagrees for {value!r}"
@pytest.mark.parametrize(
"value,expected_model,expected_provider", QUALIFIED_MODEL_CASES
)
def test_strip_matches_shared_parser(self, value, expected_model, expected_provider):
assert _strip_webui_provider_prefix(value) == expected_model
def test_explicit_provider_argument_still_wins(self):
"""An explicitly supplied provider is not overridden by the parsed hint."""
assert _split_webui_provider_model_value(
"@ollama:qwen3.8:27b-mtp-q8_0", "ollama"
) == ("qwen3.8:27b-mtp-q8_0", "ollama")
def test_no_last_colon_split_remains(self):
"""A positional rsplit would yield the bare tag; the shared grammar does not."""
model, _provider = _split_webui_provider_model_value(
"@ollama:qwen3.8:27b-mtp-q8_0", None
)
assert model != "27b-mtp-q8_0"
class TestUnqualifiedValuesUntouched:
"""Values that are not @provider-qualified must pass through unchanged."""
@pytest.mark.parametrize(
"value",
["qwen3.8:27b-mtp-q8_0", "llama4", "", " ", "@nocolon"],
)
def test_strip_passthrough(self, value):
assert _strip_webui_provider_prefix(value) == value.strip()
def test_split_passthrough_keeps_plain_model(self):
assert _split_webui_provider_model_value("qwen3.8:27b-mtp-q8_0", "ollama") == (
"qwen3.8:27b-mtp-q8_0",
"ollama",
)