diff --git a/api/config.py b/api/config.py index 4e7f8ad5..9cc61299 100644 --- a/api/config.py +++ b/api/config.py @@ -1966,8 +1966,11 @@ def get_available_models() -> dict: if _slug not in _named_custom_groups: _named_custom_groups[_slug] = (_cp_name, []) detected_providers.add(_slug) + _cp_option_id = _cp_model + if active_provider != _slug and not _cp_option_id.startswith("@"): + _cp_option_id = f"@{_slug}:{_cp_option_id}" _named_custom_groups[_slug][1].append( - {"id": _cp_model, "label": _cp_label} + {"id": _cp_option_id, "label": _cp_label} ) else: auto_detected_models.append({"id": _cp_model, "label": _cp_label}) diff --git a/tests/test_issue1106_custom_providers_models.py b/tests/test_issue1106_custom_providers_models.py index 6e34e7fa..16e7b8f1 100644 --- a/tests/test_issue1106_custom_providers_models.py +++ b/tests/test_issue1106_custom_providers_models.py @@ -196,3 +196,26 @@ class TestCustomProvidersModelsDict: # No cross-contamination assert "model-b1" not in ids_a assert "model-a1" not in ids_b + + def test_named_custom_models_are_prefixed_when_not_active_provider(self): + """Named custom provider models must carry a routing prefix when DeepSeek is active.""" + result = _models_with_cfg( + model_cfg={"provider": "deepseek", "default": "deepseek-v4-pro"}, + custom_providers=[ + { + "name": "sub2api", + "base_url": "http://127.0.0.1:8080/v1", + "model": "gpt-5.4-mini", + "models": { + "gpt-5.4-mini": {}, + "gpt-5.4": {}, + }, + } + ], + ) + group = _group_for(result, "sub2api") + assert group is not None, "sub2api group missing" + assert group["provider_id"] == "custom:sub2api" + ids = [m["id"] for m in group["models"]] + assert "@custom:sub2api:gpt-5.4-mini" in ids + assert "@custom:sub2api:gpt-5.4" in ids diff --git a/tests/test_provider_mismatch.py b/tests/test_provider_mismatch.py index 66ce09b8..2ef6a000 100644 --- a/tests/test_provider_mismatch.py +++ b/tests/test_provider_mismatch.py @@ -751,6 +751,49 @@ def test_issue1253_duplicate_model_id_active_provider_hint_preserved(monkeypatch ) +def test_named_custom_provider_hint_with_colon_is_preserved(monkeypatch): + """@custom:name:model must survive chat/start normalization for WebUI routing.""" + import api.routes as routes + + monkeypatch.setattr( + routes, + "get_available_models", + lambda: { + "active_provider": "deepseek", + "default_model": "deepseek-v4-pro", + "groups": [ + { + "provider": "sub2api", + "provider_id": "custom:sub2api", + "models": [ + { + "id": "@custom:sub2api:gpt-5.4-mini", + "label": "GPT 5.4 Mini", + } + ], + }, + { + "provider": "DeepSeek", + "provider_id": "deepseek", + "models": [ + { + "id": "deepseek-v4-pro", + "label": "DeepSeek V4 Pro", + } + ], + }, + ], + }, + ) + + effective, changed = routes._resolve_compatible_session_model( + "@custom:sub2api:gpt-5.4-mini" + ) + + assert changed is False + assert effective == "@custom:sub2api:gpt-5.4-mini" + + def test_stale_at_provider_model_falls_back_when_family_mismatches(monkeypatch): """Unroutable @provider:model should not invent a bare model for another family.""" import api.routes as routes diff --git a/tests/test_security_redaction.py b/tests/test_security_redaction.py index 09154d35..08ac49d6 100644 --- a/tests/test_security_redaction.py +++ b/tests/test_security_redaction.py @@ -11,8 +11,10 @@ Tests run against the isolated test test_server on port 8788. """ import json +import importlib import pathlib import sys +import types import urllib.request import urllib.error import pytest @@ -105,6 +107,28 @@ def test_redact_value_list(): assert result[1]["content"] == "safe text" +def test_redact_value_works_with_legacy_agent_redact_signature(monkeypatch): + """_redact_text must tolerate older redact_sensitive_text(text) signatures.""" + fake_agent = types.ModuleType("agent") + fake_redact = types.ModuleType("agent.redact") + + def _legacy_redact_sensitive_text(text): + return text + + fake_redact.redact_sensitive_text = _legacy_redact_sensitive_text + monkeypatch.setitem(sys.modules, "agent", fake_agent) + monkeypatch.setitem(sys.modules, "agent.redact", fake_redact) + + import api.helpers as helpers + helpers = importlib.reload(helpers) + try: + result = helpers._redact_value(f"token={_FAKE_GITHUB_PAT}") + assert _FAKE_GITHUB_PAT not in result + assert "ghp_Te" in result + finally: + importlib.reload(helpers) + + def test_redact_session_data_messages(): """redact_session_data masks credentials in messages[].""" from api.helpers import redact_session_data