rkihacker commited on
Commit
b4f3478
·
verified ·
1 Parent(s): 5f34a70

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +623 -0
main.py ADDED
@@ -0,0 +1,623 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI application providing OpenAI-compatible API endpoints using QodoAI.
3
+ """
4
+
5
+ import json
6
+ import time
7
+ import uuid
8
+ import logging
9
+ import asyncio
10
+ from typing import List, Dict, Optional, Union, Generator, Any, AsyncGenerator
11
+
12
+ from fastapi import FastAPI, HTTPException, Depends, Request, status
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.responses import StreamingResponse, JSONResponse
15
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
16
+ from pydantic import BaseModel, Field, validator
17
+ import uvicorn
18
+
19
+ from curl_cffi.requests import Session
20
+ from curl_cffi import CurlError
21
+
22
+ # Configure logging
23
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
24
+ logger = logging.getLogger(__name__)
25
+
26
+ security = HTTPBearer(auto_error=False)
27
+
28
+ # ============================================================================
29
+ # Exception Classes
30
+ # ============================================================================
31
+
32
+ class FailedToGenerateResponseError(Exception):
33
+ """Exception raised when response generation fails."""
34
+ pass
35
+
36
+ # ============================================================================
37
+ # Utility Functions
38
+ # ============================================================================
39
+
40
+ def sanitize_stream(data, intro_value="", to_json=True, skip_markers=None, content_extractor=None, yield_raw_on_error=True, raw=False):
41
+ """Sanitize stream data and extract content."""
42
+ if skip_markers is None:
43
+ skip_markers = []
44
+
45
+ for chunk in data:
46
+ if chunk:
47
+ try:
48
+ chunk_str = chunk.decode('utf-8') if isinstance(chunk, bytes) else str(chunk)
49
+ if any(marker in chunk_str for marker in skip_markers):
50
+ continue
51
+
52
+ if to_json:
53
+ try:
54
+ json_obj = json.loads(chunk_str)
55
+ if content_extractor:
56
+ content = content_extractor(json_obj)
57
+ if content:
58
+ yield content
59
+ except json.JSONDecodeError:
60
+ if yield_raw_on_error:
61
+ yield chunk_str
62
+ else:
63
+ yield chunk_str
64
+ except Exception as e:
65
+ if yield_raw_on_error:
66
+ yield str(chunk)
67
+
68
+ # ============================================================================
69
+ # Pydantic Models for OpenAI API Compatibility
70
+ # ============================================================================
71
+
72
+ class ChatMessage(BaseModel):
73
+ role: str = Field(..., description="The role of the message author")
74
+ content: str = Field(..., description="The content of the message")
75
+ name: Optional[str] = Field(None, description="The name of the author")
76
+
77
+ class ChatCompletionRequest(BaseModel):
78
+ model: str = Field(..., description="ID of the model to use")
79
+ messages: List[ChatMessage] = Field(..., description="List of messages comprising the conversation")
80
+ max_tokens: Optional[int] = Field(2049, description="Maximum number of tokens to generate")
81
+ temperature: Optional[float] = Field(None, ge=0, le=2, description="Sampling temperature")
82
+ top_p: Optional[float] = Field(None, ge=0, le=1, description="Nucleus sampling parameter")
83
+ stream: Optional[bool] = Field(False, description="Whether to stream back partial progress")
84
+ stop: Optional[Union[str, List[str]]] = Field(None, description="Stop sequences")
85
+ presence_penalty: Optional[float] = Field(None, ge=-2, le=2, description="Presence penalty")
86
+ frequency_penalty: Optional[float] = Field(None, ge=-2, le=2, description="Frequency penalty")
87
+
88
+ class Usage(BaseModel):
89
+ prompt_tokens: int
90
+ completion_tokens: int
91
+ total_tokens: int
92
+
93
+ class ChatCompletionMessage(BaseModel):
94
+ role: str
95
+ content: str
96
+
97
+ class Choice(BaseModel):
98
+ index: int
99
+ message: Optional[ChatCompletionMessage] = None
100
+ delta: Optional[Dict[str, Any]] = None
101
+ finish_reason: Optional[str] = None
102
+
103
+ class ChatCompletionResponse(BaseModel):
104
+ id: str
105
+ object: str = "chat.completion"
106
+ created: int
107
+ model: str
108
+ choices: List[Choice]
109
+ usage: Usage
110
+
111
+ class ChatCompletionChunk(BaseModel):
112
+ id: str
113
+ object: str = "chat.completion.chunk"
114
+ created: int
115
+ model: str
116
+ choices: List[Choice]
117
+
118
+ class ModelInfo(BaseModel):
119
+ id: str
120
+ object: str = "model"
121
+ created: int
122
+ owned_by: str = "qodo"
123
+
124
+ class ModelListResponse(BaseModel):
125
+ object: str = "list"
126
+ data: List[ModelInfo]
127
+
128
+ class HealthResponse(BaseModel):
129
+ status: str
130
+ timestamp: int
131
+
132
+ # ============================================================================
133
+ # QodoAI Implementation
134
+ # ============================================================================
135
+
136
+ class QodoAI:
137
+ """OpenAI-compatible client for Qodo AI API."""
138
+
139
+ AVAILABLE_MODELS = [
140
+ "gpt-4.1",
141
+ "gpt-4o",
142
+ "o3",
143
+ "o4-mini",
144
+ "claude-4-sonnet",
145
+ "gemini-2.5-pro"
146
+ ]
147
+
148
+ def __init__(self, api_key: Optional[str] = None, timeout: int = 30):
149
+ self.url = "https://api.cli.qodo.ai/v2/agentic/start-task"
150
+ self.info_url = "https://api.cli.qodo.ai/v2/info/get-things"
151
+ self.timeout = timeout
152
+ self.
153
+ api_key = api_key or "sk-dS7U-extxMWUxc8SbYYOuncqGUIE8-y2OY8oMCpu0eI-qnSUyH9CYWO_eAMpqwfMo7pXU3QNrclfZYMO0M6BJTM"
154
+
155
+ # Generate fingerprint
156
+ self.fingerprint = {"user_agent": "axios/1.10.0", "browser_type": "chrome"}
157
+
158
+ # Generate session ID
159
+ self.session_id = self._get_session_id()
160
+ self.request_id = str(uuid.uuid4())
161
+
162
+ # Setup headers
163
+ self.headers = {
164
+ "Accept": "text/plain",
165
+ "Accept-Encoding": "gzip, deflate, br, zstd",
166
+ "Accept-Language": "en-US,en;q=0.9",
167
+ "Authorization": f"Bearer {self.api_key}",
168
+ "Connection": "close",
169
+ "Content-Type": "application/json",
170
+ "host": "api.cli.qodo.ai",
171
+ "Request-id": self.request_id,
172
+ "Session-id": self.session_id,
173
+ "User-Agent": self.fingerprint["user_agent"],
174
+ }
175
+
176
+ # Initialize session
177
+ self.session = Session()
178
+ self.session.headers.update(self.headers)
179
+
180
+ def _get_session_id(self) -> str:
181
+ """Get session ID from Qodo API."""
182
+ try:
183
+ temp_session = Session()
184
+ temp_headers = {
185
+ "Accept": "text/plain",
186
+ "Authorization": f"Bearer {self.api_key}",
187
+ "Content-Type": "application/json",
188
+ "User-Agent": "axios/1.10.0",
189
+ }
190
+
191
+ temp_session.headers.update(temp_headers)
192
+
193
+ response = temp_session.get(self.info_url, timeout=self.timeout, impersonate="chrome110")
194
+
195
+ if response.status_code == 200:
196
+ data = response.json()
197
+ session_id = data.get("session-id")
198
+ if session_id:
199
+ return session_id
200
+
201
+ return f"20250630-{str(uuid.uuid4())}"
202
+
203
+ except Exception:
204
+ return f"20250630-{str(uuid.uuid4())}"
205
+
206
+ @staticmethod
207
+ def _qodo_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
208
+ """Extracts content from Qodo stream JSON objects."""
209
+ if isinstance(chunk, dict):
210
+ data = chunk.get("data", {})
211
+ if isinstance(data, dict):
212
+ tool_args = data.get("tool_args", {})
213
+ if isinstance(tool_args, dict):
214
+ content = tool_args.get("content")
215
+ if content:
216
+ return content
217
+
218
+ if "content" in data:
219
+ return data["content"]
220
+
221
+ if "choices" in chunk:
222
+ choices = chunk["choices"]
223
+ if isinstance(choices, list) and len(choices) > 0:
224
+ choice = choices[0]
225
+ if isinstance(choice, dict):
226
+ delta = choice.get("delta", {})
227
+ if isinstance(delta, dict) and "content" in delta:
228
+ return delta["content"]
229
+
230
+ message = choice.get("message", {})
231
+ if isinstance(message, dict) and "content" in message:
232
+ return message["content"]
233
+
234
+ elif isinstance(chunk, str):
235
+ try:
236
+ parsed = json.loads(chunk)
237
+ return QodoAI._qodo_ext
238
+ ractor(parsed)
239
+ except json.JSONDecodeError:
240
+ if chunk.strip():
241
+ return chunk.strip()
242
+
243
+ return None
244
+
245
+ def _build_payload(self, prompt: str, model: str = "claude-4-sonnet"):
246
+ """Build the payload for Qodo AI API."""
247
+ return {
248
+ "agent_type": "cli",
249
+ "session_id": self.session_id,
250
+ "user_data": {
251
+ "extension_version": "0.7.2",
252
+ "os_platform": "win32",
253
+ "os_version": "v23.9.0",
254
+ "editor_type": "cli"
255
+ },
256
+ "tools": {
257
+ "web_search": [
258
+ {
259
+ "name": "web_search",
260
+ "description": "Searches the web and returns results based on the user's query (Powered by Nimble).",
261
+ "inputSchema": {
262
+ "type": "object",
263
+ "properties": {
264
+ "query": {
265
+ "description": "The search query to execute",
266
+ "title": "Query",
267
+ "type": "string"
268
+ }
269
+ },
270
+ "required": ["query"]
271
+ },
272
+ "be_tool": True,
273
+ "autoApproved": True
274
+ }
275
+ ]
276
+ },
277
+ "user_request": prompt,
278
+ "execution_strategy": "act",
279
+ "custom_model": model,
280
+ "stream": True
281
+ }
282
+
283
+ async def create_chat_completion(self, request: ChatCompletionRequest) -> Union[ChatCompletionResponse, AsyncGenerator]:
284
+ """Create a chat completion response."""
285
+ # Get the last user message
286
+ user_prompt = ""
287
+ for message in reversed(request.messages):
288
+ if message.role == "user":
289
+ user_prompt = message.content
290
+
291
+ break
292
+
293
+ if not user_prompt:
294
+ raise HTTPException(status_code=400, detail="No user message found in messages")
295
+
296
+ payload = self._build_payload(user_prompt, request.model)
297
+ payload["stream"] = request.stream
298
+
299
+ request_id = f"chatcmpl-{uuid.uuid4()}"
300
+ created_time = int(time.time())
301
+
302
+ if request.stream:
303
+ return self._create_stream_response(request_id, created_time, request.model, payload, user_prompt)
304
+ else:
305
+ return await self._create_non_stream_response(request_id, created_time, request.model, payload, user_prompt)
306
+
307
+ async def _create_stream_response(self, request_id: str, created_time: int, model: str, payload: Dict[str, Any], user_prompt: str):
308
+ """Create streaming response."""
309
+ try:
310
+ response = self.session.post(
311
+ self.url,
312
+ json=payload,
313
+ stream=True,
314
+ timeout=self.timeout,
315
+ impersonate="chrome110"
316
+ )
317
+
318
+ if response.status_code == 401:
319
+ raise HTTPException(status_code=401, detail="Invalid API key")
320
+ elif response.status_code != 200:
321
+ raise HTTPException(status_code=500, detail=f"Qodo request failed: {response.text}")
322
+
323
+ async def generate():
324
+ try:
325
+ processed_stream = sanitize_stream(
326
+ data=response.iter_content(chunk_size=None),
327
+ intro_value="",
328
+ to_json=True,
329
+ skip_markers=["[DONE]"],
330
+ content_extractor=QodoAI._qodo_extractor,
331
+ yield_raw_on_error=True,
332
+ raw=False
333
+ )
334
+
335
+ for content_chunk in processed_stream:
336
+ if content_chunk:
337
+ chunk_data = {
338
+ "id": request_id,
339
+ "object": "chat.completion.chunk",
340
+ "created": created_time,
341
+ "model": model,
342
+ "choices": [{
343
+ "index": 0,
344
+ "delta": {"content": content_chunk, "role": "assistant"},
345
+ "finish_reason": None
346
+ }]
347
+ }
348
+ yield f"data: {json.dumps(chunk_data)}\n\n"
349
+
350
+ # Send final chunk
351
+ final_chunk = {
352
+ "id": request_id,
353
+ "object": "chat.completion.chunk",
354
+ "created": created_time,
355
+ "model": model,
356
+ "choices": [{
357
+ "index": 0,
358
+ "delta": {},
359
+ "finish_reason": "stop"
360
+ }]
361
+ }
362
+ yield f"data: {json.dumps(final_chunk)}\n\n"
363
+ yield "data: [DONE]\n\n"
364
+
365
+ except Exception as e:
366
+ logger.error(f"Streaming error: {e}")
367
+ error_chunk = {
368
+ "id": request_id,
369
+ "object": "chat.completion.chunk",
370
+ "created": created_time,
371
+ "model": model,
372
+ "choices": [{
373
+ "index": 0,
374
+ "delta": {},
375
+ "finish_reason": "stop"
376
+ }]
377
+ }
378
+ yield f"data: {json.dumps(error_chunk)}\n\n"
379
+ yield "data: [DONE]\n\n"
380
+
381
+ return generate()
382
+
383
+ except Exception as e:
384
+ logger.error(f"Stream creation error: {e}")
385
+ raise HTTPException(status_code=
386
+ 500, detail=str(e))
387
+
388
+ async def _create_non_stream_response(self, request_id: str, created_time: int, model: str, payload: Dict[str, Any], user_prompt: str) -> ChatCompletionResponse:
389
+ """Create non-streaming response."""
390
+ try:
391
+ payload["stream"] = False
392
+ response = self.session.post(
393
+ self.url,
394
+ json=payload,
395
+ timeout=self.timeout,
396
+ impersonate="chrome110"
397
+ )
398
+
399
+ if response.status_code == 401:
400
+ raise HTTPException(status_code=401, detail="Invalid API key")
401
+ elif response.status_code != 200:
402
+ raise HTTPException(status_code=500, detail=f"Qodo request failed: {response.text}")
403
+
404
+ response_text = response.text
405
+ full_response = ""
406
+
407
+ # Parse multiple JSON objects from the response
408
+ lines = response_text.replace('}\n{', '}\n{').split('\n')
409
+ json_objects = []
410
+
411
+ current_json = ""
412
+ brace_count = 0
413
+
414
+ for line in lines:
415
+ line = line.strip()
416
+ if line:
417
+ current_json += line
418
+ brace_count += line.count('{') - line.count('}')
419
+
420
+ if brace_count == 0 and current_json:
421
+ json_objects.append(current_json)
422
+ current_json = ""
423
+
424
+ if current_json and brace_count == 0:
425
+ json_objects.append(current_json)
426
+
427
+ for json_str in json_objects:
428
+ if json_str.strip():
429
+ try:
430
+ json_obj = json.loads(json_str)
431
+ content = QodoAI._qodo_extractor(json_obj)
432
+ if content:
433
+ full_response += content
434
+ except json.JSONDecodeError:
435
+ pass
436
+
437
+ # Calculate token usage
438
+ prompt_tokens = len(user_prompt.split())
439
+ completion_tokens = len(full_response.split())
440
+ total_tokens = prompt_tokens + completion_tokens
441
+
442
+ return ChatCompletionResponse(
443
+ id=request_id,
444
+ created=created_time,
445
+ model=model,
446
+ choices=[Choice(
447
+ index=0,
448
+ message=ChatCompletionMessage(role="assistant", content=full_response),
449
+ finish_reason="stop"
450
+ )],
451
+ usage=Usage(
452
+ prompt_tokens=prompt_tokens,
453
+ completion_tokens=completion_tokens,
454
+ total_tokens=total_tokens
455
+ )
456
+ )
457
+
458
+ except Exception as e:
459
+ logger.error(f"Non-stream response error: {e}")
460
+ raise HTTPException(status_code=500, detail=str(e))
461
+
462
+ # ============================================================================
463
+ # FastAPI Application
464
+ # ============================================================================
465
+
466
+ # Global QodoAI client
467
+ qodo_client = None
468
+
469
+ @asynccontextmanager
470
+ async def lifespan(app: FastAPI):
471
+ """Application lifespan manager."""
472
+ global qodo_client
473
+ logger.info("Starting FastAPI application...")
474
+ qodo_client = QodoAI()
475
+ yield
476
+ logger.info("Shutting down FastAPI application...")
477
+
478
+ # Create FastAPI app
479
+ app = FastAPI(
480
+ title="QodoAI OpenAI-Compatible API",
481
+ description="FastAPI application providing OpenAI-compatible endpoints using QodoAI",
482
+ version="1.0.0",
483
+ lifespan=lifespan
484
+ )
485
+
486
+ # Add CORS middleware
487
+ app.add_middleware(
488
+ CORSMiddleware,
489
+ allow_origins=["*"],
490
+ allow_credentials=True,
491
+ allow_methods=["*"],
492
+
493
+ allow_headers=["*"],
494
+ )
495
+
496
+ # Authentication dependency
497
+ async def verify_api_key(credentials: HTTPAuthorizationCredentials = Depends(security)):
498
+ """Verify API key from Authorization header."""
499
+ if not credentials:
500
+ raise HTTPException(
501
+ status_code=status.HTTP_401_UNAUTHORIZED,
502
+ detail="Missing API key",
503
+ headers={"WWW-Authenticate": "Bearer"},
504
+ )
505
+
506
+ # In a production environment, you would validate the API key here
507
+ # For now, we accept any non-empty key
508
+ if not credentials.credentials:
509
+ raise HTTPException(
510
+ status_code=status.HTTP_401_UNAUTHORIZED,
511
+ detail="Invalid API key",
512
+ headers={"WWW-Authenticate": "Bearer"},
513
+ )
514
+
515
+ return credentials.credentials
516
+
517
+ # ============================================================================
518
+ # API Endpoints
519
+ # ============================================================================
520
+
521
+ @app.get("/health", response_model=HealthResponse)
522
+ async def health_check():
523
+ """Health check endpoint."""
524
+ return HealthResponse(
525
+ status="healthy",
526
+ timestamp=int(time.time())
527
+ )
528
+
529
+ @app.get("/v1/models", response_model=ModelListResponse)
530
+ async def list_models(api_key: str = Depends(verify_api_key)):
531
+ """List available models."""
532
+ try:
533
+ models = []
534
+ for model_id in QodoAI.AVAILABLE_MODELS:
535
+ models.append(ModelInfo(
536
+ id=model_id,
537
+ created=int(time.time()),
538
+ owned_by="qodo"
539
+ ))
540
+
541
+ return ModelListResponse(data=models)
542
+
543
+ except Exception as e:
544
+ logger.error(f"Error listing models: {e}")
545
+ raise HTTPException(status_code=500, detail="Failed to list models")
546
+
547
+ @app.post("/v1/chat/completions")
548
+ async def create_chat_completion(
549
+ request: ChatCompletion
550
+ Request,
551
+ api_key: str = Depends(verify_api_key)
552
+ ):
553
+ """Create a chat completion."""
554
+ try:
555
+ # Validate model
556
+ if request.model not in QodoAI.AVAILABLE_MODELS:
557
+ raise HTTPException(
558
+ status_code=400,
559
+ detail=f"Model '{request.model}' is not available. Available models: {QodoAI.AVAILABLE_MODELS}"
560
+ )
561
+
562
+ # Create chat completion
563
+ result = await qodo_client.create_chat_completion(request)
564
+
565
+ if request.stream:
566
+ # Return streaming response
567
+ return StreamingResponse(
568
+ result,
569
+ media_type="text/plain",
570
+ headers={
571
+ "Cache-Control": "no-cache",
572
+ "Connection": "keep-alive",
573
+ "Content-Type": "text/plain; charset=utf-8"
574
+ }
575
+ )
576
+ else:
577
+ # Return non-streaming response
578
+ return result
579
+
580
+ except HTTPException:
581
+ raise
582
+ except Exception as e:
583
+ logger.error(f"Error creating chat completion: {e}")
584
+ raise HTTPException(status_code=500, detail="Failed to create chat completion")
585
+
586
+ @app.exception_handler(Exception)
587
+ async def global_exception_handler(request: Request, exc: Exception):
588
+ """Global exception handler."""
589
+ logger.error(f"Unhandled exception: {exc}")
590
+ return JSONResponse(
591
+ status_code=500,
592
+ content={"error": {"message": "Internal server error", "type": "internal_error"}}
593
+ )
594
+
595
+ @app.middleware("http")
596
+ async def log_requests(request: Request, call_next):
597
+ """Log all requests."""
598
+ start_time = time.time()
599
+
600
+ # Log request
601
+ logger.info(f"{request.method} {request.url.path} - Start")
602
+
603
+ response
604
+ = await call_next(request)
605
+
606
+ # Log response
607
+ process_time = time.time() - start_time
608
+ logger.info(f"{request.method} {request.url.path} - {response.status_code} - {process_time:.3f}s")
609
+
610
+ return response
611
+
612
+ # ============================================================================
613
+ # Main Application Entry Point
614
+ # ============================================================================
615
+
616
+ if __name__ == "__main__":
617
+ uvicorn.run(
618
+ "main:app",
619
+ host="0.0.0.0",
620
+ port=8000,
621
+ reload=True,
622
+ log_level="info"
623
+ )