"""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