All checks were successful
CI / changes (pull_request) Successful in 5s
CI / backend-tests (push) Successful in 6m25s
CI / backend-tests (pull_request) Successful in 6m20s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 2m42s
Deploy / deploy (push) Successful in 1m13s
CI / changes (push) Successful in 5s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
Optional external-OpenAI layer over the deterministic forecasting engine. Gated by llm_enabled (default False) so prod makes no network calls until deliberately enabled. Allowlist-first SafePayload contract + is_confidential hard-block + RU-PII regex scrub (mandatory on the external path). Abstract LLMProvider seam (is_external) for a future RU-hosted provider. Sync httpx core (Celery-friendly); tool/function-calling pass-through; timeout + bounded 429/5xx retry + per-request call cap, all degrading to fallback. Raw httpx (no openai SDK -> no pyproject/lock drift). 47 tests, ruff + mypy clean. Refs #960
40 lines
1.8 KiB
Python
40 lines
1.8 KiB
Python
"""Config defaults для LLM-инфраструктуры (#960).
|
|
|
|
Критично: llm_enabled СТРОГО False по умолчанию, ключ None. Это гарантирует, что
|
|
прод не позвонит в OpenAI до осознанного включения.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.core.config import Settings
|
|
|
|
|
|
def test_llm_disabled_by_default() -> None:
|
|
"""llm_enabled default False — прод не делает сетевых вызовов случайно."""
|
|
s = Settings(_env_file=None) # type: ignore[call-arg] # игнор .env при проверке дефолтов
|
|
assert s.llm_enabled is False
|
|
|
|
|
|
def test_openai_api_key_none_by_default() -> None:
|
|
"""Ключ не зашит — None по умолчанию (читается только из env OPENAI_API_KEY)."""
|
|
s = Settings(_env_file=None) # type: ignore[call-arg]
|
|
assert s.openai_api_key is None
|
|
|
|
|
|
def test_llm_safe_defaults() -> None:
|
|
"""Прочие дефолты безопасны: внешний base_url, конечные таймаут/лимиты, cap=None."""
|
|
s = Settings(_env_file=None) # type: ignore[call-arg]
|
|
assert s.llm_model == "gpt-4o-mini"
|
|
assert s.llm_base_url == "https://api.openai.com/v1"
|
|
assert s.llm_timeout_s > 0
|
|
assert s.llm_max_output_tokens > 0
|
|
assert s.llm_max_calls_per_request >= 1
|
|
assert s.llm_daily_cost_cap_usd is None
|
|
assert s.llm_max_retries >= 0
|
|
|
|
|
|
def test_openai_api_key_reads_from_env(monkeypatch) -> None:
|
|
"""OPENAI_API_KEY из окружения мапится в settings.openai_api_key (fake-ключ)."""
|
|
monkeypatch.setenv("OPENAI_API_KEY", "test-fake-key-not-real")
|
|
s = Settings(_env_file=None) # type: ignore[call-arg]
|
|
assert s.openai_api_key == "test-fake-key-not-real"
|