diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3635dd2 --- /dev/null +++ b/.env.example @@ -0,0 +1,65 @@ +# ── LLM (OpenRouter / любой совместимый провайдер) ──────────── +# Базовый URL и ключ: +# - по умолчанию: OpenRouter +# - можно переопределить под другой API, если он совместим по формату +LLM_BASE_URL=https://openrouter.ai/api/v1/chat/completions +LLM_API_KEY=sk-or-v1-... +# Для обратной совместимости с текущей конфигурацией: +ROUTER_KEY=${LLM_API_KEY} + +# Авторизационный заголовок и префикс (опционально): +#LLM_AUTH_HEADER=Authorization +#LLM_AUTH_PREFIX=Bearer + +# Прокси: HTTP или SOCKS5, например: +# socks5://user:pass@127.0.0.1:1080 +# http://127.0.0.1:8080 +# Любая из переменных может использоваться (берётся первая непустая): +#LLM_PROXY= +#LLM_SOCKS5= +#LLM_SOCKS5_PROXY= + +# ── Model routing ───────────────────────────────────────────── +CHAT_MODEL=deepseek/deepseek-v4-flash +SYSTEM_MODEL=deepseek/deepseek-chat-v3 +LLM_FALLBACK_MODEL=mistralai/mistral-nemo + +RPG_PLOT_MODEL=deepseek/deepseek-chat-v3 +RPG_NARRATOR_MODEL=deepseek/deepseek-chat-v3 +RPG_FACTS_MODEL=deepseek/deepseek-chat-v3 +SD_PROMPT_MODEL=deepseek/deepseek-chat-v3 + +FACTS_STORE_LIMIT=100 + +# ── Comfy / SD ──────────────────────────────────────────────── +# Comfy на том же хосте, что и docker compose (Linux): +# вариант A — LAN-IP сервера (часто работает сразу) +SD_BASE_URL=http://192.168.1.109:8188 +# вариант B — host.docker.internal (нужен extra_hosts в compose, уже добавлен) +# SD_BASE_URL=http://host.docker.internal:8188 +# Comfy на другой машине в сети: +# SD_BASE_URL=http://84.50.156.15:8188 + +SD_COMFY_TOKEN= +SD_CHECKPOINT= +SD_UNET=anima-preview3-base.safetensors +SD_CLIP=qwen_3_06b_base.safetensors +SD_VAE=qwen_image_vae.safetensors +SD_STYLE_LORA=anima-preview-3-masterpieces-v5.safetensors +SD_STYLE_LORA_WEIGHT=0.7 + +SD_AUTO_GENERATE=true +SD_STEPS=30 +SD_CFG=4 +SD_SAMPLER=er_sde +SD_SCHEDULER=simple +SD_ANIMA_DUAL_COMPARE=false + +# ── LibreTranslate ──────────────────────────────────────────── +# Тот же хост: LAN-IP или host.docker.internal:5100 +LIBRETRANSLATE_URL=http://192.168.1.109:5100 + +# Порт веб-UI и API (по умолчанию 8201) +APP_PORT=8201 + +# DB_PATH и IMAGES_DIR заданы в docker-compose.yml — в .env не нужны diff --git a/services/llm.py b/services/llm.py index 4a1a796..81d95de 100644 --- a/services/llm.py +++ b/services/llm.py @@ -8,16 +8,33 @@ load_dotenv() logger = logging.getLogger(__name__) -OPENROUTER_KEY = os.getenv("ROUTER_KEY") -OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" +# ── Provider / transport configuration ───────────────────────────── +# По умолчанию работаем через OpenRouter, но базовый URL и ключ +# можно переопределить для любого совместимого провайдера. +LLM_BASE_URL = ( + os.getenv("LLM_BASE_URL") + or "https://openrouter.ai/api/v1/chat/completions" +) +LLM_API_KEY = os.getenv("LLM_API_KEY") or os.getenv("ROUTER_KEY") +LLM_AUTH_HEADER = os.getenv("LLM_AUTH_HEADER", "Authorization") +LLM_AUTH_PREFIX = os.getenv("LLM_AUTH_PREFIX", "Bearer ") +# SOCKS5 / HTTP‑прокси (например: socks5://user:pass@host:1080) +LLM_PROXY_URL = ( + os.getenv("LLM_PROXY") + or os.getenv("LLM_SOCKS5") + or os.getenv("LLM_SOCKS5_PROXY") +) +PROXIES = {"all": LLM_PROXY_URL} if LLM_PROXY_URL else None + +# ── Модели ───────────────────────────────────────────────────────── CHAT_MODEL = os.getenv("CHAT_MODEL", "mistralai/mistral-nemo") SYSTEM_MODEL = os.getenv("SYSTEM_MODEL", "google/gemini-2.5-flash") # Softer model when primary returns content_filter / empty / API errors (default: CHAT_MODEL). LLM_FALLBACK_MODEL = (os.getenv("LLM_FALLBACK_MODEL") or "").strip() or CHAT_MODEL HEADERS = { - "Authorization": f"Bearer {OPENROUTER_KEY}", + LLM_AUTH_HEADER: f"{LLM_AUTH_PREFIX}{LLM_API_KEY}" if LLM_API_KEY else "", "Content-Type": "application/json", "HTTP-Referer": "http://localhost:8000", } @@ -86,12 +103,12 @@ def _clean(messages: list) -> list: async def _post_once(model: str, messages: list, extra: dict | None = None) -> str: - if not OPENROUTER_KEY: - raise LLMError("ROUTER_KEY is not set in environment") + if not LLM_API_KEY: + raise LLMError("LLM_API_KEY / ROUTER_KEY is not set in environment") payload = {"model": model, "messages": _clean(messages), **(extra or {})} - async with httpx.AsyncClient(timeout=90) as client: - r = await client.post(OPENROUTER_URL, headers=HEADERS, json=payload) + async with httpx.AsyncClient(timeout=90, proxies=PROXIES) as client: + r = await client.post(LLM_BASE_URL, headers=HEADERS, json=payload) try: data = r.json() except Exception as e: @@ -108,7 +125,7 @@ async def _post_once(model: str, messages: list, extra: dict | None = None) -> s return _parse_completion_body(data) except LLMError: logger.warning( - "OpenRouter completion failed model=%s status=%s body=%.500s", + "LLM completion failed model=%s status=%s body=%.500s", model, r.status_code, data, @@ -157,9 +174,9 @@ async def stream_message(messages: list): } timeout = httpx.Timeout(connect=10, read=120, write=10, pool=5) chunk_count = 0 - async with httpx.AsyncClient(timeout=timeout) as client: + async with httpx.AsyncClient(timeout=timeout, proxies=PROXIES) as client: try: - async with client.stream("POST", OPENROUTER_URL, headers=HEADERS, json=payload) as response: + async with client.stream("POST", LLM_BASE_URL, headers=HEADERS, json=payload) as response: response.raise_for_status() buf = "" async for raw in response.aiter_bytes():