feat: сервисы моделей compose пропускаются офлайн-проверкой адреса

This commit is contained in:
gglamer 2026-09-28 21:14:38 +00:00
commit c3fc1cec79
2 changed files with 14 additions and 1 deletions

View file

@ -24,6 +24,11 @@ from app.db.models import LlmCache
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
#: Имена сервисов моделей из корневого docker-compose.yml. Однословное имя
#: разрешает только DNS внутренней сети Compose, наружу оно не уходит.
DOCKER_MODEL_HOSTS = frozenset({"host.docker.internal", "llm-qwen", "llm-vikhr"})
class LlmUnavailable(RuntimeError): class LlmUnavailable(RuntimeError):
"""Сеть, ключ или провайдер отказали. Звонящий откатывается на заготовки, """Сеть, ключ или провайдер отказали. Звонящий откатывается на заготовки,
занятие продолжается — молчащий звонящий хуже шаблонной фразы.""" занятие продолжается — молчащий звонящий хуже шаблонной фразы."""
@ -40,7 +45,7 @@ def is_loopback_url(value: str, *, allow_docker_host: bool = False) -> bool:
try: try:
local_host = ip_address(host).is_loopback local_host = ip_address(host).is_loopback
except ValueError: except ValueError:
local_host = allow_docker_host and host == "host.docker.internal" local_host = allow_docker_host and host in DOCKER_MODEL_HOSTS
return (url.scheme == "http" and local_host and url.port is not None return (url.scheme == "http" and local_host and url.port is not None
and not url.username and not url.password) and not url.username and not url.password)
except (ValueError, TypeError): except (ValueError, TypeError):

View file

@ -43,6 +43,14 @@ def test_offline_model_address_must_be_literal_loopback():
) )
def test_compose_model_services_need_the_docker_flag():
for url in ("http://llm-qwen:18080/v1", "http://llm-vikhr:18081/v1"):
assert not is_loopback_url(url)
assert is_loopback_url(url, allow_docker_host=True)
assert not is_loopback_url("http://llm-qwen.evil.test:18080/v1", allow_docker_host=True)
assert not is_loopback_url("http://llm-other:18080/v1", allow_docker_host=True)
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("api_key", ["", "leftover-cloud-key"]) @pytest.mark.parametrize("api_key", ["", "leftover-cloud-key"])
async def test_local_llm_never_sends_an_api_key(monkeypatch, api_key): async def test_local_llm_never_sends_an_api_key(monkeypatch, api_key):