163ace4283
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m14s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 10m19s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 46s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 27s
build / build (push) Successful in 1m18s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m45s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 10m2s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 36s
Testy / Kontrola składni wszystkich warstw (push) Successful in 23s
Uzytkownik wybiera nie tylko dostawce, ale konkretny model — Fable czy Opus u Anthropica, gpt-4o-mini czy gpt-5 u OpenAI, cokolwiek ma pobrane lokalnie. - app/llm/catalog.py: podpowiedzi modeli per dostawca wraz z oknem kontekstu, nadpisywalne przez <DOSTAWCA>_MODELS; GET /llm/models wystawia je dla UI. - Pole modelu w UI jest TEKSTOWE z datalista, nie zamknietym <select> — konto moze miec dostep do modeli, o ktorych kod nie wie, a nowe wychodza szybciej, niz aktualizuje sie katalog. Puste pole = model domyslny dostawcy. - static/models.js: zmiana dostawcy przelacza podpowiedzi, podmienia placeholder na model domyslny i pokazuje okno kontekstu wybranego modelu. - build_provider(name, model) — model z zadania wygrywa nad konfiguracja. WAZNE (znalezione przy tescie e2e): liczenie budzetu „maksymalny kontekst modelu" szlo przez build_provider(), ktory WYMAGA klucza API — bez klucza budzet cicho spadal do wartosci zapasowej i byl identyczny dla wszystkich modeli Anthropic. Budzet zalezy wylacznie od okna kontekstu, wiec doszlo resolve_model(), ktore rozwiazuje nazwe modelu bez budowania dostawcy. Teraz budzet realnie sie rozni: Opus/Fable 3,48 mln znakow, Haiku 536 tys., gpt-4o-mini 438 tys., llama3.1 8 tys. Pewnosc danych w katalogu: modele Anthropic pochodza z oficjalnej dokumentacji API (okna i limity zgodne z limits.py); modele OpenAI to podpowiedzi, ktorych nie weryfikowalem; lokalne zaleza od tego, co masz pobrane. Testy: 174 passed / 1 skipped (logika) + 17 (prezentacja). Nowy test strukturalny pilnuje, ze KAZDE wywolanie w dol niesie wybrany model i dostawce — dokladnie ta klasa bledu zlapala brakujacy parametr przy horoskopie okresowym. Zweryfikowane w przegladarce: przelaczanie dostawcy podmienia liste modeli. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
4.9 KiB
Python
122 lines
4.9 KiB
Python
"""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.
|
|
|
|
Konfiguracja jest **per dostawca**, bo UI pozwala przełączać go przy każdym żądaniu.
|
|
Wspólne `LLM_*` nie wystarczy: ustawienie `LLM_BASE_URL` na lokalny model kierowałoby
|
|
tam także żądania do OpenAI, a `LLM_MODEL=llama3.1:8b` kazałoby Anthropic użyć modelu
|
|
llama. Dlatego każdy dostawca ma własny komplet zmiennych.
|
|
|
|
Zmienne środowiskowe:
|
|
LLM_PROVIDER local (domyślnie) | openai | anthropic — dostawca domyślny
|
|
LLM_TIMEOUT sekundy (domyślnie 120)
|
|
LLM_MAX_TOKENS limit długości odpowiedzi (domyślnie 2000)
|
|
|
|
<DOSTAWCA>_MODEL / _BASE_URL / _API_KEY — konfiguracja konkretnego dostawcy:
|
|
LOCAL_MODEL, LOCAL_BASE_URL (klucz zwykle zbędny)
|
|
OPENAI_MODEL, OPENAI_BASE_URL, OPENAI_API_KEY
|
|
ANTHROPIC_MODEL, ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY
|
|
|
|
Klucze WYŁĄCZNIE z sekretu — nigdy w repo, w UI ani w logach.
|
|
|
|
Zgodność wstecz: wspólne `LLM_MODEL` / `LLM_BASE_URL` / `LLM_API_KEY` nadal działają,
|
|
ale stosują się TYLKO do dostawcy domyślnego (LLM_PROVIDER) — czyli konfiguracja
|
|
instalacji jednodostawcowej zostaje nietknięta, a pozostali dostawcy jej nie dziedziczą.
|
|
"""
|
|
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",
|
|
# Opus 4.8 świadomie zamiast Sonnet 5: Sonnet uruchamia myślenie adaptacyjne,
|
|
# gdy pominąć parametr `thinking`, a jego tokeny liczą się do max_tokens —
|
|
# przy ciasnym limicie cała tura wychodziła jako samo myślenie z pustym
|
|
# tekstem. To była przyczyna pustych odpowiedzi na Anthropicu.
|
|
ANTHROPIC: "claude-opus-4-8",
|
|
}
|
|
_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 setting(provider: str, suffix: str, fallback: str = "") -> str:
|
|
"""Ustawienie dostawcy: <DOSTAWCA>_<SUFIKS> → LLM_<SUFIKS> → wbudowana domyślna.
|
|
|
|
Wspólne `LLM_*` stosuje się WYŁĄCZNIE do dostawcy domyślnego — inaczej adres
|
|
lokalnego modelu przejąłby żądania do chmury (i odwrotnie).
|
|
"""
|
|
specific = os.getenv(f"{provider.upper()}_{suffix}")
|
|
if specific:
|
|
return specific
|
|
if provider == default_provider_name():
|
|
generic = os.getenv(f"LLM_{suffix}")
|
|
if generic:
|
|
return generic
|
|
return fallback
|
|
|
|
|
|
def resolve_model(name: str | None = None, model: str | None = None) -> tuple[str, str]:
|
|
"""(dostawca, model) BEZ budowania dostawcy — czyli bez wymogu klucza API.
|
|
|
|
Rozmiar budżetu promptu zależy tylko od okna kontekstu modelu, więc nie może
|
|
zależeć od tego, czy klucz jest już skonfigurowany.
|
|
"""
|
|
provider = (name or default_provider_name()).lower()
|
|
if provider not in PROVIDERS:
|
|
provider = default_provider_name()
|
|
chosen = (model or "").strip() or setting(provider, "MODEL", _DEFAULT_MODEL[provider])
|
|
return provider, chosen
|
|
|
|
|
|
def build_provider(name: str | None = None, model: str | None = None) -> LLMProvider:
|
|
"""Dostawca modelu. `model` z żądania wygrywa nad konfiguracją — użytkownik
|
|
wybiera model w UI, a konfiguracja podaje tylko wartość domyślną."""
|
|
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 = (model or "").strip() or setting(name, "MODEL", _DEFAULT_MODEL[name])
|
|
base_url = setting(name, "BASE_URL", _DEFAULT_URL[name])
|
|
api_key = setting(name, "API_KEY")
|
|
|
|
if name in (OPENAI, ANTHROPIC) and not api_key:
|
|
raise LLMError(
|
|
f"Brak klucza dla dostawcy {name} — ustaw {name.upper()}_API_KEY "
|
|
f"(z sekretu). Model lokalny klucza nie wymaga."
|
|
)
|
|
if name == ANTHROPIC:
|
|
return AnthropicProvider(base_url, model, api_key, timeout())
|
|
if name == OPENAI:
|
|
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)
|