82 lines
3.4 KiB
Python
82 lines
3.4 KiB
Python
"""Provider adapter for verified delivery; guards stay independent of the model vendor."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
|
|
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
return None
|
|
|
|
|
|
def complete_with_configured_model(messages: list[dict], *, max_tokens: int | None = None) -> dict:
|
|
if max_tokens is not None and (type(max_tokens) is not int or not 1 <= max_tokens <= 16384):
|
|
raise ValueError("Invalid model output budget")
|
|
provider = os.getenv("ANSWER_PROVIDER", "openai_compatible").strip()
|
|
model = os.getenv("ANSWER_MODEL", "").strip()
|
|
if provider == "anthropic":
|
|
key = (os.getenv("ANSWER_API_KEY") or os.getenv("ANTHROPIC_API_KEY", "")).strip()
|
|
endpoint = os.getenv("ANSWER_LLM_URL", "https://api.anthropic.com/v1/messages").strip()
|
|
system = "\n\n".join(row["content"] for row in messages if row["role"] == "system")
|
|
body = {
|
|
"model": model,
|
|
"messages": [row for row in messages if row["role"] != "system"],
|
|
"max_tokens": max_tokens or int(os.getenv("ANSWER_MAX_TOKENS", "4096")),
|
|
"temperature": 0,
|
|
}
|
|
if system:
|
|
body["system"] = system
|
|
headers = {"x-api-key": key, "anthropic-version": "2023-06-01"}
|
|
elif provider == "openai_compatible":
|
|
# Compatibility for historical experiments; new installations use ANSWER_API_KEY.
|
|
legacy = os.getenv("OPENWEBUI_API_KEY", "").strip()
|
|
key = os.getenv("ANSWER_API_KEY", "").strip() or legacy
|
|
endpoint = os.getenv(
|
|
"ANSWER_LLM_URL", "https://ui.tukekemt.xyz/api/chat/completions" if legacy else ""
|
|
).strip()
|
|
model = model or ("model120-fast" if legacy else "")
|
|
body = {"model": model, "messages": messages, "temperature": 0, "stream": False}
|
|
if max_tokens is not None:
|
|
body["max_tokens"] = max_tokens
|
|
headers = {"Authorization": "Bearer " + key}
|
|
else:
|
|
raise RuntimeError("Unsupported answer provider")
|
|
parsed = urllib.parse.urlsplit(endpoint)
|
|
if (
|
|
not key
|
|
or not model
|
|
or any(c in key for c in "\r\n")
|
|
or parsed.scheme != "https"
|
|
or not parsed.hostname
|
|
or parsed.username is not None
|
|
or parsed.password is not None
|
|
or parsed.fragment
|
|
or parsed.query
|
|
or any(c.isspace() for c in endpoint)
|
|
):
|
|
raise RuntimeError("Answer model is not configured")
|
|
request = urllib.request.Request(
|
|
endpoint,
|
|
data=json.dumps(body, ensure_ascii=False).encode(),
|
|
headers={"Content-Type": "application/json", **headers},
|
|
)
|
|
with urllib.request.build_opener(NoRedirect()).open(request, timeout=120) as response:
|
|
data = response.read(2 * 1024 * 1024 + 1)
|
|
if len(data) > 2 * 1024 * 1024:
|
|
raise RuntimeError("Answer model output is too large")
|
|
value = json.loads(data)
|
|
if provider == "anthropic":
|
|
if value.get("stop_reason") != "end_turn":
|
|
raise RuntimeError("Incomplete answer model output")
|
|
text = "".join(
|
|
block["text"] for block in value.get("content", []) if block.get("type") == "text"
|
|
)
|
|
if not text:
|
|
raise RuntimeError("Missing answer model output")
|
|
return {"choices": [{"message": {"content": text}}]}
|
|
return value
|