AIMaster7 commited on
Commit
eea98cf
·
verified ·
1 Parent(s): 43bd325

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +212 -216
main.py CHANGED
@@ -4,54 +4,58 @@ import os
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
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"},
26
  {"id": "gpt-3.5-turbo", "object": "model", "created": int(time.time()), "owned_by": "system"},
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
- """
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
@@ -62,11 +66,58 @@ class ChatRequest(BaseModel):
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,201 +127,142 @@ 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):
267
- prompt: str
268
- aspect_ratio: Optional[str] = "1:1"
269
- n: Optional[int] = 1
270
- user: Optional[str] = None
271
- model: Optional[str] = "default"
272
-
273
- @app.post("/v1/images/generations")
274
  async def generate_images(request: ImageGenerationRequest):
275
  """Handles image generation requests."""
276
  results = []
@@ -281,20 +273,20 @@ async def generate_images(request: ImageGenerationRequest):
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"}
@@ -308,74 +300,78 @@ async def generate_images(request: ImageGenerationRequest):
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 ===
312
- class ModerationRequest(BaseModel):
313
- input: Union[str, List[str]]
314
- model: Optional[str] = "text-moderation-stable"
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)
 
4
  import secrets
5
  import string
6
  import time
7
+ import tempfile
8
  from typing import List, Optional, Union, Any
9
+
10
  import httpx
11
  from dotenv import load_dotenv
12
+ from fastapi import FastAPI, HTTPException
13
  from fastapi.responses import JSONResponse, StreamingResponse
14
+ from pydantic import BaseModel, Field, model_validator
15
+
16
+ # New import for OCR
17
+ from gradio_client import Client, handle_file
18
 
19
  # --- Configuration ---
20
  load_dotenv()
21
+
22
  # Env variables for external services
23
  IMAGE_API_URL = os.environ.get("IMAGE_API_URL", "https://image.api.example.com")
24
  SNAPZION_UPLOAD_URL = "https://upload.snapzion.com/api/public-upload"
25
  SNAPZION_API_KEY = os.environ.get("SNAP", "")
26
+ CHAT_API_URL = "https://www.chatwithmono.xyz/api/chat"
27
+ IMAGE_GEN_API_URL = "https://www.chatwithmono.xyz/api/image"
28
+ MODERATION_API_URL = "https://www.chatwithmono.xyz/api/moderation"
29
 
30
+ # --- Model Definitions ---
31
+ # Added florence-2-ocr for the new endpoint
32
  AVAILABLE_MODELS = [
33
  {"id": "gpt-4-turbo", "object": "model", "created": int(time.time()), "owned_by": "system"},
34
  {"id": "gpt-4o", "object": "model", "created": int(time.time()), "owned_by": "system"},
35
  {"id": "gpt-3.5-turbo", "object": "model", "created": int(time.time()), "owned_by": "system"},
36
  {"id": "dall-e-3", "object": "model", "created": int(time.time()), "owned_by": "system"},
37
  {"id": "text-moderation-stable", "object": "model", "created": int(time.time()), "owned_by": "system"},
38
+ {"id": "florence-2-ocr", "object": "model", "created": int(time.time()), "owned_by": "system"},
39
  ]
40
  MODEL_ALIASES = {}
41
 
42
+ # --- FastAPI Application & Global Clients ---
43
  app = FastAPI(
44
  title="OpenAI Compatible API",
45
  description="An adapter for various services to be compatible with the OpenAI API specification.",
46
+ version="1.1.0"
47
  )
48
 
49
+ # Initialize Gradio client for OCR globally to avoid re-initialization on each request
50
+ try:
51
+ ocr_client = Client("multimodalart/Florence-2-l4")
52
+ except Exception as e:
53
+ print(f"Warning: Could not initialize Gradio client for OCR: {e}")
54
+ ocr_client = None
 
 
55
 
56
+ # --- Pydantic Models ---
 
 
 
 
57
 
58
+ # /v1/chat/completions
59
  class Message(BaseModel):
60
  role: str
61
  content: str
 
66
  stream: Optional[bool] = False
67
  tools: Optional[Any] = None
68
 
69
+ # /v1/images/generations
70
+ class ImageGenerationRequest(BaseModel):
71
+ prompt: str
72
+ aspect_ratio: Optional[str] = "1:1"
73
+ n: Optional[int] = 1
74
+ user: Optional[str] = None
75
+ model: Optional[str] = "default"
76
+
77
+ # /v1/moderations
78
+ class ModerationRequest(BaseModel):
79
+ input: Union[str, List[str]]
80
+ model: Optional[str] = "text-moderation-stable"
81
+
82
+ # /v1/ocr
83
+ class OcrRequest(BaseModel):
84
+ image_url: Optional[str] = Field(None, description="URL of the image to process.")
85
+ image_b64: Optional[str] = Field(None, description="Base64 encoded string of the image to process.")
86
+
87
+ @model_validator(mode='before')
88
+ @classmethod
89
+ def check_sources(cls, data: Any) -> Any:
90
+ if isinstance(data, dict):
91
+ url = data.get('image_url')
92
+ b64 = data.get('image_b64')
93
+ if not (url or b64):
94
+ raise ValueError('Either image_url or image_b64 must be provided.')
95
+ if url and b64:
96
+ raise ValueError('Provide either image_url or image_b64, not both.')
97
+ return data
98
+
99
+ class OcrResponse(BaseModel):
100
+ ocr_text: str
101
+ raw_response: dict
102
+
103
+
104
+ # --- Helper Function for Random ID Generation ---
105
+ def generate_random_id(prefix: str, length: int = 29) -> str:
106
+ """Generates a cryptographically secure, random alphanumeric ID."""
107
+ population = string.ascii_letters + string.digits
108
+ random_part = "".join(secrets.choice(population) for _ in range(length))
109
+ return f"{prefix}{random_part}"
110
+
111
+ # === API Endpoints ===
112
+
113
+ @app.get("/v1/models", tags=["Models"])
114
+ async def list_models():
115
+ """Lists the available models."""
116
+ return {"object": "list", "data": AVAILABLE_MODELS}
117
+
118
+ @app.post("/v1/chat/completions", tags=["Chat"])
119
  async def chat_completion(request: ChatRequest):
120
+ """Handles chat completion requests, supporting streaming and non-streaming."""
 
 
121
  model_id = MODEL_ALIASES.get(request.model, request.model)
122
  chat_id = generate_random_id("chatcmpl-")
123
  headers = {
 
127
  'referer': 'https://www.chatwithmono.xyz/',
128
  'user-agent': 'Mozilla/5.0',
129
  }
130
+
131
+ # Handle tool prompting
132
  if request.tools:
133
+ 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 tags. Respond in the format {{"name": tool name, "parameters": dictionary of argument name and its value}}. Do not use variables.
134
+ Tools: {";".join(f"<tool>{tool}</tool>" for tool in request.tools)}
 
 
 
 
135
  Response Format for tool call:
 
136
  <tool_call>
137
  {{"name": <function-name>, "arguments": <args-json-object>}}
138
+ </tool_call>"""
 
 
 
 
 
 
 
 
139
  if request.messages[0].role == "system":
140
  request.messages[0].content += "\n\n" + tool_prompt
141
  else:
142
+ request.messages.insert(0, Message(role="system", content=tool_prompt))
143
+
144
+ payload = {"messages": [msg.model_dump() for msg in request.messages], "model": model_id}
145
 
 
 
 
 
146
  if request.stream:
147
  async def event_stream():
148
  created = int(time.time())
 
149
  usage_info = None
150
+ is_first_chunk = True
151
+ tool_call_buffer = ""
152
+ in_tool_call = False
153
+
154
  try:
155
  async with httpx.AsyncClient(timeout=120) as client:
156
+ async with client.stream("POST", CHAT_API_URL, headers=headers, json=payload) as response:
157
  response.raise_for_status()
158
  async for line in response.aiter_lines():
159
  if not line: continue
160
  if line.startswith("0:"):
161
  try:
162
  content_piece = json.loads(line[2:])
163
+ except json.JSONDecodeError:
164
+ continue
165
+
166
+ current_buffer = content_piece
167
+ if in_tool_call:
168
+ current_buffer = tool_call_buffer + content_piece
169
+
170
+ if "</tool_call>" in current_buffer:
171
+ tool_str = current_buffer.split("<tool_call>")[1].split("</tool_call>")[0]
172
+ tool_json = json.loads(tool_str.strip())
173
+ delta = {
174
+ "content": None,
175
+ "tool_calls": [{"index": 0, "id": generate_random_id("call_"), "type": "function",
176
+ "function": {"name": tool_json["name"], "arguments": json.dumps(tool_json["parameters"])}}]
177
+ }
178
+ chunk = {"id": chat_id, "object": "chat.completion.chunk", "created": created, "model": model_id,
179
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}], "usage": None}
180
+ yield f"data: {json.dumps(chunk)}\n\n"
181
+
182
+ in_tool_call = False
183
+ tool_call_buffer = ""
184
+ # Process text that might come after the tool call in the same chunk
185
+ remaining_text = current_buffer.split("</tool_call>", 1)[1]
186
+ if remaining_text:
187
+ content_piece = remaining_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  else:
189
+ continue
190
+
191
+ if "<tool_call>" in content_piece:
192
+ in_tool_call = True
193
+ tool_call_buffer += content_piece.split("<tool_call>", 1)[1]
194
+ # Process text that came before the tool call
195
+ text_before = content_piece.split("<tool_call>", 1)[0]
196
+ if text_before:
197
+ # Send the text before the tool call starts
198
+ delta = {"content": text_before, "tool_calls": None}
199
+ chunk = {"id": chat_id, "object": "chat.completion.chunk", "created": created, "model": model_id,
200
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}], "usage": None}
201
+ yield f"data: {json.dumps(chunk)}\n\n"
202
+ if "</tool_call>" not in tool_call_buffer:
203
+ continue # Wait for the closing tag
204
+
205
+ if not in_tool_call:
206
+ delta = {"content": content_piece}
207
+ if is_first_chunk:
208
+ delta["role"] = "assistant"
209
+ is_first_chunk = False
210
+ chunk = {"id": chat_id, "object": "chat.completion.chunk", "created": created, "model": model_id,
211
+ "choices": [{"index": 0, "delta": delta, "finish_reason": None}], "usage": None}
212
+ yield f"data: {json.dumps(chunk)}\n\n"
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  elif line.startswith(("e:", "d:")):
215
  try:
216
  usage_info = json.loads(line[2:]).get("usage")
217
  except (json.JSONDecodeError, AttributeError): pass
218
  break
219
+
220
+ # Finalize
221
  final_usage = None
222
  if usage_info:
223
+ final_usage = {"prompt_tokens": usage_info.get("promptTokens", 0), "completion_tokens": usage_info.get("completionTokens", 0), "total_tokens": usage_info.get("promptTokens", 0) + usage_info.get("completionTokens", 0)}
224
+ done_chunk = {"id": chat_id, "object": "chat.completion.chunk", "created": created, "model": model_id,
225
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "stop" if not in_tool_call else "tool_calls"}], "usage": final_usage}
 
 
 
 
 
 
 
 
 
 
 
 
226
  yield f"data: {json.dumps(done_chunk)}\n\n"
227
+
228
  except httpx.HTTPStatusError as e:
229
+ error_content = {"error": {"message": f"Upstream API error: {e.response.status_code}. Details: {e.response.text}", "type": "upstream_error", "code": str(e.response.status_code)}}
 
 
 
 
 
230
  yield f"data: {json.dumps(error_content)}\n\n"
231
  finally:
232
  yield "data: [DONE]\n\n"
233
+
234
  return StreamingResponse(event_stream(), media_type="text/event-stream")
235
  else: # Non-streaming
236
+ full_response, usage_info = "", {}
 
237
  try:
238
  async with httpx.AsyncClient(timeout=120) as client:
239
+ async with client.stream("POST", CHAT_API_URL, headers=headers, json=payload) as response:
240
  response.raise_for_status()
241
  async for chunk in response.aiter_lines():
242
  if chunk.startswith("0:"):
243
+ try: full_response += json.loads(chunk[2:])
244
  except: continue
245
  elif chunk.startswith(("e:", "d:")):
246
  try: usage_info = json.loads(chunk[2:]).get("usage", {})
247
  except: continue
248
 
249
+ tool_calls = None
250
+ content_response = full_response
251
+ if "<tool_call>" in full_response and "</tool_call>" in full_response:
252
+ tool_call_str = full_response.split("<tool_call>")[1].split("</tool_call>")[0]
253
  tool_call = json.loads(tool_call_str.strip())
254
+ tool_calls = [{"id": generate_random_id("call_"), "type": "function", "function": {"name": tool_call["name"], "arguments": json.dumps(tool_call["parameters"])}}]
255
+ content_response = None
256
+
 
257
  return JSONResponse(content={
258
  "id": chat_id, "object": "chat.completion", "created": int(time.time()), "model": model_id,
259
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": content_response, "tool_calls": tool_calls}, "finish_reason": "stop" if not tool_calls else "tool_calls"}],
260
+ "usage": {"prompt_tokens": usage_info.get("promptTokens", 0), "completion_tokens": usage_info.get("completionTokens", 0), "total_tokens": usage_info.get("promptTokens", 0) + usage_info.get("completionTokens", 0)}
 
 
 
 
261
  })
262
  except httpx.HTTPStatusError as e:
263
  return JSONResponse(status_code=e.response.status_code, content={"error": {"message": f"Upstream API error. Details: {e.response.text}", "type": "upstream_error"}})
264
 
265
+ @app.post("/v1/images/generations", tags=["Images"])
 
 
 
 
 
 
 
 
 
266
  async def generate_images(request: ImageGenerationRequest):
267
  """Handles image generation requests."""
268
  results = []
 
273
  if model in ["gpt-image-1", "dall-e-3", "dall-e-2", "nextlm-image-1"]:
274
  headers = {'Content-Type': 'application/json', 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.chatwithmono.xyz/'}
275
  payload = {"prompt": request.prompt, "model": model}
276
+ resp = await client.post(IMAGE_GEN_API_URL, headers=headers, json=payload)
277
  resp.raise_for_status()
278
  data = resp.json()
279
  b64_image = data.get("image")
280
  if not b64_image: return JSONResponse(status_code=502, content={"error": "Missing base64 image in response"})
281
+
282
+ image_url = f"data:image/png;base64,{b64_image}"
283
  if SNAPZION_API_KEY:
284
  upload_headers = {"Authorization": SNAPZION_API_KEY}
285
  upload_files = {'file': ('image.png', base64.b64decode(b64_image), 'image/png')}
286
  upload_resp = await client.post(SNAPZION_UPLOAD_URL, headers=upload_headers, files=upload_files)
287
+ if upload_resp.status_code == 200:
288
+ image_url = upload_resp.json().get("url", image_url)
289
+
 
 
290
  results.append({"url": image_url, "b64_json": b64_image, "revised_prompt": data.get("revised_prompt")})
291
  else:
292
  params = {"prompt": request.prompt, "aspect_ratio": request.aspect_ratio, "link": "typegpt.net"}
 
300
  return JSONResponse(status_code=500, content={"error": "An internal error occurred.", "details": str(e)})
301
  return {"created": int(time.time()), "data": results}
302
 
303
+ @app.post("/v1/ocr", response_model=OcrResponse, tags=["OCR"])
304
+ async def perform_ocr(request: OcrRequest):
 
 
 
 
 
305
  """
306
+ Performs Optical Character Recognition (OCR) on an image using the Florence-2 model.
307
+ Provide an image via a URL or a base64 encoded string.
308
  """
309
+ if not ocr_client:
310
+ raise HTTPException(status_code=503, detail="OCR service is not available. Gradio client failed to initialize.")
311
+
312
+ image_path, temp_file_path = None, None
313
+ try:
314
+ if request.image_url:
315
+ image_path = request.image_url
316
+ elif request.image_b64:
317
+ image_bytes = base64.b64decode(request.image_b64)
318
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
319
+ temp_file.write(image_bytes)
320
+ temp_file_path = temp_file.name
321
+ image_path = temp_file_path
322
+
323
+ prediction = ocr_client.predict(image=handle_file(image_path), task_prompt="OCR", api_name="/process_image")
324
+
325
+ if not prediction or not isinstance(prediction, tuple):
326
+ raise HTTPException(status_code=502, detail="Invalid response from OCR service.")
327
+
328
+ raw_result = prediction[0]
329
+ ocr_text = raw_result.get("OCR", "")
330
+ return OcrResponse(ocr_text=ocr_text, raw_response=raw_result)
331
+ except Exception as e:
332
+ raise HTTPException(status_code=500, detail=f"An error occurred during OCR processing: {str(e)}")
333
+ finally:
334
+ if temp_file_path:
335
+ os.unlink(temp_file_path)
336
+
337
+ @app.post("/v1/moderations", tags=["Moderation"])
338
+ async def create_moderation(request: ModerationRequest):
339
+ """Handles moderation requests, conforming to the OpenAI API specification."""
340
  input_texts = [request.input] if isinstance(request.input, str) else request.input
341
  if not input_texts:
342
+ return JSONResponse(status_code=400, content={"error": {"message": "Request must have at least one input string."}})
343
+ headers = {'Content-Type': 'application/json', 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.chatwithmono.xyz/'}
 
 
 
 
 
344
  results = []
345
  try:
346
  async with httpx.AsyncClient(timeout=30) as client:
347
  for text_input in input_texts:
348
+ resp = await client.post(MODERATION_API_URL, headers=headers, json={"text": text_input})
 
349
  resp.raise_for_status()
350
  upstream_data = resp.json()
 
351
  upstream_categories = upstream_data.get("categories", {})
352
  openai_categories = {
353
+ "hate": upstream_categories.get("hate", False), "hate/threatening": False, "harassment": False, "harassment/threatening": False,
 
354
  "self-harm": upstream_categories.get("self-harm", False), "self-harm/intent": False, "self-harm/instructions": False,
355
  "sexual": upstream_categories.get("sexual", False), "sexual/minors": False,
356
  "violence": upstream_categories.get("violence", False), "violence/graphic": False,
357
  }
 
 
358
  result_item = {
359
+ "flagged": upstream_data.get("overall_sentiment") == "flagged",
360
  "categories": openai_categories,
361
+ "category_scores": {k: 1.0 if v else 0.0 for k, v in openai_categories.items()},
362
  }
363
+ if reason := upstream_data.get("reason"):
 
 
 
 
364
  result_item["reason"] = reason
 
365
  results.append(result_item)
366
  except httpx.HTTPStatusError as e:
367
+ return JSONResponse(status_code=502, content={"error": {"message": f"Moderation failed. Upstream error: {e.response.status_code}", "details": e.response.text}})
 
 
 
368
  except Exception as e:
369
+ return JSONResponse(status_code=500, content={"error": {"message": "An internal error occurred during moderation.", "details": str(e)}})
370
+
371
+ return JSONResponse(content={"id": generate_random_id("modr-"), "model": request.model, "results": results})
372
+
 
 
 
 
373
 
374
  # --- Main Execution ---
375
  if __name__ == "__main__":
376
  import uvicorn
377
+ uvicorn.run(app, host="0.0.0.0", port=8000)