58 lines
3.0 KiB
Python
58 lines
3.0 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]) -> dict:
|
|
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': 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}
|
|
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
|