dp-zp-agent/tools/answers/model_client.py

62 lines
3.2 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