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:
+10
-4
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Any OpenAI-style `/chat/completions` endpoint: OpenAI, YandexGPT compatibility API, a local model server."""
|
||||
"""Any OpenAI-style `/chat/completions` endpoint: OpenRouter, OpenAI, YandexGPT compatibility API, a local server."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
@@ -34,6 +34,7 @@ class OpenAICompatProvider:
|
||||
headers={"Authorization": f"Bearer {settings.api_key.get_secret_value()}"},
|
||||
timeout=settings.timeout,
|
||||
transport=transport,
|
||||
proxy=settings.proxy_url.get_secret_value() if settings.proxy_url else None,
|
||||
)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user