AIMaster7 commited on
Commit
c8a5a1f
·
verified ·
1 Parent(s): 7316e92

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +145 -58
main.py CHANGED
@@ -4,8 +4,7 @@ import os
4
  import secrets
5
  import string
6
  import time
7
- from typing import List, Optional, Union
8
-
9
  import httpx
10
  from dotenv import load_dotenv
11
  from fastapi import FastAPI
@@ -13,9 +12,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
13
  from pydantic import BaseModel
14
 
15
  # --- Configuration ---
16
-
17
  load_dotenv()
18
-
19
  # Env variables for external services
20
  IMAGE_API_URL = os.environ.get("IMAGE_API_URL", "https://image.api.example.com")
21
  SNAPZION_UPLOAD_URL = "https://upload.snapzion.com/api/public-upload"
@@ -30,18 +27,15 @@ AVAILABLE_MODELS = [
30
  {"id": "dall-e-3", "object": "model", "created": int(time.time()), "owned_by": "system"},
31
  {"id": "text-moderation-stable", "object": "model", "created": int(time.time()), "owned_by": "system"},
32
  ]
33
-
34
  MODEL_ALIASES = {}
35
 
36
  # --- FastAPI Application ---
37
-
38
  app = FastAPI(
39
  title="OpenAI Compatible API",
40
  description="An adapter for various services to be compatible with the OpenAI API specification.",
41
  version="1.0.0"
42
  )
43
 
44
-
45
  # --- Helper Function for Random ID Generation ---
46
  def generate_random_id(prefix: str, length: int = 29) -> str:
47
  """
@@ -51,17 +45,13 @@ def generate_random_id(prefix: str, length: int = 29) -> str:
51
  random_part = "".join(secrets.choice(population) for _ in range(length))
52
  return f"{prefix}{random_part}"
53
 
54
-
55
  # === API Endpoints ===
56
-
57
  @app.get("/v1/models")
58
  async def list_models():
59
  """Lists the available models."""
60
  return {"object": "list", "data": AVAILABLE_MODELS}
61
 
62
-
63
  # === Chat Completion ===
64
-
65
  class Message(BaseModel):
66
  role: str
67
  content: str
@@ -70,6 +60,7 @@ class ChatRequest(BaseModel):
70
  messages: List[Message]
71
  model: str
72
  stream: Optional[bool] = False
 
73
 
74
  @app.post("/v1/chat/completions")
75
  async def chat_completion(request: ChatRequest):
@@ -85,17 +76,44 @@ async def chat_completion(request: ChatRequest):
85
  'referer': 'https://www.chatwithmono.xyz/',
86
  'user-agent': 'Mozilla/5.0',
87
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  payload = {
89
- "messages": [{"role": msg.role, "content": msg.content} for msg in request.messages],
90
  "model": model_id
91
  }
92
-
93
  if request.stream:
94
  async def event_stream():
95
  created = int(time.time())
96
  is_first_chunk = True
97
  usage_info = None
98
-
 
 
99
  try:
100
  async with httpx.AsyncClient(timeout=120) as client:
101
  async with client.stream("POST", "https://www.chatwithmono.xyz/api/chat", headers=headers, json=payload) as response:
@@ -105,41 +123,114 @@ async def chat_completion(request: ChatRequest):
105
  if line.startswith("0:"):
106
  try:
107
  content_piece = json.loads(line[2:])
108
- delta = {"content": content_piece, "function_call": None, "tool_calls": None}
109
- if is_first_chunk:
110
- delta["role"] = "assistant"
111
- is_first_chunk = False
112
- chunk_data = {
113
- "id": chat_id, "object": "chat.completion.chunk", "created": created,
114
- "model": model_id,
115
- "choices": [{"index": 0, "delta": delta, "finish_reason": None}],
116
- "usage": None
117
- }
118
- yield f"data: {json.dumps(chunk_data)}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  except json.JSONDecodeError: continue
120
  elif line.startswith(("e:", "d:")):
121
  try:
122
  usage_info = json.loads(line[2:]).get("usage")
123
  except (json.JSONDecodeError, AttributeError): pass
124
  break
125
- final_usage = None
126
- if usage_info:
127
- prompt_tokens = usage_info.get("promptTokens", 0)
128
- completion_tokens = usage_info.get("completionTokens", 0)
129
- final_usage = {
130
- "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens,
131
- "total_tokens": prompt_tokens + completion_tokens,
132
- }
133
- done_chunk = {
134
- "id": chat_id, "object": "chat.completion.chunk", "created": created, "model": model_id,
135
- "choices": [{
136
- "index": 0,
137
- "delta": {"role": "assistant", "content": None, "function_call": None, "tool_calls": None},
138
- "finish_reason": "stop"
139
- }],
140
- "usage": final_usage
141
- }
142
- yield f"data: {json.dumps(done_chunk)}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  except httpx.HTTPStatusError as e:
144
  error_content = {
145
  "error": {
@@ -153,6 +244,7 @@ async def chat_completion(request: ChatRequest):
153
  return StreamingResponse(event_stream(), media_type="text/event-stream")
154
  else: # Non-streaming
155
  assistant_response, usage_info = "", {}
 
156
  try:
157
  async with httpx.AsyncClient(timeout=120) as client:
158
  async with client.stream("POST", "https://www.chatwithmono.xyz/api/chat", headers=headers, json=payload) as response:
@@ -164,9 +256,15 @@ async def chat_completion(request: ChatRequest):
164
  elif chunk.startswith(("e:", "d:")):
165
  try: usage_info = json.loads(chunk[2:]).get("usage", {})
166
  except: continue
 
 
 
 
 
 
167
  return JSONResponse(content={
168
  "id": chat_id, "object": "chat.completion", "created": int(time.time()), "model": model_id,
169
- "choices": [{"index": 0, "message": {"role": "assistant", "content": assistant_response}, "finish_reason": "stop"}],
170
  "usage": {
171
  "prompt_tokens": usage_info.get("promptTokens", 0),
172
  "completion_tokens": usage_info.get("completionTokens", 0),
@@ -178,7 +276,6 @@ async def chat_completion(request: ChatRequest):
178
 
179
 
180
  # === Image Generation ===
181
-
182
  class ImageGenerationRequest(BaseModel):
183
  prompt: str
184
  aspect_ratio: Optional[str] = "1:1"
@@ -224,9 +321,7 @@ async def generate_images(request: ImageGenerationRequest):
224
  return JSONResponse(status_code=500, content={"error": "An internal error occurred.", "details": str(e)})
225
  return {"created": int(time.time()), "data": results}
226
 
227
-
228
  # === Moderation Endpoint ===
229
-
230
  class ModerationRequest(BaseModel):
231
  input: Union[str, List[str]]
232
  model: Optional[str] = "text-moderation-stable"
@@ -240,14 +335,12 @@ async def create_moderation(request: ModerationRequest):
240
  input_texts = [request.input] if isinstance(request.input, str) else request.input
241
  if not input_texts:
242
  return JSONResponse(status_code=400, content={"error": {"message": "Request must have at least one input string.", "type": "invalid_request_error"}})
243
-
244
  moderation_url = "https://www.chatwithmono.xyz/api/moderation"
245
  headers = {
246
  'Content-Type': 'application/json',
247
  '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',
248
  'Referer': 'https://www.chatwithmono.xyz/',
249
  }
250
-
251
  results = []
252
  try:
253
  async with httpx.AsyncClient(timeout=30) as client:
@@ -256,7 +349,6 @@ async def create_moderation(request: ModerationRequest):
256
  resp = await client.post(moderation_url, headers=headers, json=payload)
257
  resp.raise_for_status()
258
  upstream_data = resp.json()
259
-
260
  # --- Transform upstream response to OpenAI format ---
261
  upstream_categories = upstream_data.get("categories", {})
262
  openai_categories = {
@@ -268,21 +360,19 @@ async def create_moderation(request: ModerationRequest):
268
  }
269
  category_scores = {k: 1.0 if v else 0.0 for k, v in openai_categories.items()}
270
  flagged = upstream_data.get("overall_sentiment") == "flagged"
271
-
272
  result_item = {
273
  "flagged": flagged,
274
  "categories": openai_categories,
275
  "category_scores": category_scores,
276
  }
277
-
278
  # --- NEW: Conditionally add the 'reason' field ---
279
  # This is a custom extension to the OpenAI spec to provide more detail.
280
  reason = upstream_data.get("reason")
281
  if reason:
282
  result_item["reason"] = reason
283
-
284
- results.append(result_item)
285
 
 
286
  except httpx.HTTPStatusError as e:
287
  return JSONResponse(
288
  status_code=502, # Bad Gateway
@@ -290,7 +380,6 @@ async def create_moderation(request: ModerationRequest):
290
  )
291
  except Exception as e:
292
  return JSONResponse(status_code=500, content={"error": {"message": "An internal error occurred during moderation.", "type": "internal_error", "details": str(e)}})
293
-
294
  # Build the final OpenAI-compatible response
295
  final_response = {
296
  "id": generate_random_id("modr-"),
@@ -299,9 +388,7 @@ async def create_moderation(request: ModerationRequest):
299
  }
300
  return JSONResponse(content=final_response)
301
 
302
-
303
  # --- Main Execution ---
304
-
305
  if __name__ == "__main__":
306
  import uvicorn
307
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
4
  import secrets
5
  import string
6
  import time
7
+ from typing import List, Optional, Union, Any
 
8
  import httpx
9
  from dotenv import load_dotenv
10
  from fastapi import FastAPI
 
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"
 
27
  {"id": "dall-e-3", "object": "model", "created": int(time.time()), "owned_by": "system"},
28
  {"id": "text-moderation-stable", "object": "model", "created": int(time.time()), "owned_by": "system"},
29
  ]
 
30
  MODEL_ALIASES = {}
31
 
32
  # --- FastAPI Application ---
 
33
  app = FastAPI(
34
  title="OpenAI Compatible API",
35
  description="An adapter for various services to be compatible with the OpenAI API specification.",
36
  version="1.0.0"
37
  )
38
 
 
39
  # --- Helper Function for Random ID Generation ---
40
  def generate_random_id(prefix: str, length: int = 29) -> str:
41
  """
 
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
 
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):
 
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:
 
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
+ else:
138
+ # No tool call, send buffered chunks as regular content
139
+ delta = {"content": full_buffer, "tool_calls": None}
140
+ if is_first_chunk:
141
+ delta["role"] = "assistant"
142
+ is_first_chunk = False
143
+ chunk_data = {
144
+ "id": chat_id, "object": "chat.completion.chunk", "created": created,
145
+ "model": model_id,
146
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}],
147
+ "usage": None
148
+ }
149
+ yield f"data: {json.dumps(chunk_data)}\n\n"
150
+
151
+ # Process the current chunk
152
+ if is_tool_call:
153
+ chunks_buffer.append(content_piece)
154
+
155
+ full_buffer = ''.join(chunks_buffer)
156
+
157
+ if "</tool_call>" in full_buffer:
158
+ print("Tool call End detected")
159
+ # Process tool call in the current chunk
160
+ tool_call_str = full_buffer.split("<tool_call>")[1].split("</tool_call>")[0]
161
+ tool_call_json = json.loads(tool_call_str.strip())
162
+ delta = {
163
+ "content": None,
164
+ "tool_calls": [{
165
+ "index": 0,
166
+ "id": generate_random_id("call_"),
167
+ "type": "function",
168
+ "function": {
169
+ "name": tool_call_json["name"],
170
+ "arguments": json.dumps(tool_call_json["parameters"])
171
+ }
172
+ }]
173
+ }
174
+ chunk_data = {
175
+ "id": chat_id, "object": "chat.completion.chunk", "created": created,
176
+ "model": model_id,
177
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}],
178
+ "usage": None
179
+ }
180
+ yield f"data: {json.dumps(chunk_data)}\n\n"
181
+ else:
182
+ continue
183
+ else:
184
+ # Regular content
185
+ delta = {"content": content_piece, "tool_calls": None}
186
+ if is_first_chunk:
187
+ delta["role"] = "assistant"
188
+ is_first_chunk = False
189
+ chunk_data = {
190
+ "id": chat_id, "object": "chat.completion.chunk", "created": created,
191
+ "model": model_id,
192
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}],
193
+ "usage": None
194
+ }
195
+ yield f"data: {json.dumps(chunk_data)}\n\n"
196
  except json.JSONDecodeError: continue
197
  elif line.startswith(("e:", "d:")):
198
  try:
199
  usage_info = json.loads(line[2:]).get("usage")
200
  except (json.JSONDecodeError, AttributeError): pass
201
  break
202
+ # Handle any remaining buffer content
203
+ if chunks_buffer and not is_tool_call:
204
+ full_buffer = ''.join(chunks_buffer)
205
+ delta = {"content": full_buffer, "tool_calls": None}
206
+ if is_first_chunk:
207
+ delta["role"] = "assistant"
208
+ is_first_chunk = False
209
+ chunk_data = {
210
+ "id": chat_id, "object": "chat.completion.chunk", "created": created,
211
+ "model": model_id,
212
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}],
213
+ "usage": None
214
+ }
215
+ yield f"data: {json.dumps(chunk_data)}\n\n"
216
+ final_usage = None
217
+ if usage_info:
218
+ prompt_tokens = usage_info.get("promptTokens", 0)
219
+ completion_tokens = usage_info.get("completionTokens", 0)
220
+ final_usage = {
221
+ "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens,
222
+ "total_tokens": prompt_tokens + completion_tokens,
223
+ }
224
+ done_chunk = {
225
+ "id": chat_id, "object": "chat.completion.chunk", "created": created, "model": model_id,
226
+ "choices": [{
227
+ "index": 0,
228
+ "delta": {"role": "assistant", "content": None, "function_call": None, "tool_calls": None},
229
+ "finish_reason": "stop"
230
+ }],
231
+ "usage": final_usage
232
+ }
233
+ yield f"data: {json.dumps(done_chunk)}\n\n"
234
  except httpx.HTTPStatusError as e:
235
  error_content = {
236
  "error": {
 
244
  return StreamingResponse(event_stream(), media_type="text/event-stream")
245
  else: # Non-streaming
246
  assistant_response, usage_info = "", {}
247
+ tool_call_json = None
248
  try:
249
  async with httpx.AsyncClient(timeout=120) as client:
250
  async with client.stream("POST", "https://www.chatwithmono.xyz/api/chat", headers=headers, json=payload) as response:
 
256
  elif chunk.startswith(("e:", "d:")):
257
  try: usage_info = json.loads(chunk[2:]).get("usage", {})
258
  except: continue
259
+
260
+ if "<tool_call>" in assistant_response and "</tool_call>" in assistant_response:
261
+ tool_call_str = assistant_response.split("<tool_call>")[1].split("</tool_call>")[0]
262
+ tool_call_json = json.loads(tool_call_str.strip())
263
+
264
+
265
  return JSONResponse(content={
266
  "id": chat_id, "object": "chat.completion", "created": int(time.time()), "model": model_id,
267
+ "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"}],
268
  "usage": {
269
  "prompt_tokens": usage_info.get("promptTokens", 0),
270
  "completion_tokens": usage_info.get("completionTokens", 0),
 
276
 
277
 
278
  # === Image Generation ===
 
279
  class ImageGenerationRequest(BaseModel):
280
  prompt: str
281
  aspect_ratio: Optional[str] = "1:1"
 
321
  return JSONResponse(status_code=500, content={"error": "An internal error occurred.", "details": str(e)})
322
  return {"created": int(time.time()), "data": results}
323
 
 
324
  # === Moderation Endpoint ===
 
325
  class ModerationRequest(BaseModel):
326
  input: Union[str, List[str]]
327
  model: Optional[str] = "text-moderation-stable"
 
335
  input_texts = [request.input] if isinstance(request.input, str) else request.input
336
  if not input_texts:
337
  return JSONResponse(status_code=400, content={"error": {"message": "Request must have at least one input string.", "type": "invalid_request_error"}})
 
338
  moderation_url = "https://www.chatwithmono.xyz/api/moderation"
339
  headers = {
340
  'Content-Type': 'application/json',
341
  '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',
342
  'Referer': 'https://www.chatwithmono.xyz/',
343
  }
 
344
  results = []
345
  try:
346
  async with httpx.AsyncClient(timeout=30) as client:
 
349
  resp = await client.post(moderation_url, headers=headers, json=payload)
350
  resp.raise_for_status()
351
  upstream_data = resp.json()
 
352
  # --- Transform upstream response to OpenAI format ---
353
  upstream_categories = upstream_data.get("categories", {})
354
  openai_categories = {
 
360
  }
361
  category_scores = {k: 1.0 if v else 0.0 for k, v in openai_categories.items()}
362
  flagged = upstream_data.get("overall_sentiment") == "flagged"
 
363
  result_item = {
364
  "flagged": flagged,
365
  "categories": openai_categories,
366
  "category_scores": category_scores,
367
  }
368
+
369
  # --- NEW: Conditionally add the 'reason' field ---
370
  # This is a custom extension to the OpenAI spec to provide more detail.
371
  reason = upstream_data.get("reason")
372
  if reason:
373
  result_item["reason"] = reason
 
 
374
 
375
+ results.append(result_item)
376
  except httpx.HTTPStatusError as e:
377
  return JSONResponse(
378
  status_code=502, # Bad Gateway
 
380
  )
381
  except Exception as e:
382
  return JSONResponse(status_code=500, content={"error": {"message": "An internal error occurred during moderation.", "type": "internal_error", "details": str(e)}})
 
383
  # Build the final OpenAI-compatible response
384
  final_response = {
385
  "id": generate_random_id("modr-"),
 
388
  }
389
  return JSONResponse(content=final_response)
390
 
 
391
  # --- Main Execution ---
 
392
  if __name__ == "__main__":
393
  import uvicorn
394
+ uvicorn.run(app, host="0.0.0.0", port=8000)