feat(llm): прокси до LLM и ключ OPENROUTER_API_KEY — OpenRouter для агента логов
CI / ruff · mypy · pytest (pull_request) Successful in 2m44s

- LLM_PROXY_URL (SecretStr): запросы к LLM через HTTP/SOCKS-прокси — OpenRouter
  не принимает IP из РФ, как в meditron (OPENROUTER_PROXY_URL)
- ключ читается из LLM_API_KEY или OPENROUTER_API_KEY (первый важнее)
- .env.example, README, DEPLOY: настройка OpenRouter

Refs #52

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 14:09:19 +03:00
co-authored by Claude Opus 5.5
parent a0d48d8cbc
commit 38bb238651
6 changed files with 57 additions and 8 deletions
+36
View File
@@ -137,3 +137,39 @@ def test_factory_picks_provider() -> None:
)
def test_parse_json_object(text: str, expected: dict[str, Any] | None) -> None:
assert parse_json_object(text) == expected
def test_openrouter_key_name_is_accepted(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("LLM_API_KEY", raising=False)
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
assert LLMSettings(_env_file=None).api_key.get_secret_value() == "or-key" # type: ignore[call-arg]
monkeypatch.setenv("LLM_API_KEY", "llm-key") # the project's own name wins
assert LLMSettings(_env_file=None).api_key.get_secret_value() == "llm-key" # type: ignore[call-arg]
async def test_requests_go_through_the_proxy() -> None:
seen: list[str] = []
class Proxy(httpx.AsyncBaseTransport):
"""Stands in for the HTTP proxy: httpx routes requests to the transport mounted for the proxy."""
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
seen.append(str(request.url))
return httpx.Response(200, json=completion("через прокси"))
provider = OpenAICompatProvider(settings(proxy_url="http://user:pass@proxy.example.test:3128"))
mounts = provider._http._mounts # httpx keeps the proxy transport here, no public accessor
assert mounts, "no proxy transport mounted"
provider._http._mounts = {pattern: Proxy() for pattern in mounts}
result = await provider.complete([ChatMessage("user", "x")])
await provider.aclose()
assert result.text == "через прокси" and seen == [f"{BASE}/chat/completions"]
def test_proxy_is_a_secret() -> None:
s = settings(proxy_url="http://user:pass@proxy.example.test:3128")
assert "pass" not in repr(s)