AIMaster7 commited on
Commit
565e834
·
verified ·
1 Parent(s): c3b4d73

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +216 -267
main.py CHANGED
@@ -7,17 +7,19 @@ import time
7
  from typing import List, Optional, Union, Any
8
  import httpx
9
  from dotenv import load_dotenv
10
- from fastapi import FastAPI, HTTPException
11
  from fastapi.responses import JSONResponse, StreamingResponse
12
- from pydantic import BaseModel, Field
13
 
14
  # --- Configuration ---
15
  load_dotenv()
 
16
  IMAGE_API_URL = os.environ.get("IMAGE_API_URL", "https://image.api.example.com")
17
  SNAPZION_UPLOAD_URL = "https://upload.snapzion.com/api/public-upload"
18
  SNAPZION_API_KEY = os.environ.get("SNAP", "")
19
 
20
  # --- Dummy Model Definitions ---
 
21
  AVAILABLE_MODELS = [
22
  {"id": "gpt-4-turbo", "object": "model", "created": int(time.time()), "owned_by": "system"},
23
  {"id": "gpt-4o", "object": "model", "created": int(time.time()), "owned_by": "system"},
@@ -36,56 +38,35 @@ app = FastAPI(
36
 
37
  # --- Helper Function for Random ID Generation ---
38
  def generate_random_id(prefix: str, length: int = 29) -> str:
 
 
 
39
  population = string.ascii_letters + string.digits
40
  random_part = "".join(secrets.choice(population) for _ in range(length))
41
  return f"{prefix}{random_part}"
42
 
43
- # === Tool Call Models ===
44
- class FunctionCall(BaseModel):
45
- name: str
46
- arguments: str
47
-
48
- class ToolCall(BaseModel):
49
- id: str
50
- type: str = "function"
51
- function: FunctionCall
52
-
53
- # === Message Models ===
54
- class Message(BaseModel):
55
- role: str
56
- content: Optional[str] = None
57
- name: Optional[str] = None
58
- tool_calls: Optional[List[ToolCall]] = None
59
- tool_call_id: Optional[str] = None
60
-
61
  # === API Endpoints ===
62
  @app.get("/v1/models")
63
  async def list_models():
 
64
  return {"object": "list", "data": AVAILABLE_MODELS}
65
 
66
  # === Chat Completion ===
 
 
 
 
67
  class ChatRequest(BaseModel):
68
  messages: List[Message]
69
  model: str
70
  stream: Optional[bool] = False
71
  tools: Optional[Any] = None
72
 
73
- def build_tool_prompt(tools: List[Any]) -> str:
74
- tool_definitions = "\n".join([
75
- f"- {tool['function']['name']}: {tool['function'].get('description', 'No description available')}"
76
- for tool in tools
77
- ])
78
- return f"""You have access to tools. To call a tool, respond with JSON inside <tool_call></tool_call> tags.
79
- Available Tools:
80
- {tool_definitions}
81
-
82
- Response Format:
83
- <tool_call>
84
- {{"name": "tool_name", "parameters": {{"arg1": "value1"}}}}
85
- </tool_call>"""
86
-
87
  @app.post("/v1/chat/completions")
88
  async def chat_completion(request: ChatRequest):
 
 
 
89
  model_id = MODEL_ALIASES.get(request.model, request.model)
90
  chat_id = generate_random_id("chatcmpl-")
91
  headers = {
@@ -95,189 +76,191 @@ async def chat_completion(request: ChatRequest):
95
  'referer': 'https://www.chatwithmono.xyz/',
96
  'user-agent': 'Mozilla/5.0',
97
  }
98
-
99
- # Handle tool definitions
100
  if request.tools:
101
- tool_prompt = build_tool_prompt(request.tools)
102
- if request.messages and request.messages[0].role == "system":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  request.messages[0].content += "\n\n" + tool_prompt
104
  else:
105
- request.messages.insert(0, Message(role="system", content=tool_prompt))
 
106
 
107
  payload = {
108
- "messages": [msg.model_dump(exclude_none=True) for msg in request.messages],
109
  "model": model_id
110
  }
111
-
112
- # Streaming response
113
  if request.stream:
114
  async def event_stream():
115
  created = int(time.time())
116
- chunks_buffer = []
117
- max_initial_chunks = 4
118
  is_tool_call = False
119
- tool_call_content = ""
120
-
121
  try:
122
  async with httpx.AsyncClient(timeout=120) as client:
123
- async with client.stream("POST", "https://www.chatwithmono.xyz/api/chat",
124
- headers=headers, json=payload) as response:
125
  response.raise_for_status()
126
  async for line in response.aiter_lines():
127
- if not line:
128
- continue
129
  if line.startswith("0:"):
130
  try:
131
  content_piece = json.loads(line[2:])
132
- # Buffer initial chunks
 
133
  if len(chunks_buffer) < max_initial_chunks:
134
  chunks_buffer.append(content_piece)
135
  continue
136
-
137
- # Check for tool call pattern
138
- if not is_tool_call:
139
- full_buffer = ''.join(chunks_buffer + [content_piece])
140
  if "<tool_call>" in full_buffer:
 
141
  is_tool_call = True
142
- tool_call_content = full_buffer
143
- chunks_buffer = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  else:
145
- # Send buffered chunks
146
- if chunks_buffer:
147
- delta = {"content": "".join(chunks_buffer)}
148
- if not tool_call_content: # Only add role in first chunk
149
- delta["role"] = "assistant"
150
- yield create_chunk(chat_id, created, model_id, delta)
151
- chunks_buffer = []
152
-
153
- # Send current chunk
154
- delta = {"content": content_piece}
155
- yield create_chunk(chat_id, created, model_id, delta)
156
  else:
157
- # Accumulate tool call content
158
- tool_call_content += content_piece
159
- if "</tool_call>" in tool_call_content:
160
- tool_call_str = tool_call_content.split("<tool_call>")[1].split("</tool_call>")[0].strip()
161
- try:
162
- tool_call_data = json.loads(tool_call_str)
163
- tool_call = ToolCall(
164
- id=generate_random_id("call_"),
165
- function=FunctionCall(
166
- name=tool_call_data["name"],
167
- arguments=json.dumps(tool_call_data.get("parameters", {}))
168
- )
169
- delta = {
170
- "content": None,
171
- "tool_calls": [tool_call.model_dump()]
172
- }
173
- yield create_chunk(chat_id, created, model_id, delta)
174
- is_tool_call = False
175
- tool_call_content = ""
176
- except (json.JSONDecodeError, KeyError) as e:
177
- print(f"Tool call parsing error: {e}")
178
- except json.JSONDecodeError:
179
- continue
180
-
181
  elif line.startswith(("e:", "d:")):
 
 
 
182
  break
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  except httpx.HTTPStatusError as e:
185
  error_content = {
186
  "error": {
187
- "message": f"Upstream error: {e.response.status_code}",
188
- "type": "upstream_error",
189
- "code": str(e.response.status_code)
190
  }
191
  }
192
  yield f"data: {json.dumps(error_content)}\n\n"
193
  finally:
194
- # Finish signal
195
- done_chunk = {
196
- "id": chat_id,
197
- "object": "chat.completion.chunk",
198
- "created": created,
199
- "model": model_id,
200
- "choices": [{
201
- "index": 0,
202
- "delta": {},
203
- "finish_reason": "stop"
204
- }]
205
- }
206
- yield f"data: {json.dumps(done_chunk)}\n\n"
207
  yield "data: [DONE]\n\n"
208
-
209
  return StreamingResponse(event_stream(), media_type="text/event-stream")
210
-
211
- # Non-streaming response
212
- else:
213
  try:
214
  async with httpx.AsyncClient(timeout=120) as client:
215
- response = await client.post(
216
- "https://www.chatwithmono.xyz/api/chat",
217
- headers=headers,
218
- json=payload
219
- )
220
- response.raise_for_status()
221
- content = ""
222
- for line in response.text.splitlines():
223
- if line.startswith("0:"):
224
- try:
225
- content += json.loads(line[2:])
226
- except json.JSONDecodeError:
227
- continue
228
 
229
- tool_calls = None
230
- if "<tool_call>" in content and "</tool_call>" in content:
231
- try:
232
- tool_call_str = content.split("<tool_call>")[1].split("</tool_call>")[0].strip()
233
- tool_call_data = json.loads(tool_call_str)
234
- tool_call = ToolCall(
235
- id=generate_random_id("call_"),
236
- function=FunctionCall(
237
- name=tool_call_data["name"],
238
- arguments=json.dumps(tool_call_data.get("parameters", {}))
239
- )
240
- tool_calls = [tool_call.model_dump()]
241
- content = None # Clear content for tool call
242
- except (json.JSONDecodeError, KeyError) as e:
243
- print(f"Tool call parsing error: {e}")
244
 
245
- return JSONResponse(content={
246
- "id": chat_id,
247
- "object": "chat.completion",
248
- "created": int(time.time()),
249
- "model": model_id,
250
- "choices": [{
251
- "index": 0,
252
- "message": {
253
- "role": "assistant",
254
- "content": content,
255
- "tool_calls": tool_calls
256
- },
257
- "finish_reason": "tool_calls" if tool_calls else "stop"
258
- }],
259
- "usage": {
260
- "prompt_tokens": 0,
261
- "completion_tokens": 0,
262
- "total_tokens": 0
263
- }
264
- })
265
-
266
  except httpx.HTTPStatusError as e:
267
- return JSONResponse(
268
- status_code=502,
269
- content={"error": {"message": f"Upstream error: {e.response.text}", "type": "upstream_error"}}
270
- )
271
 
272
- def create_chunk(chat_id: str, created: int, model: str, delta: dict) -> str:
273
- chunk = {
274
- "id": chat_id,
275
- "object": "chat.completion.chunk",
276
- "created": created,
277
- "model": model,
278
- "choices": [{"index": 0, "delta": delta}]
279
- }
280
- return f"data: {json.dumps(chunk)}\n\n"
281
 
282
  # === Image Generation ===
283
  class ImageGenerationRequest(BaseModel):
@@ -289,65 +272,40 @@ class ImageGenerationRequest(BaseModel):
289
 
290
  @app.post("/v1/images/generations")
291
  async def generate_images(request: ImageGenerationRequest):
 
292
  results = []
293
  try:
294
  async with httpx.AsyncClient(timeout=120) as client:
295
  for _ in range(request.n):
296
- if request.model in ["gpt-image-1", "dall-e-3", "dall-e-2", "nextlm-image-1"]:
297
- # Mono image generation
298
- resp = await client.post(
299
- "https://www.chatwithmono.xyz/api/image",
300
- json={"prompt": request.prompt, "model": request.model},
301
- headers={'Content-Type': 'application/json'}
302
- )
303
  resp.raise_for_status()
304
  data = resp.json()
305
  b64_image = data.get("image")
306
-
307
- if not b64_image:
308
- raise HTTPException(502, "Missing image in response")
309
-
310
- # Upload to Snapzion if API key available
311
  if SNAPZION_API_KEY:
312
- files = {'file': ('image.png', base64.b64decode(b64_image), 'image/png')}
313
- upload_resp = await client.post(
314
- SNAPZION_UPLOAD_URL,
315
- files=files,
316
- headers={"Authorization": SNAPZION_API_KEY}
317
- )
318
  upload_resp.raise_for_status()
319
- image_url = upload_resp.json().get("url")
 
320
  else:
321
  image_url = f"data:image/png;base64,{b64_image}"
322
-
323
- results.append({
324
- "url": image_url,
325
- "b64_json": b64_image,
326
- "revised_prompt": data.get("revised_prompt", request.prompt)
327
- })
328
  else:
329
- # Default image generation
330
- params = {"prompt": request.prompt, "aspect_ratio": request.aspect_ratio}
331
  resp = await client.get(IMAGE_API_URL, params=params)
332
  resp.raise_for_status()
333
  data = resp.json()
334
- results.append({
335
- "url": data.get("image_link"),
336
- "b64_json": data.get("base64_output"),
337
- "revised_prompt": request.prompt
338
- })
339
-
340
  except httpx.HTTPStatusError as e:
341
- return JSONResponse(
342
- status_code=502,
343
- content={"error": f"Image service error: {e.response.status_code}"}
344
- )
345
  except Exception as e:
346
- return JSONResponse(
347
- status_code=500,
348
- content={"error": f"Internal error: {str(e)}"}
349
- )
350
-
351
  return {"created": int(time.time()), "data": results}
352
 
353
  # === Moderation Endpoint ===
@@ -357,76 +315,67 @@ class ModerationRequest(BaseModel):
357
 
358
  @app.post("/v1/moderations")
359
  async def create_moderation(request: ModerationRequest):
 
 
 
 
360
  input_texts = [request.input] if isinstance(request.input, str) else request.input
361
  if not input_texts:
362
- return JSONResponse(
363
- status_code=400,
364
- content={"error": "At least one input string is required"}
365
- )
366
-
367
  headers = {
368
  'Content-Type': 'application/json',
369
- 'User-Agent': 'Mozilla/5.0',
370
  'Referer': 'https://www.chatwithmono.xyz/',
371
  }
372
-
373
  results = []
374
- async with httpx.AsyncClient(timeout=30) as client:
375
- for text in input_texts:
376
- try:
377
- resp = await client.post(
378
- "https://www.chatwithmono.xyz/api/moderation",
379
- json={"text": text},
380
- headers=headers
381
- )
382
  resp.raise_for_status()
383
- data = resp.json()
384
-
385
- # Transform to OpenAI format
386
- flagged = data.get("overall_sentiment") == "flagged"
387
- categories = data.get("categories", {})
388
  openai_categories = {
389
- "hate": categories.get("hate", False),
390
- "hate/threatening": False,
391
- "self-harm": categories.get("self-harm", False),
392
- "sexual": categories.get("sexual", False),
393
- "sexual/minors": False,
394
- "violence": categories.get("violence", False),
395
- "violence/graphic": False,
396
  }
397
-
 
398
  result_item = {
399
  "flagged": flagged,
400
  "categories": openai_categories,
401
- "category_scores": {k: 1.0 if v else 0.0 for k, v in openai_categories.items()}
402
  }
403
-
404
- # Add reason if available
405
- if "reason" in data:
406
- result_item["reason"] = data["reason"]
407
-
 
 
408
  results.append(result_item)
409
-
410
- except httpx.HTTPStatusError:
411
- results.append({
412
- "flagged": False,
413
- "categories": {k: False for k in [
414
- "hate", "hate/threatening", "self-harm",
415
- "sexual", "sexual/minors", "violence", "violence/graphic"
416
- ]},
417
- "category_scores": {k: 0.0 for k in [
418
- "hate", "hate/threatening", "self-harm",
419
- "sexual", "sexual/minors", "violence", "violence/graphic"
420
- ]}
421
- })
422
-
423
- return {
424
  "id": generate_random_id("modr-"),
425
  "model": request.model,
426
- "results": results
427
  }
 
428
 
429
  # --- Main Execution ---
430
  if __name__ == "__main__":
431
  import uvicorn
432
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
7
  from typing import List, Optional, Union, Any
8
  import httpx
9
  from dotenv import load_dotenv
10
+ from fastapi import FastAPI
11
  from fastapi.responses import JSONResponse, StreamingResponse
12
+ from pydantic import BaseModel
13
 
14
  # --- Configuration ---
15
  load_dotenv()
16
+ # Env variables for external services
17
  IMAGE_API_URL = os.environ.get("IMAGE_API_URL", "https://image.api.example.com")
18
  SNAPZION_UPLOAD_URL = "https://upload.snapzion.com/api/public-upload"
19
  SNAPZION_API_KEY = os.environ.get("SNAP", "")
20
 
21
  # --- Dummy Model Definitions ---
22
+ # In a real application, these would be defined properly.
23
  AVAILABLE_MODELS = [
24
  {"id": "gpt-4-turbo", "object": "model", "created": int(time.time()), "owned_by": "system"},
25
  {"id": "gpt-4o", "object": "model", "created": int(time.time()), "owned_by": "system"},
 
38
 
39
  # --- Helper Function for Random ID Generation ---
40
  def generate_random_id(prefix: str, length: int = 29) -> str:
41
+ """
42
+ Generates a cryptographically secure, random alphanumeric ID.
43
+ """
44
  population = string.ascii_letters + string.digits
45
  random_part = "".join(secrets.choice(population) for _ in range(length))
46
  return f"{prefix}{random_part}"
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  # === API Endpoints ===
49
  @app.get("/v1/models")
50
  async def list_models():
51
+ """Lists the available models."""
52
  return {"object": "list", "data": AVAILABLE_MODELS}
53
 
54
  # === Chat Completion ===
55
+ class Message(BaseModel):
56
+ role: str
57
+ content: str
58
+
59
  class ChatRequest(BaseModel):
60
  messages: List[Message]
61
  model: str
62
  stream: Optional[bool] = False
63
  tools: Optional[Any] = None
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  @app.post("/v1/chat/completions")
66
  async def chat_completion(request: ChatRequest):
67
+ """
68
+ Handles chat completion requests, supporting both streaming and non-streaming responses.
69
+ """
70
  model_id = MODEL_ALIASES.get(request.model, request.model)
71
  chat_id = generate_random_id("chatcmpl-")
72
  headers = {
 
76
  'referer': 'https://www.chatwithmono.xyz/',
77
  'user-agent': 'Mozilla/5.0',
78
  }
 
 
79
  if request.tools:
80
+ # Handle tool by giving in system prompt.
81
+ # Tool call must be encoded in <tool_call><tool_call> XML tag.
82
+ tool_prompt = f"""You have access to the following tools . To call a tool, please respond with JSON for a tool call within <tool_call><tool_call> XML tag. Respond in the format {{"name": tool name, "parameters": dictionary of argument name and its value}}. Do not use variables.
83
+ Tools:
84
+ {";".join(f"<tool>{tool}</tool>" for tool in request.tools)}
85
+
86
+ Response Format for tool call:
87
+ For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
88
+ <tool_call>
89
+ {{"name": <function-name>, "arguments": <args-json-object>}}
90
+ </tool_call>
91
+
92
+ Example of tool calling:
93
+ <tool_call>
94
+ {{"name": "get_weather", "parameters": {{"city": "New York"}}}}
95
+ </tool_call>
96
+
97
+ Using tools is recommended.
98
+ """
99
+ if request.messages[0].role == "system":
100
  request.messages[0].content += "\n\n" + tool_prompt
101
  else:
102
+ request.messages.insert(0, {"role": "system", "content": tool_prompt})
103
+ request_data = request.model_dump(exclude_unset=True)
104
 
105
  payload = {
106
+ "messages": request_data["messages"],
107
  "model": model_id
108
  }
 
 
109
  if request.stream:
110
  async def event_stream():
111
  created = int(time.time())
112
+ is_first_chunk = True
113
+ usage_info = None
114
  is_tool_call = False
115
+ chunks_buffer = []
116
+ max_initial_chunks = 4 # Number of initial chunks to buffer
117
  try:
118
  async with httpx.AsyncClient(timeout=120) as client:
119
+ async with client.stream("POST", "https://www.chatwithmono.xyz/api/chat", headers=headers, json=payload) as response:
 
120
  response.raise_for_status()
121
  async for line in response.aiter_lines():
122
+ if not line: continue
 
123
  if line.startswith("0:"):
124
  try:
125
  content_piece = json.loads(line[2:])
126
+ print(content_piece)
127
+ # Buffer the first few chunks
128
  if len(chunks_buffer) < max_initial_chunks:
129
  chunks_buffer.append(content_piece)
130
  continue
131
+ # Process the buffered chunks if we haven't already
132
+ if chunks_buffer and not is_tool_call:
133
+ full_buffer = ''.join(chunks_buffer)
 
134
  if "<tool_call>" in full_buffer:
135
+ print("Tool call detected")
136
  is_tool_call = True
137
+
138
+ # Process the current chunk
139
+ if is_tool_call:
140
+ chunks_buffer.append(content_piece)
141
+
142
+ full_buffer = ''.join(chunks_buffer)
143
+
144
+ if "</tool_call>" in full_buffer:
145
+ print("Tool call End detected")
146
+ # Process tool call in the current chunk
147
+ tool_call_str = full_buffer.split("<tool_call>")[1].split("</tool_call>")[0]
148
+ tool_call_json = json.loads(tool_call_str.strip())
149
+ delta = {
150
+ "content": None,
151
+ "tool_calls": [{
152
+ "index": 0,
153
+ "id": generate_random_id("call_"),
154
+ "type": "function",
155
+ "function": {
156
+ "name": tool_call_json["name"],
157
+ "arguments": json.dumps(tool_call_json["parameters"])
158
+ }
159
+ }]
160
+ }
161
+ chunk_data = {
162
+ "id": chat_id, "object": "chat.completion.chunk", "created": created,
163
+ "model": model_id,
164
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}],
165
+ "usage": None
166
+ }
167
+ yield f"data: {json.dumps(chunk_data)}\n\n"
168
  else:
169
+ continue
 
 
 
 
 
 
 
 
 
 
170
  else:
171
+
172
+ # Regular content
173
+ if is_first_chunk:
174
+ delta = {"content": "".join(chunks_buffer), "tool_calls": None}
175
+ delta["role"] = "assistant"
176
+ is_first_chunk = False
177
+ chunk_data = {
178
+ "id": chat_id, "object": "chat.completion.chunk", "created": created,
179
+ "model": model_id,
180
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}],
181
+ "usage": None
182
+ }
183
+ yield f"data: {json.dumps(chunk_data)}\n\n"
184
+
185
+ delta = {"content": content_piece, "tool_calls": None}
186
+
187
+ chunk_data = {
188
+ "id": chat_id, "object": "chat.completion.chunk", "created": created,
189
+ "model": model_id,
190
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}],
191
+ "usage": None
192
+ }
193
+ yield f"data: {json.dumps(chunk_data)}\n\n"
194
+ except json.JSONDecodeError: continue
195
  elif line.startswith(("e:", "d:")):
196
+ try:
197
+ usage_info = json.loads(line[2:]).get("usage")
198
+ except (json.JSONDecodeError, AttributeError): pass
199
  break
200
 
201
+ final_usage = None
202
+ if usage_info:
203
+ prompt_tokens = usage_info.get("promptTokens", 0)
204
+ completion_tokens = usage_info.get("completionTokens", 0)
205
+ final_usage = {
206
+ "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens,
207
+ "total_tokens": prompt_tokens + completion_tokens,
208
+ }
209
+ done_chunk = {
210
+ "id": chat_id, "object": "chat.completion.chunk", "created": created, "model": model_id,
211
+ "choices": [{
212
+ "index": 0,
213
+ "delta": {"role": "assistant", "content": None, "function_call": None, "tool_calls": None},
214
+ "finish_reason": "stop"
215
+ }],
216
+ "usage": final_usage
217
+ }
218
+ yield f"data: {json.dumps(done_chunk)}\n\n"
219
  except httpx.HTTPStatusError as e:
220
  error_content = {
221
  "error": {
222
+ "message": f"Upstream API error: {e.response.status_code}. Details: {e.response.text}",
223
+ "type": "upstream_error", "code": str(e.response.status_code)
 
224
  }
225
  }
226
  yield f"data: {json.dumps(error_content)}\n\n"
227
  finally:
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  yield "data: [DONE]\n\n"
 
229
  return StreamingResponse(event_stream(), media_type="text/event-stream")
230
+ else: # Non-streaming
231
+ assistant_response, usage_info = "", {}
232
+ tool_call_json = None
233
  try:
234
  async with httpx.AsyncClient(timeout=120) as client:
235
+ async with client.stream("POST", "https://www.chatwithmono.xyz/api/chat", headers=headers, json=payload) as response:
236
+ response.raise_for_status()
237
+ async for chunk in response.aiter_lines():
238
+ if chunk.startswith("0:"):
239
+ try: assistant_response += json.loads(chunk[2:])
240
+ except: continue
241
+ elif chunk.startswith(("e:", "d:")):
242
+ try: usage_info = json.loads(chunk[2:]).get("usage", {})
243
+ except: continue
 
 
 
 
244
 
245
+ if "<tool_call>" in assistant_response and "</tool_call>" in assistant_response:
246
+ tool_call_str = assistant_response.split("<tool_call>")[1].split("</tool_call>")[0]
247
+ tool_call = json.loads(tool_call_str.strip())
248
+ tool_call_json = [{"id": generate_random_id("call_"),"function": {"name": tool_call["name"], "arguments": json.dumps(tool_call["parameters"])}}]
 
 
 
 
 
 
 
 
 
 
 
249
 
250
+
251
+
252
+ return JSONResponse(content={
253
+ "id": chat_id, "object": "chat.completion", "created": int(time.time()), "model": model_id,
254
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": assistant_response if tool_call_json is None else None, "tool_calls": tool_call_json}, "finish_reason": "stop"}],
255
+ "usage": {
256
+ "prompt_tokens": usage_info.get("promptTokens", 0),
257
+ "completion_tokens": usage_info.get("completionTokens", 0),
258
+ "total_tokens": usage_info.get("promptTokens", 0) + usage_info.get("completionTokens", 0),
259
+ }
260
+ })
 
 
 
 
 
 
 
 
 
 
261
  except httpx.HTTPStatusError as e:
262
+ return JSONResponse(status_code=e.response.status_code, content={"error": {"message": f"Upstream API error. Details: {e.response.text}", "type": "upstream_error"}})
 
 
 
263
 
 
 
 
 
 
 
 
 
 
264
 
265
  # === Image Generation ===
266
  class ImageGenerationRequest(BaseModel):
 
272
 
273
  @app.post("/v1/images/generations")
274
  async def generate_images(request: ImageGenerationRequest):
275
+ """Handles image generation requests."""
276
  results = []
277
  try:
278
  async with httpx.AsyncClient(timeout=120) as client:
279
  for _ in range(request.n):
280
+ model = request.model or "default"
281
+ if model in ["gpt-image-1", "dall-e-3", "dall-e-2", "nextlm-image-1"]:
282
+ headers = {'Content-Type': 'application/json', 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.chatwithmono.xyz/'}
283
+ payload = {"prompt": request.prompt, "model": model}
284
+ resp = await client.post("https://www.chatwithmono.xyz/api/image", headers=headers, json=payload)
 
 
285
  resp.raise_for_status()
286
  data = resp.json()
287
  b64_image = data.get("image")
288
+ if not b64_image: return JSONResponse(status_code=502, content={"error": "Missing base64 image in response"})
 
 
 
 
289
  if SNAPZION_API_KEY:
290
+ upload_headers = {"Authorization": SNAPZION_API_KEY}
291
+ upload_files = {'file': ('image.png', base64.b64decode(b64_image), 'image/png')}
292
+ upload_resp = await client.post(SNAPZION_UPLOAD_URL, headers=upload_headers, files=upload_files)
 
 
 
293
  upload_resp.raise_for_status()
294
+ upload_data = upload_resp.json()
295
+ image_url = upload_data.get("url")
296
  else:
297
  image_url = f"data:image/png;base64,{b64_image}"
298
+ results.append({"url": image_url, "b64_json": b64_image, "revised_prompt": data.get("revised_prompt")})
 
 
 
 
 
299
  else:
300
+ params = {"prompt": request.prompt, "aspect_ratio": request.aspect_ratio, "link": "typegpt.net"}
 
301
  resp = await client.get(IMAGE_API_URL, params=params)
302
  resp.raise_for_status()
303
  data = resp.json()
304
+ results.append({"url": data.get("image_link"), "b64_json": data.get("base64_output")})
 
 
 
 
 
305
  except httpx.HTTPStatusError as e:
306
+ return JSONResponse(status_code=502, content={"error": f"Image generation failed. Upstream error: {e.response.status_code}", "details": e.response.text})
 
 
 
307
  except Exception as e:
308
+ return JSONResponse(status_code=500, content={"error": "An internal error occurred.", "details": str(e)})
 
 
 
 
309
  return {"created": int(time.time()), "data": results}
310
 
311
  # === Moderation Endpoint ===
 
315
 
316
  @app.post("/v1/moderations")
317
  async def create_moderation(request: ModerationRequest):
318
+ """
319
+ Handles moderation requests, conforming to the OpenAI API specification.
320
+ Includes a custom 'reason' field in the result if provided by the upstream API.
321
+ """
322
  input_texts = [request.input] if isinstance(request.input, str) else request.input
323
  if not input_texts:
324
+ return JSONResponse(status_code=400, content={"error": {"message": "Request must have at least one input string.", "type": "invalid_request_error"}})
325
+ moderation_url = "https://www.chatwithmono.xyz/api/moderation"
 
 
 
326
  headers = {
327
  'Content-Type': 'application/json',
328
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
329
  'Referer': 'https://www.chatwithmono.xyz/',
330
  }
 
331
  results = []
332
+ try:
333
+ async with httpx.AsyncClient(timeout=30) as client:
334
+ for text_input in input_texts:
335
+ payload = {"text": text_input}
336
+ resp = await client.post(moderation_url, headers=headers, json=payload)
 
 
 
337
  resp.raise_for_status()
338
+ upstream_data = resp.json()
339
+ # --- Transform upstream response to OpenAI format ---
340
+ upstream_categories = upstream_data.get("categories", {})
 
 
341
  openai_categories = {
342
+ "hate": upstream_categories.get("hate", False), "hate/threatening": False,
343
+ "harassment": False, "harassment/threatening": False,
344
+ "self-harm": upstream_categories.get("self-harm", False), "self-harm/intent": False, "self-harm/instructions": False,
345
+ "sexual": upstream_categories.get("sexual", False), "sexual/minors": False,
346
+ "violence": upstream_categories.get("violence", False), "violence/graphic": False,
 
 
347
  }
348
+ category_scores = {k: 1.0 if v else 0.0 for k, v in openai_categories.items()}
349
+ flagged = upstream_data.get("overall_sentiment") == "flagged"
350
  result_item = {
351
  "flagged": flagged,
352
  "categories": openai_categories,
353
+ "category_scores": category_scores,
354
  }
355
+
356
+ # --- NEW: Conditionally add the 'reason' field ---
357
+ # This is a custom extension to the OpenAI spec to provide more detail.
358
+ reason = upstream_data.get("reason")
359
+ if reason:
360
+ result_item["reason"] = reason
361
+
362
  results.append(result_item)
363
+ except httpx.HTTPStatusError as e:
364
+ return JSONResponse(
365
+ status_code=502, # Bad Gateway
366
+ content={"error": {"message": f"Moderation failed. Upstream error: {e.response.status_code}", "type": "upstream_error", "details": e.response.text}}
367
+ )
368
+ except Exception as e:
369
+ return JSONResponse(status_code=500, content={"error": {"message": "An internal error occurred during moderation.", "type": "internal_error", "details": str(e)}})
370
+ # Build the final OpenAI-compatible response
371
+ final_response = {
 
 
 
 
 
 
372
  "id": generate_random_id("modr-"),
373
  "model": request.model,
374
+ "results": results,
375
  }
376
+ return JSONResponse(content=final_response)
377
 
378
  # --- Main Execution ---
379
  if __name__ == "__main__":
380
  import uvicorn
381
+ uvicorn.run(app, host="0.0.0.0", port=8000)