| """OpenAI-compatible chat endpoint over the reasoning engine. |
| |
| Serves ``/v1/chat/completions`` and ``/v1/models`` so any OpenAI-API chat client can talk to a |
| hybrid checkpoint with its own sampler — the model cannot run under llama.cpp-family runtimes, |
| whose autoregressive decoding never matches the block-denoising objective. |
| |
| Clients resend the full message history on every request and carry no thinking notes, while the |
| training layout replaces messages outside the visible window with the ledger of notes taken on |
| them. The server therefore caches each turn's notes keyed by a hash of the exact history that |
| produced it: a client that resends history verbatim reconstructs the same ledger the playground |
| would hold. On a cache miss (server restart, edited history) the older turns simply contribute |
| nothing, which is the playground's restart behaviour as well. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import hashlib |
| import json |
| import threading |
| import time |
| import uuid |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
| from pathlib import Path |
|
|
| from diffusion_lm.claims import ( |
| KEEP_MESSAGES, |
| SYSTEM, |
| chat_prefix, |
| ledger_line, |
| ledger_notes, |
| merge_notes, |
| ) |
| from diffusion_lm.reasoning_playground import ReasoningEngine |
| from diffusion_lm.train import resolve_device |
|
|
| MAX_ANSWER_TOKENS = 384 |
| NOTE_CACHE_LIMIT = 4096 |
|
|
|
|
| def _turn_key(system: str, messages: list[dict[str, str]]) -> str: |
| payload = json.dumps( |
| [system] + [[m['role'], m['content']] for m in messages], |
| ensure_ascii=False, sort_keys=False, |
| ) |
| return hashlib.sha256(payload.encode('utf-8')).hexdigest() |
|
|
|
|
| class ChatService: |
| """One engine plus the note cache; generation is serialized on the GPU.""" |
|
|
| def __init__(self, engine: ReasoningEngine, model_id: str, keep_messages: int) -> None: |
| self.engine = engine |
| self.model_id = model_id |
| self.keep_messages = keep_messages |
| self.notes: dict[str, str] = {} |
| self.lock = threading.Lock() |
|
|
| def _attach_notes(self, system: str, messages: list[dict[str, str]]) -> list[dict[str, str]]: |
| attached = [] |
| for index, message in enumerate(messages): |
| entry = {'role': message['role'], 'content': message['content']} |
| if message['role'] == 'assistant': |
| note = self.notes.get(_turn_key(system, messages[: index + 1])) |
| if note: |
| entry['note'] = note |
| attached.append(entry) |
| return attached |
|
|
| def build_prefix(self, system: str, messages: list[dict[str, str]]) -> str: |
| attached = self._attach_notes(system, messages) |
| older = merge_notes(ledger_notes(attached, self.keep_messages)) |
| window = attached[max(0, len(attached) - self.keep_messages):] |
| return chat_prefix( |
| [{'role': m['role'], 'content': m['content']} for m in window], |
| system=system, extra=ledger_line(older), |
| ) |
|
|
| def remember(self, system: str, messages: list[dict[str, str]], |
| answer: str, note: str) -> None: |
| if not note: |
| return |
| if len(self.notes) >= NOTE_CACHE_LIMIT: |
| self.notes.clear() |
| turn = messages + [{'role': 'assistant', 'content': answer}] |
| self.notes[_turn_key(system, turn)] = note |
|
|
| def generate(self, system: str, messages: list[dict[str, str]], *, temperature: float, |
| top_p: float, max_tokens: int, seed: int): |
| """Yield ``(answer_so_far, note, done)``; the note arrives with the final snapshot.""" |
|
|
| prefix = self.build_prefix(system, messages) |
| blocks: list[tuple[int, str]] = [] |
| answer = '' |
| with self.lock: |
| for _, answer, _ in self.engine.stream_chat( |
| prefix, |
| temperature=temperature, |
| |
| |
| |
| steps_per_block=16, |
| max_answer_tokens=min(MAX_ANSWER_TOKENS, max_tokens), |
| top_p=top_p, |
| seed=seed, |
| blocks_out=blocks, |
| ): |
| yield answer, '', False |
| note = '; '.join(text for _, text in blocks if text) |
| self.remember(system, messages, answer.strip(), note) |
| yield answer, note, True |
|
|
|
|
| def _split_messages(raw: list[dict]) -> tuple[str, list[dict[str, str]]]: |
| system = SYSTEM |
| messages = [] |
| for message in raw: |
| role, content = message.get('role'), str(message.get('content') or '') |
| if role == 'system': |
| system = content or system |
| elif role in ('user', 'assistant'): |
| messages.append({'role': role, 'content': content}) |
| return system, messages |
|
|
|
|
| class Handler(BaseHTTPRequestHandler): |
| service: ChatService |
|
|
| def log_message(self, format: str, *args) -> None: |
| pass |
|
|
| def _json(self, status: int, body: dict) -> None: |
| data = json.dumps(body, ensure_ascii=False).encode('utf-8') |
| self.send_response(status) |
| self.send_header('Content-Type', 'application/json') |
| self.send_header('Content-Length', str(len(data))) |
| self.send_header('Access-Control-Allow-Origin', '*') |
| self.end_headers() |
| self.wfile.write(data) |
|
|
| def do_OPTIONS(self) -> None: |
| self.send_response(204) |
| self.send_header('Access-Control-Allow-Origin', '*') |
| self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') |
| self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization') |
| self.end_headers() |
|
|
| def do_GET(self) -> None: |
| if self.path.rstrip('/') in ('/v1/models', '/models'): |
| self._json(200, {'object': 'list', 'data': [ |
| {'id': self.service.model_id, 'object': 'model', 'owned_by': 'mini-mdlm'}, |
| ]}) |
| else: |
| self._json(404, {'error': 'not found'}) |
|
|
| def do_POST(self) -> None: |
| if self.path.rstrip('/') not in ('/v1/chat/completions', '/chat/completions'): |
| self._json(404, {'error': 'not found'}) |
| return |
| try: |
| length = int(self.headers.get('Content-Length', 0)) |
| request = json.loads(self.rfile.read(length)) |
| system, messages = _split_messages(request.get('messages') or []) |
| if not messages or messages[-1]['role'] != 'user': |
| raise ValueError('last message must be from the user') |
| except (ValueError, json.JSONDecodeError) as error: |
| self._json(400, {'error': {'message': str(error), 'type': 'invalid_request_error'}}) |
| return |
|
|
| temperature = float(request.get('temperature') or 0.8) |
| top_p = float(request.get('top_p') or 0.95) |
| max_tokens = int(request.get('max_tokens') or MAX_ANSWER_TOKENS) |
| seed = int(request.get('seed') or 0) |
| stream = bool(request.get('stream')) |
| completion_id = f'chatcmpl-{uuid.uuid4().hex[:24]}' |
| created = int(time.time()) |
|
|
| snapshots = self.service.generate( |
| system, messages, temperature=temperature, top_p=top_p, |
| max_tokens=max_tokens, seed=seed, |
| ) |
| if not stream: |
| answer = note = '' |
| for answer, note, _ in snapshots: |
| pass |
| message = {'role': 'assistant', 'content': answer.strip()} |
| if note: |
| message['reasoning_content'] = note |
| self._json(200, { |
| 'id': completion_id, 'object': 'chat.completion', 'created': created, |
| 'model': self.service.model_id, |
| 'choices': [{'index': 0, 'message': message, 'finish_reason': 'stop'}], |
| }) |
| return |
|
|
| self.send_response(200) |
| self.send_header('Content-Type', 'text/event-stream') |
| self.send_header('Cache-Control', 'no-cache') |
| self.send_header('Access-Control-Allow-Origin', '*') |
| self.end_headers() |
|
|
| def chunk(delta: dict, finish: str | None = None) -> bytes: |
| body = { |
| 'id': completion_id, 'object': 'chat.completion.chunk', 'created': created, |
| 'model': self.service.model_id, |
| 'choices': [{'index': 0, 'delta': delta, 'finish_reason': finish}], |
| } |
| return f'data: {json.dumps(body, ensure_ascii=False)}\n\n'.encode('utf-8') |
|
|
| try: |
| self.wfile.write(chunk({'role': 'assistant'})) |
| sent = '' |
| for answer, _, done in snapshots: |
| if len(answer) > len(sent): |
| self.wfile.write(chunk({'content': answer[len(sent):]})) |
| self.wfile.flush() |
| sent = answer |
| self.wfile.write(chunk({}, finish='stop')) |
| self.wfile.write(b'data: [DONE]\n\n') |
| except (BrokenPipeError, ConnectionResetError): |
| pass |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument('--checkpoint', type=Path, |
| default=Path('outputs/qwen06b-genchat-sft/inference-latest.pt')) |
| parser.add_argument('--tokenizer', type=Path, |
| default=Path('artifacts/tokenizer-qwen3-adaptive.json')) |
| parser.add_argument('--model-id', default=None, |
| help='name reported to clients; defaults to the checkpoint directory') |
| parser.add_argument('--keep-messages', type=int, default=KEEP_MESSAGES) |
| parser.add_argument('--host', default='127.0.0.1') |
| parser.add_argument('--port', type=int, default=7998) |
| parser.add_argument('--device', default='auto') |
| args = parser.parse_args() |
|
|
| device = resolve_device(args.device) |
| engine = ReasoningEngine(args.checkpoint, args.tokenizer, device) |
| if not engine.chat_ready: |
| raise SystemExit(f'{args.checkpoint} is not an adaptive hybrid over a ChatML tokenizer') |
| Handler.service = ChatService( |
| engine, args.model_id or args.checkpoint.parent.name, args.keep_messages, |
| ) |
| server = ThreadingHTTPServer((args.host, args.port), Handler) |
| print(f'serving {Handler.service.model_id} on http://{args.host}:{args.port}/v1') |
| server.serve_forever() |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|