172 lines
5.2 KiB
Python
172 lines
5.2 KiB
Python
"""Two independently configured OpenAI-compatible providers: generate vs detect."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from urllib.parse import urlparse
|
|
|
|
import httpx
|
|
|
|
from env_loader import load_env_file
|
|
|
|
load_env_file()
|
|
|
|
|
|
class ProviderError(Exception):
|
|
def __init__(self, code: str, message: str, status_code: int = 503):
|
|
super().__init__(message)
|
|
self.code = code
|
|
self.message = message
|
|
self.status_code = status_code
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProviderConfig:
|
|
role: str
|
|
name: str
|
|
mode: str
|
|
url: str
|
|
model: str
|
|
key: str
|
|
local: bool
|
|
zdr: bool
|
|
no_train: bool
|
|
|
|
|
|
def _truthy(name: str) -> bool:
|
|
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def _is_local_url(url: str) -> bool:
|
|
host = (urlparse(url).hostname or "").lower()
|
|
return host in {"localhost", "127.0.0.1", "::1", "ollama"}
|
|
|
|
|
|
def _setting(role: str) -> dict:
|
|
try:
|
|
from provider_settings import get_setting
|
|
|
|
return get_setting(role) or {}
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def _text(env_name: str, db_value: str | None, default: str = "") -> str:
|
|
env = (os.environ.get(env_name) or "").strip()
|
|
if env:
|
|
return env
|
|
if db_value:
|
|
return str(db_value).strip()
|
|
return default
|
|
|
|
|
|
def _flag(env_name: str, db_value, default: bool = True) -> bool:
|
|
if (os.environ.get(env_name) or "").strip():
|
|
return _truthy(env_name)
|
|
if db_value is None:
|
|
return default
|
|
return bool(int(db_value) if not isinstance(db_value, bool) else db_value)
|
|
|
|
|
|
def generate_provider() -> ProviderConfig | None:
|
|
if _truthy("KANSHO_FAKE_PROVIDER"):
|
|
return ProviderConfig(
|
|
role="generate",
|
|
name="fake",
|
|
mode="fake",
|
|
url="",
|
|
model="fake",
|
|
key="",
|
|
local=True,
|
|
zdr=True,
|
|
no_train=True,
|
|
)
|
|
row = _setting("generate")
|
|
key = (os.environ.get("KANSHO_PROVIDER_KEY") or "").strip()
|
|
url = _text("KANSHO_PROVIDER_URL", row.get("url"), "https://openrouter.ai/api/v1/chat/completions")
|
|
model = _text("KANSHO_PROVIDER_MODEL", row.get("model"), "openai/gpt-4o-mini")
|
|
local = _is_local_url(url)
|
|
zdr = local or _flag("KANSHO_PROVIDER_ZDR", row.get("zdr"), True)
|
|
no_train = local or _flag("KANSHO_PROVIDER_NO_TRAIN", row.get("no_train"), True)
|
|
if not local and not key:
|
|
return None
|
|
if not local and (not zdr or not no_train):
|
|
return None
|
|
return ProviderConfig(
|
|
role="generate",
|
|
name=row.get("name") or ("ollama" if local else "openrouter"),
|
|
mode="http",
|
|
url=url,
|
|
model=model,
|
|
key=key,
|
|
local=local,
|
|
zdr=zdr,
|
|
no_train=no_train,
|
|
)
|
|
|
|
|
|
def detect_provider() -> ProviderConfig | None:
|
|
if _truthy("KANSHO_FAKE_DETECT"):
|
|
return ProviderConfig(
|
|
role="detect",
|
|
name="fake-detect",
|
|
mode="fake",
|
|
url="",
|
|
model="fake",
|
|
key="",
|
|
local=True,
|
|
zdr=True,
|
|
no_train=True,
|
|
)
|
|
row = _setting("detect")
|
|
url = _text("KANSHO_DETECT_PROVIDER_URL", row.get("url"), "")
|
|
if not url:
|
|
return None
|
|
model = _text("KANSHO_DETECT_PROVIDER_MODEL", row.get("model"), "openai/gpt-4.1-nano")
|
|
key = (os.environ.get("KANSHO_DETECT_PROVIDER_KEY") or "").strip()
|
|
local = _is_local_url(url)
|
|
if not key and not local:
|
|
key = (os.environ.get("KANSHO_PROVIDER_KEY") or "").strip()
|
|
zdr = local or _flag("KANSHO_DETECT_ZDR", row.get("zdr"), True)
|
|
no_train = local or _flag("KANSHO_DETECT_NO_TRAIN", row.get("no_train"), True)
|
|
if not local and not key:
|
|
return None
|
|
if not local and (not zdr or not no_train):
|
|
return None
|
|
return ProviderConfig(
|
|
role="detect",
|
|
name=row.get("name") or ("ollama" if local else "openrouter"),
|
|
mode="http",
|
|
url=url,
|
|
model=model,
|
|
key=key,
|
|
local=local,
|
|
zdr=zdr,
|
|
no_train=no_train,
|
|
)
|
|
|
|
|
|
def complete_chat(config: ProviderConfig, messages: list[dict], *, timeout: float, max_tokens: int | None = None) -> str:
|
|
payload: dict = {"model": config.model, "messages": messages}
|
|
if max_tokens is not None:
|
|
payload["max_tokens"] = max_tokens
|
|
if not config.local:
|
|
payload["provider"] = {"data_collection": "deny"}
|
|
headers = {"Content-Type": "application/json"}
|
|
if config.key:
|
|
headers["Authorization"] = f"Bearer {config.key}"
|
|
if not config.local:
|
|
headers["HTTP-Referer"] = "https://kansho.local"
|
|
headers["X-Title"] = "Kansho"
|
|
try:
|
|
response = httpx.post(config.url, json=payload, headers=headers, timeout=timeout)
|
|
except httpx.HTTPError as exc:
|
|
raise ProviderError("provider_unreachable", f"{config.role}-Provider ist nicht erreichbar.") from exc
|
|
if response.status_code >= 400:
|
|
raise ProviderError("provider_rejected", f"{config.role}-Provider hat die Anfrage abgelehnt.")
|
|
data = response.json()
|
|
try:
|
|
return data["choices"][0]["message"]["content"]
|
|
except (KeyError, IndexError, TypeError) as exc:
|
|
raise ProviderError("provider_shape", f"Unerwartete {config.role}-Antwort.") from exc
|