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
+10 -4
View File
@@ -3,7 +3,7 @@
from pathlib import Path
from typing import Annotated, Literal
from pydantic import Field, SecretStr, field_validator
from pydantic import AliasChoices, Field, SecretStr, field_validator
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
_ENV = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
@@ -35,13 +35,19 @@ class AccessSettings(BaseSettings):
class LLMSettings(BaseSettings):
model_config = SettingsConfigDict(**_ENV, env_prefix="LLM_")
model_config = SettingsConfigDict(**_ENV, env_prefix="LLM_", populate_by_name=True)
# mock: deterministic offline answers (tests, demo without network); openai_compat: any OpenAI-style API (ADR-008)
# mock: deterministic offline answers (tests, demo without network); openai_compat: any OpenAI-style API (ADR-008),
# e.g. OpenRouter: LLM_BASE_URL=https://openrouter.ai/api/v1
provider: Literal["mock", "openai_compat"] = "mock"
base_url: str = ""
model: str = ""
api_key: SecretStr = SecretStr("")
# OPENROUTER_API_KEY is accepted too — the name meditron and Vobot use for the same key
api_key: SecretStr = Field(
default=SecretStr(""), validation_alias=AliasChoices("LLM_API_KEY", "OPENROUTER_API_KEY")
)
# OpenRouter refuses RF IPs: the server goes through an HTTP/SOCKS proxy (as meditron does)
proxy_url: SecretStr | None = None
timeout: float = Field(default=30.0, gt=0)
max_attempts: int = Field(default=3, ge=1)
temperature: float = Field(default=0.0, ge=0, le=2)