feat(llm): прокси до LLM и ключ OPENROUTER_API_KEY — OpenRouter для агента логов
CI / ruff · mypy · pytest (pull_request) Successful in 2m44s
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user