devMartianLabs commited on
Commit
cee29ea
·
verified ·
1 Parent(s): 0c2439e

Update professional engineer-grade serve_openai_api.py (CLI chat + OpenAI server + capability tags)

Browse files
Files changed (1) hide show
  1. serve_openai_api.py +224 -0
serve_openai_api.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MarsNext Phobos-1 — OpenAI-Compatible REST API Server (`/v1/chat/completions` & `/v1/models`)
4
+ (`phobos-1/models/phobos_scratch_v1/serve_openai_api.py`)
5
+ ==============================================================================================
6
+ Zero-dependency HTTP server (`ThreadingHTTPServer`) exposing OpenAI-compatible endpoints:
7
+ - `GET /health` -> Server & model status
8
+ - `GET /v1/models` -> Lists `marsnext/Phobos-1`
9
+ - `POST /v1/chat/completions` -> Supports standard JSON (`stream: false`) and SSE (`stream: true`)
10
+ - `POST /v1/completions` -> Legacy prompt completion endpoint
11
+
12
+ Compatible with the official `openai` Python/TS SDK, `curl`, OpenWebUI, LangChain, and Cursor.
13
+ Strictly <= 220 lines (`RULE[user_global]`).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import time
21
+ import uuid
22
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
23
+ from pathlib import Path
24
+ from typing import Any, Dict, List
25
+
26
+ import torch
27
+
28
+ from chat_cli import (
29
+ load_phobos_engine,
30
+ resolve_default_checkpoint,
31
+ resolve_default_tokenizer,
32
+ sample_next_token,
33
+ )
34
+
35
+ MODEL_ID = "marsnext/Phobos-1"
36
+ _ENGINE: Dict[str, Any] = {}
37
+
38
+
39
+ def _format_messages(messages: List[Dict[str, str]], force_think: bool = False) -> str:
40
+ """Formats OpenAI chat messages into Phobos-1 `<|user|>` / `<|assistant|>` sequence."""
41
+ parts: List[str] = ["<|bos|>"]
42
+ sys_prompt = ""
43
+ for msg in messages:
44
+ role = msg.get("role", "user")
45
+ content = str(msg.get("content", "")).strip()
46
+ if role == "system":
47
+ sys_prompt = content
48
+ elif role == "user":
49
+ if sys_prompt:
50
+ content = f"{sys_prompt}\n\n{content}"
51
+ sys_prompt = ""
52
+ parts.append(f"<|user|>\n{content}\n")
53
+ elif role == "assistant":
54
+ parts.append(f"<|assistant|>\n{content}<|eos|>\n")
55
+ parts.append("<|assistant|>\n" + ("<think>\n" if force_think else ""))
56
+ return "".join(parts)
57
+
58
+
59
+ class PhobosOpenAIHandler(BaseHTTPRequestHandler):
60
+ """Handles OpenAI REST requests for `marsnext/Phobos-1`."""
61
+
62
+ def _send_json(self, code: int, payload: Dict[str, Any]) -> None:
63
+ raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
64
+ self.send_response(code)
65
+ self.send_header("Content-Type", "application/json; charset=utf-8")
66
+ self.send_header("Access-Control-Allow-Origin", "*")
67
+ self.send_header("Content-Length", str(len(raw)))
68
+ self.end_headers()
69
+ self.wfile.write(raw)
70
+
71
+ def do_OPTIONS(self) -> None: # noqa: N802
72
+ self.send_response(204)
73
+ self.send_header("Access-Control-Allow-Origin", "*")
74
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
75
+ self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
76
+ self.end_headers()
77
+
78
+ def do_GET(self) -> None: # noqa: N802
79
+ if self.path.startswith("/health"):
80
+ self._send_json(200, {"status": "ok", "model": MODEL_ID, "device": str(_ENGINE["device"])})
81
+ elif self.path.startswith("/v1/models"):
82
+ self._send_json(
83
+ 200,
84
+ {
85
+ "object": "list",
86
+ "data": [
87
+ {
88
+ "id": MODEL_ID,
89
+ "object": "model",
90
+ "created": int(time.time()),
91
+ "owned_by": "marsnext",
92
+ }
93
+ ],
94
+ },
95
+ )
96
+ else:
97
+ self._send_json(404, {"error": {"message": f"Unknown endpoint {self.path}"}})
98
+
99
+ def do_POST(self) -> None: # noqa: N802
100
+ if not (self.path.startswith("/v1/chat/completions") or self.path.startswith("/v1/completions")):
101
+ self._send_json(404, {"error": {"message": f"Unknown endpoint {self.path}"}})
102
+ return
103
+
104
+ length = int(self.headers.get("Content-Length", "0"))
105
+ body = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
106
+
107
+ max_tokens = int(body.get("max_tokens", 256))
108
+ temperature = float(body.get("temperature", 0.0))
109
+ top_p = float(body.get("top_p", 0.9))
110
+ rep_penalty = float(body.get("repetition_penalty", 1.15))
111
+ stream = bool(body.get("stream", False))
112
+ force_think = bool(body.get("think", False))
113
+
114
+ if "messages" in body:
115
+ formatted = _format_messages(body["messages"], force_think=force_think)
116
+ else:
117
+ prompt_str = str(body.get("prompt", "")).strip()
118
+ formatted = f"<|bos|><|user|>\n{prompt_str}\n<|assistant|>\n" + ("<think>\n" if force_think else "")
119
+
120
+ model = _ENGINE["model"]
121
+ tok = _ENGINE["tok"]
122
+ device = _ENGINE["device"]
123
+
124
+ input_ids = tok.encode(formatted, max_len=1536)
125
+ ids = torch.tensor([input_ids], dtype=torch.long, device=device)
126
+ gen_ids: List[int] = tok.encode("<think>\n", max_len=16) if force_think else []
127
+ req_id = f"chatcmpl-{uuid.uuid4().hex[:12]}"
128
+ created = int(time.time())
129
+
130
+ if stream:
131
+ self.send_response(200)
132
+ self.send_header("Content-Type", "text/event-stream; charset=utf-8")
133
+ self.send_header("Cache-Control", "no-cache")
134
+ self.send_header("Access-Control-Allow-Origin", "*")
135
+ self.end_headers()
136
+ prev_text = ""
137
+ with torch.no_grad():
138
+ for _ in range(max_tokens):
139
+ logits = model(ids[:, -2048:])["logits"][0, -1, :].float()
140
+ next_id = sample_next_token(logits, ids[0].tolist(), temperature, top_p, rep_penalty)
141
+ if next_id == tok.EOS_ID:
142
+ break
143
+ gen_ids.append(next_id)
144
+ ids = torch.cat([ids, torch.tensor([[next_id]], dtype=torch.long, device=device)], dim=1)
145
+ cur_text = tok.decode(gen_ids, skip_special_tokens=False)
146
+ delta = cur_text[len(prev_text):]
147
+ if delta:
148
+ prev_text = cur_text
149
+ chunk = {
150
+ "id": req_id,
151
+ "object": "chat.completion.chunk",
152
+ "created": created,
153
+ "model": MODEL_ID,
154
+ "choices": [{"index": 0, "delta": {"content": delta}, "finish_reason": None}],
155
+ }
156
+ self.wfile.write(f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode("utf-8"))
157
+ self.wfile.flush()
158
+ done_chunk = {
159
+ "id": req_id,
160
+ "object": "chat.completion.chunk",
161
+ "created": created,
162
+ "model": MODEL_ID,
163
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
164
+ }
165
+ self.wfile.write(f"data: {json.dumps(done_chunk)}\n\ndata: [DONE]\n\n".encode("utf-8"))
166
+ self.wfile.flush()
167
+ return
168
+
169
+ with torch.no_grad():
170
+ for _ in range(max_tokens):
171
+ logits = model(ids[:, -2048:])["logits"][0, -1, :].float()
172
+ next_id = sample_next_token(logits, ids[0].tolist(), temperature, top_p, rep_penalty)
173
+ if next_id == tok.EOS_ID:
174
+ break
175
+ gen_ids.append(next_id)
176
+ ids = torch.cat([ids, torch.tensor([[next_id]], dtype=torch.long, device=device)], dim=1)
177
+
178
+ reply_text = tok.decode(gen_ids, skip_special_tokens=False)
179
+ self._send_json(
180
+ 200,
181
+ {
182
+ "id": req_id,
183
+ "object": "chat.completion",
184
+ "created": created,
185
+ "model": MODEL_ID,
186
+ "choices": [
187
+ {
188
+ "index": 0,
189
+ "message": {"role": "assistant", "content": reply_text},
190
+ "finish_reason": "stop",
191
+ }
192
+ ],
193
+ "usage": {
194
+ "prompt_tokens": len(input_ids),
195
+ "completion_tokens": len(gen_ids),
196
+ "total_tokens": len(input_ids) + len(gen_ids),
197
+ },
198
+ },
199
+ )
200
+
201
+
202
+ def main() -> None:
203
+ parser = argparse.ArgumentParser(description="MarsNext Phobos-1 OpenAI-Compatible API Server")
204
+ parser.add_argument("--ckpt", "--weights", dest="ckpt", type=Path, default=None, help="Path to Phobos-1 .pt checkpoint")
205
+ parser.add_argument("--tokenizer", type=Path, default=None, help="Path to phobos_bpe_32k.json")
206
+ parser.add_argument("--device", type=str, default=None, help="Device: cuda, mps, or cpu")
207
+ parser.add_argument("--host", type=str, default="0.0.0.0", help="Bind host")
208
+ parser.add_argument("--port", type=int, default=8000, help="Bind port")
209
+ args = parser.parse_args()
210
+
211
+ ckpt_path = args.ckpt or resolve_default_checkpoint()
212
+ tok_path = args.tokenizer or resolve_default_tokenizer()
213
+ model, tok, device = load_phobos_engine(ckpt_path, tok_path, args.device)
214
+ _ENGINE["model"] = model
215
+ _ENGINE["tok"] = tok
216
+ _ENGINE["device"] = device
217
+
218
+ server = ThreadingHTTPServer((args.host, args.port), PhobosOpenAIHandler)
219
+ print(f"[MarsNext Phobos-1 OpenAI Server] Listening on http://{args.host}:{args.port}/v1 (device={device})", flush=True)
220
+ server.serve_forever()
221
+
222
+
223
+ if __name__ == "__main__":
224
+ main()