"""Wybór dostawcy LLM (LOG-31) — jedyne miejsce znające konkretne implementacje. Domyślny jest **model lokalny**: prompt niesie oryginalne opisy z baz, więc domyślnie nic nie opuszcza naszej sieci (LOG-32). Chmurę włącza się świadomie — przez konfigurację albo pojedyncze żądanie. Zmienne środowiskowe: LLM_PROVIDER local (domyślnie) | openai | anthropic LLM_MODEL nazwa modelu (domyślna zależy od dostawcy) LLM_BASE_URL adres API (domyślnie: lokalny serwer zgodny z OpenAI) LLM_API_KEY klucz — WYŁĄCZNIE z sekretu; niepotrzebny dla modelu lokalnego LLM_TIMEOUT sekundy (domyślnie 120) LLM_MAX_TOKENS limit długości odpowiedzi (domyślnie 2000) """ from __future__ import annotations import os from app.llm.base import LLMError, LLMProvider from app.llm.providers import AnthropicProvider, ChatCompletionsProvider LOCAL = "local" OPENAI = "openai" ANTHROPIC = "anthropic" PROVIDERS = (LOCAL, OPENAI, ANTHROPIC) _DEFAULT_MODEL = { LOCAL: "llama3.1:8b", OPENAI: "gpt-4o-mini", ANTHROPIC: "claude-sonnet-5", } _DEFAULT_URL = { # Ollama i vLLM wystawiają zgodne API pod /v1 LOCAL: "http://localhost:11434/v1", OPENAI: "https://api.openai.com/v1", ANTHROPIC: "https://api.anthropic.com", } def default_provider_name() -> str: return os.getenv("LLM_PROVIDER", LOCAL).lower() def max_tokens() -> int: return int(os.getenv("LLM_MAX_TOKENS", "2000")) def timeout() -> float: return float(os.getenv("LLM_TIMEOUT", "120")) def build_provider(name: str | None = None) -> LLMProvider: name = (name or default_provider_name()).lower() if name not in PROVIDERS: raise LLMError(f"Nieznany dostawca LLM: {name!r} (dostępne: {', '.join(PROVIDERS)})") model = os.getenv("LLM_MODEL") or _DEFAULT_MODEL[name] base_url = os.getenv("LLM_BASE_URL") or _DEFAULT_URL[name] api_key = os.getenv("LLM_API_KEY", "") if name == ANTHROPIC: return AnthropicProvider(base_url, model, api_key, timeout()) if name == OPENAI: if not api_key: raise LLMError("Brak LLM_API_KEY — dostawca openai wymaga klucza.") return ChatCompletionsProvider(OPENAI, base_url, model, api_key, timeout(), leaves_lan=True) # lokalny — klucz zwykle zbędny; treść NIE opuszcza sieci return ChatCompletionsProvider(LOCAL, base_url, model, api_key, timeout(), leaves_lan=False)