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

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +267 -216
main.py CHANGED
@@ -7,19 +7,17 @@ import time
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,35 +36,56 @@ app = FastAPI(
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,191 +95,189 @@ async def chat_completion(request: ChatRequest):
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,40 +289,65 @@ 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,67 +357,76 @@ class ModerationRequest(BaseModel):
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)
 
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
 
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
  '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
 
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
 
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)