"""Фейковый OpenAI-совместимый сервер: отдаёт заранее заданные ответы стримом.""" from __future__ import annotations import json import threading from dataclasses import dataclass, field from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Any @dataclass class Reply: text: str = "" # (имя, аргументы JSON); аргументы отдаются несколькими кусками, как у настоящих серверов tool_calls: list[tuple[str, str]] = field(default_factory=list) status: int = 200 error: str = "" class FakeLLMServer: def __init__(self) -> None: self.replies: list[Reply] = [] self.requests: list[dict[str, Any]] = [] server = self class Handler(BaseHTTPRequestHandler): def log_message(self, *args: object) -> None: pass def do_POST(self) -> None: body = json.loads(self.rfile.read(int(self.headers["Content-Length"]))) server.requests.append(body) reply = server.replies.pop(0) if server.replies else Reply(text="") if reply.status != 200: payload = json.dumps({"error": {"message": reply.error}}).encode() self.send_response(reply.status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) return self.send_response(200) self.send_header("Content-Type", "text/event-stream") self.end_headers() for delta in _deltas(reply): chunk = { "id": "fake", "object": "chat.completion.chunk", "created": 0, "model": body["model"], "choices": [{"index": 0, "delta": delta, "finish_reason": None}], } self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode()) self.wfile.write(b"data: [DONE]\n\n") self._server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) threading.Thread(target=self._server.serve_forever, daemon=True).start() @property def base_url(self) -> str: return f"http://127.0.0.1:{self._server.server_port}/v1" def close(self) -> None: self._server.shutdown() self._server.server_close() def _deltas(reply: Reply) -> list[dict[str, Any]]: deltas: list[dict[str, Any]] = [] for start in range(0, len(reply.text), 5): deltas.append({"content": reply.text[start : start + 5]}) for index, (name, arguments) in enumerate(reply.tool_calls): middle = len(arguments) // 2 deltas.append( { "tool_calls": [ { "index": index, "id": f"call_{index}", "type": "function", "function": {"name": name, "arguments": arguments[:middle]}, } ] } ) deltas.append( {"tool_calls": [{"index": index, "function": {"arguments": arguments[middle:]}}]} ) return deltas