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:
2 files changed
+141
-5
No files matched your search
+26
-5
@@ -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",
|
||||
)
|
||||
Reference in new issue
Block a user