Raju2024 commited on
Commit
bfcfdb7
·
verified ·
1 Parent(s): 7f32bff

Update puter_server.py

Browse files
Files changed (1) hide show
  1. puter_server.py +358 -102
puter_server.py CHANGED
@@ -1,134 +1,390 @@
1
- from fastapi import FastAPI, Request
2
- from fastapi.responses import JSONResponse, StreamingResponse
3
- import requests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import json
 
5
  import uuid
6
- from typing import Dict, Any, List
 
7
 
8
- from config import (
9
- PUTER_API_URL,
10
- PUTER_HEADERS,
11
- PUTER_AUTH_BEARER,
12
- MODEL_MAPPING,
13
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- app = FastAPI()
 
16
 
 
 
17
 
18
- # ---------- Helpers ----------
19
 
20
- def build_puter_payload(openai_req: Dict[str, Any]) -> Dict[str, Any]:
21
- messages = [{"content": m["content"]} for m in openai_req["messages"]]
 
 
 
 
 
 
22
 
23
- model_key = openai_req.get("model", "default")
24
- mapping = MODEL_MAPPING.get(model_key, MODEL_MAPPING["default"])
 
 
 
 
 
 
 
 
25
 
26
- args: Dict[str, Any] = {
27
- "model": mapping["puter_model"],
28
- "messages": messages,
29
- "stream": True,
30
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- # Forward OpenAI-compatible params
33
- for key in [
34
- "temperature",
35
- "max_tokens",
36
- "top_p",
37
- "stop",
38
- "presence_penalty",
39
- "frequency_penalty",
40
- "user",
41
- ]:
42
- if key in openai_req:
43
- args[key] = openai_req[key]
44
-
45
- return {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  "interface": "puter-chat-completion",
47
- "driver": mapping["driver"],
48
- "method": "complete",
49
  "test_mode": False,
50
- "args": args,
 
 
 
 
 
51
  }
 
52
 
53
 
54
- def stream_to_openai(resp):
55
- completion_id = f"chatcmpl-{uuid.uuid4().hex}"
 
 
56
 
57
- for line in resp.iter_lines():
58
- if not line:
59
- continue
60
 
61
- data = json.loads(line.decode("utf-8"))
 
62
 
63
- token = data.get("text")
64
- if not token:
65
- continue
66
 
67
- chunk = {
68
- "id": completion_id,
69
- "object": "chat.completion.chunk",
70
- "choices": [
71
- {
72
- "delta": {"content": token},
73
- "index": 0,
74
- "finish_reason": None,
75
- }
76
- ],
77
- }
78
- yield f"data: {json.dumps(chunk)}\n\n"
79
 
80
- yield "data: [DONE]\n\n"
 
 
 
 
 
 
 
 
 
 
81
 
 
 
 
82
 
83
- # ---------- Routes ----------
84
 
85
- @app.post("/v1/chat/completions")
86
- async def chat_completions(req: Request):
87
- openai_req = await req.json()
88
- stream = openai_req.get("stream", False)
 
 
 
 
89
 
90
- payload = build_puter_payload(openai_req)
 
 
 
 
 
 
 
91
 
92
- headers = {
93
- **PUTER_HEADERS,
94
- "authorization": f"Bearer {PUTER_AUTH_BEARER}",
95
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- resp = requests.post(
98
- PUTER_API_URL,
99
- headers=headers,
100
- json=payload,
101
- stream=True,
102
- timeout=300,
103
- )
104
 
105
- if stream:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  return StreamingResponse(
107
- stream_to_openai(resp),
108
  media_type="text/event-stream",
 
 
 
 
 
 
 
109
  )
110
 
111
- # Non-stream response aggregation
112
- full_text = ""
113
- for line in resp.iter_lines():
114
- if not line:
115
- continue
116
- data = json.loads(line.decode("utf-8"))
117
- full_text += data.get("text", "")
118
-
119
- return JSONResponse(
120
- {
121
- "id": f"chatcmpl-{uuid.uuid4().hex}",
122
- "object": "chat.completion",
123
- "choices": [
124
- {
125
- "index": 0,
126
- "message": {
127
- "role": "assistant",
128
- "content": full_text,
129
- },
130
- "finish_reason": "stop",
131
- }
132
- ],
133
- }
134
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Puter.com Reverse OpenAI-Compatible API Server
4
+
5
+ Accepts OpenAI Chat Completions requests and forwards them to:
6
+ POST https://api.puter.com/drivers/call
7
+
8
+ with payload:
9
+ {
10
+ "interface": "puter-chat-completion",
11
+ "driver": "xai",
12
+ "test_mode": false,
13
+ "method": "complete",
14
+ "args": {
15
+ "messages": [{"content": "..."}],
16
+ "model": "x-ai/grok-4.1-fast",
17
+ "stream": true
18
+ }
19
+ }
20
+ """
21
  import json
22
+ import time
23
  import uuid
24
+ import logging
25
+ from typing import Any, Dict, List, Optional, Union, AsyncGenerator
26
 
27
+ import requests
28
+ from fastapi import FastAPI, HTTPException, Request
29
+ from fastapi.middleware.cors import CORSMiddleware
30
+ from fastapi.responses import StreamingResponse, JSONResponse
31
+ from pydantic import BaseModel, Field
32
+
33
+ try:
34
+ from .config import (
35
+ PUTER_HEADERS,
36
+ PUTER_AUTH_BEARER,
37
+ SERVER_CONFIG,
38
+ MODEL_MAPPING,
39
+ )
40
+ except ImportError:
41
+ from config import (
42
+ PUTER_HEADERS,
43
+ PUTER_AUTH_BEARER,
44
+ SERVER_CONFIG,
45
+ MODEL_MAPPING,
46
+ )
47
 
48
+ logger = logging.getLogger(__name__)
49
+ logging.basicConfig(level=logging.INFO)
50
 
51
+ PUTER_URL = "https://api.puter.com/drivers/call"
52
+ REQUEST_TIMEOUT = 120
53
 
 
54
 
55
+ # ===== OpenAI-compatible models =====
56
+ class OpenAIMessage(BaseModel):
57
+ role: Optional[str] = Field(default=None, description="Role")
58
+ content: Optional[Union[str, List[Dict[str, Any]]]] = None
59
+ name: Optional[str] = None
60
+ function_call: Optional[Dict[str, Any]] = None
61
+ tool_calls: Optional[List[Dict[str, Any]]] = None
62
+ tool_call_id: Optional[str] = None
63
 
64
+ def get_text(self) -> str:
65
+ if isinstance(self.content, str):
66
+ return self.content
67
+ if isinstance(self.content, list):
68
+ parts: List[str] = []
69
+ for item in self.content:
70
+ if isinstance(item, dict) and item.get("type") == "text":
71
+ parts.append(item.get("text", ""))
72
+ return "".join(parts)
73
+ return str(self.content) if self.content is not None else ""
74
 
75
+ class Config:
76
+ extra = "allow"
77
+
78
+
79
+ class OpenAIFunction(BaseModel):
80
+ name: str
81
+ description: Optional[str] = None
82
+ parameters: Optional[Dict[str, Any]] = None
83
+
84
+ class Config:
85
+ extra = "allow"
86
+
87
+
88
+ class OpenAITool(BaseModel):
89
+ type: str = Field(default="function")
90
+ function: Optional[OpenAIFunction] = None
91
+
92
+ class Config:
93
+ extra = "allow"
94
 
95
+
96
+ class OpenAIChatRequest(BaseModel):
97
+ model: str
98
+ messages: List[OpenAIMessage]
99
+ max_tokens: Optional[int] = None
100
+ temperature: Optional[float] = None
101
+ top_p: Optional[float] = None
102
+ n: Optional[int] = 1
103
+ stream: Optional[bool] = False
104
+ stop: Optional[Union[str, List[str]]] = None
105
+ presence_penalty: Optional[float] = None
106
+ frequency_penalty: Optional[float] = None
107
+ logit_bias: Optional[Dict[str, float]] = None
108
+ user: Optional[str] = None
109
+ tools: Optional[List[OpenAITool]] = None
110
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None
111
+ functions: Optional[List[OpenAIFunction]] = None
112
+ function_call: Optional[Union[str, Dict[str, Any]]] = None
113
+
114
+ class Config:
115
+ extra = "allow"
116
+
117
+
118
+ class OpenAIChoice(BaseModel):
119
+ index: int = 0
120
+ message: Dict[str, Any]
121
+ finish_reason: Optional[str] = None
122
+
123
+
124
+ class OpenAIChatResponse(BaseModel):
125
+ id: str
126
+ object: str = "chat.completion"
127
+ created: int
128
+ model: str
129
+ choices: List[OpenAIChoice]
130
+ usage: Optional[Dict[str, int]] = None
131
+
132
+
133
+ class OpenAIStreamChoice(BaseModel):
134
+ index: int = 0
135
+ delta: Dict[str, Any]
136
+ finish_reason: Optional[str] = None
137
+
138
+
139
+ class OpenAIStreamChunk(BaseModel):
140
+ id: str
141
+ object: str = "chat.completion.chunk"
142
+ created: int
143
+ model: str
144
+ choices: List[OpenAIStreamChoice]
145
+
146
+
147
+ def _build_puter_payload(openai_req: OpenAIChatRequest) -> Dict[str, Any]:
148
+ # Map OpenAI messages to Puter format: only 'content' is used
149
+ mapped_messages: List[Dict[str, str]] = []
150
+ for m in openai_req.messages:
151
+ txt = m.get_text()
152
+ mapped_messages.append({"content": txt})
153
+
154
+ # Model mapping: map OpenAI model key -> (driver, puter_model)
155
+ mapping = MODEL_MAPPING.get(openai_req.model) or MODEL_MAPPING.get("default")
156
+ driver = mapping["driver"]
157
+ puter_model = mapping["puter_model"]
158
+
159
+ payload: Dict[str, Any] = {
160
  "interface": "puter-chat-completion",
161
+ "driver": driver,
 
162
  "test_mode": False,
163
+ "method": "complete",
164
+ "args": {
165
+ "messages": mapped_messages,
166
+ "model": puter_model,
167
+ "stream": True, # always request streaming upstream; we aggregate if needed
168
+ },
169
  }
170
+ return payload
171
 
172
 
173
+ def _headers_with_auth() -> Dict[str, str]:
174
+ h = dict(PUTER_HEADERS)
175
+ h["authorization"] = f"Bearer {PuterAuth.token}"
176
+ return h
177
 
 
 
 
178
 
179
+ class PuterAuth:
180
+ token: str = PUTER_AUTH_BEARER
181
 
 
 
 
182
 
183
+ async def _stream_openai_chunks(openai_req: OpenAIChatRequest, request_id: str) -> AsyncGenerator[str, None]:
184
+ headers = _headers_with_auth()
185
+ payload = _build_puter_payload(openai_req)
 
 
 
 
 
 
 
 
 
186
 
187
+ with requests.Session() as sess:
188
+ try:
189
+ resp = sess.post(
190
+ PUTER_URL,
191
+ headers=headers,
192
+ json=payload,
193
+ stream=True,
194
+ timeout=REQUEST_TIMEOUT,
195
+ )
196
+ except requests.RequestException as e:
197
+ raise HTTPException(status_code=502, detail=f"Upstream connection error: {e}")
198
 
199
+ if resp.status_code != 200:
200
+ detail = resp.text[:500]
201
+ raise HTTPException(status_code=502, detail=f"Upstream error {resp.status_code}: {detail}")
202
 
203
+ created = int(time.time())
204
 
205
+ # Initial role chunk
206
+ initial = OpenAIStreamChunk(
207
+ id=request_id,
208
+ created=created,
209
+ model=openai_req.model,
210
+ choices=[OpenAIStreamChoice(index=0, delta={"role": "assistant"}, finish_reason=None)],
211
+ )
212
+ yield f"data: {initial.model_dump_json()}\n\n"
213
 
214
+ # Stream content
215
+ for raw in resp.iter_lines():
216
+ if not raw:
217
+ continue
218
+ try:
219
+ line = raw.decode("utf-8", errors="ignore")
220
+ except Exception:
221
+ continue
222
 
223
+ text_piece: Optional[str] = None
224
+ # Many APIs stream JSON lines; try to parse
225
+ try:
226
+ obj = json.loads(line)
227
+ # Common keys
228
+ for k in ("delta", "text", "content", "output"):
229
+ if isinstance(obj.get(k), str) and obj.get(k):
230
+ text_piece = obj.get(k)
231
+ break
232
+ except Exception:
233
+ # Fallback to raw text
234
+ if line and line != "[DONE]":
235
+ text_piece = line
236
+
237
+ if not text_piece:
238
+ continue
239
+
240
+ chunk = OpenAIStreamChunk(
241
+ id=request_id,
242
+ created=created,
243
+ model=openai_req.model,
244
+ choices=[OpenAIStreamChoice(index=0, delta={"content": text_piece}, finish_reason=None)],
245
+ )
246
+ yield f"data: {chunk.model_dump_json()}\n\n"
247
+
248
+ final = OpenAIStreamChunk(
249
+ id=request_id,
250
+ created=created,
251
+ model=openai_req.model,
252
+ choices=[OpenAIStreamChoice(index=0, delta={}, finish_reason="stop")],
253
+ )
254
+ yield f"data: {final.model_dump_json()}\n\n"
255
+ yield "data: [DONE]\n\n"
256
 
 
 
 
 
 
 
 
257
 
258
+ def _complete_non_streaming(openai_req: OpenAIChatRequest) -> str:
259
+ headers = _headers_with_auth()
260
+ payload = _build_puter_payload(openai_req)
261
+ payload["args"]["stream"] = True
262
+
263
+ with requests.Session() as sess:
264
+ try:
265
+ resp = sess.post(
266
+ PUTER_URL,
267
+ headers=headers,
268
+ json=payload,
269
+ stream=True,
270
+ timeout=REQUEST_TIMEOUT,
271
+ )
272
+ except requests.RequestException as e:
273
+ raise HTTPException(status_code=502, detail=f"Upstream connection error: {e}")
274
+
275
+ if resp.status_code != 200:
276
+ detail = resp.text[:500]
277
+ raise HTTPException(status_code=502, detail=f"Upstream error {resp.status_code}: {detail}")
278
+
279
+ parts: List[str] = []
280
+ for raw in resp.iter_lines():
281
+ if not raw:
282
+ continue
283
+ try:
284
+ line = raw.decode("utf-8", errors="ignore")
285
+ except Exception:
286
+ continue
287
+ try:
288
+ obj = json.loads(line)
289
+ for k in ("delta", "text", "content", "output"):
290
+ if isinstance(obj.get(k), str) and obj.get(k):
291
+ parts.append(obj.get(k))
292
+ break
293
+ except Exception:
294
+ if line and line != "[DONE]":
295
+ parts.append(line)
296
+ return "".join(parts)
297
+
298
+
299
+ # ===== FastAPI app =====
300
+ app = FastAPI(
301
+ title="Puter Reverse OpenAI API",
302
+ version="1.0.0",
303
+ description="OpenAI-compatible API proxying to api.puter.com"
304
+ )
305
+
306
+ app.add_middleware(
307
+ CORSMiddleware,
308
+ allow_origins=["*"],
309
+ allow_credentials=True,
310
+ allow_methods=["*"],
311
+ allow_headers=["*"],
312
+ )
313
+
314
+
315
+ @app.get("/")
316
+ async def root():
317
+ return {"message": "Puter Reverse OpenAI API", "status": "running", "version": "1.0.0"}
318
+
319
+
320
+ @app.get("/health")
321
+ async def health():
322
+ return {"status": "healthy", "timestamp": int(time.time())}
323
+
324
+
325
+ @app.get("/v1/models")
326
+ async def models():
327
+ created = int(time.time())
328
+ data = []
329
+ for key in [k for k in MODEL_MAPPING.keys() if k != "default"]:
330
+ data.append({"id": key, "object": "model", "created": created, "owned_by": "puter"})
331
+ if not data:
332
+ data.append({"id": "x-ai/grok-4.1-fast", "object": "model", "created": created, "owned_by": "puter"})
333
+ return {"object": "list", "data": data}
334
+
335
+
336
+ @app.post("/v1/chat/completions")
337
+ async def chat(request: OpenAIChatRequest):
338
+ req_id = f"chatcmpl-{uuid.uuid4().hex[:12]}"
339
+ logger.info(f"[{req_id}] model={request.model}, stream={bool(request.stream)}")
340
+
341
+ if bool(request.stream):
342
  return StreamingResponse(
343
+ _stream_openai_chunks(request, req_id),
344
  media_type="text/event-stream",
345
+ headers={
346
+ "Cache-Control": "no-cache",
347
+ "Connection": "keep-alive",
348
+ "X-Accel-Buffering": "no",
349
+ "Access-Control-Allow-Origin": "*",
350
+ "Access-Control-Allow-Headers": "*",
351
+ },
352
  )
353
 
354
+ content = _complete_non_streaming(request)
355
+ created = int(time.time())
356
+ response = OpenAIChatResponse(
357
+ id=req_id,
358
+ created=created,
359
+ model=request.model,
360
+ choices=[OpenAIChoice(index=0, message={"role": "assistant", "content": content}, finish_reason="stop")],
361
+ usage={
362
+ "prompt_tokens": len(" ".join([m.get_text() for m in request.messages]).split()),
363
+ "completion_tokens": len(content.split()),
364
+ "total_tokens": len(" ".join([m.get_text() for m in request.messages]).split()) + len(content.split()),
365
+ },
 
 
 
 
 
 
 
 
 
 
 
366
  )
367
+ return response
368
+
369
+
370
+ @app.post("/v1/chat/completions/raw")
371
+ async def raw(req: Request):
372
+ body = await req.body()
373
+ try:
374
+ obj = json.loads(body)
375
+ _ = OpenAIChatRequest(**obj)
376
+ return {"valid": True}
377
+ except Exception as e:
378
+ return JSONResponse(status_code=422, content={"valid": False, "error": str(e)})
379
+
380
+
381
+ if __name__ == "__main__":
382
+ try:
383
+ import uvicorn
384
+ host = SERVER_CONFIG.get("host", "0.0.0.0")
385
+ port = int(SERVER_CONFIG.get("port", 8781))
386
+ logger.info(f"Starting Puter Reverse API on {host}:{port}")
387
+ uvicorn.run(app, host=host, port=port, log_level="info")
388
+ except Exception as e:
389
+ logger.error(f"Failed to start server: {e}")
390
+