AIMaster7 commited on
Commit
fcc1420
·
verified ·
1 Parent(s): e6b7148

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +428 -0
main.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import uuid
5
+ from typing import List, Dict, Optional, Union, Generator, Any
6
+
7
+ # --- Core Dependencies ---
8
+ import uvicorn
9
+ from fastapi import FastAPI, HTTPException, Request
10
+ from fastapi.responses import JSONResponse, StreamingResponse
11
+ from pydantic import BaseModel, Field
12
+ from curl_cffi.requests import Session
13
+ from curl_cffi import CurlError
14
+
15
+ # --- Environment Configuration ---
16
+ QODO_API_KEY = os.getenv("QODO_API_KEY", "useme")
17
+ QODO_URL = os.getenv("QODO_URL", "https://hello.com")
18
+ QODO_INFO_URL = os.getenv("QODO_INFO_URL", "https://openai.com")
19
+
20
+ # --- Recreated/Mocked webscout Dependencies ---
21
+ # This section recreates the necessary classes and functions
22
+ # to make the QodoAI provider self-contained.
23
+
24
+ # webscout.exceptions
25
+ class exceptions:
26
+ class FailedToGenerateResponseError(Exception):
27
+ pass
28
+
29
+ # webscout.AIutel.sanitize_stream
30
+ def sanitize_stream(data: Generator[bytes, None, None], content_extractor: callable, **kwargs: Any) -> Generator[str, None, None]:
31
+ """
32
+ Parses a stream of byte chunks, extracts complete JSON objects,
33
+ and yields content processed by the content_extractor.
34
+ """
35
+ buffer = ""
36
+ for byte_chunk in data:
37
+ buffer += byte_chunk.decode('utf-8', errors='ignore')
38
+
39
+ start_index = 0
40
+ while True:
41
+ # Find the start of a potential JSON object
42
+ try:
43
+ obj_start = buffer.index('{', start_index)
44
+ except ValueError:
45
+ # No more objects in buffer, keep the remainder for the next chunk
46
+ buffer = buffer[start_index:]
47
+ break
48
+
49
+ # Find the corresponding end brace
50
+ brace_count = 1
51
+ i = obj_start + 1
52
+ while i < len(buffer) and brace_count > 0:
53
+ if buffer[i] == '{':
54
+ brace_count += 1
55
+ elif buffer[i] == '}':
56
+ brace_count -= 1
57
+ i += 1
58
+
59
+ if brace_count == 0: # Found a complete object
60
+ json_str = buffer[obj_start:i]
61
+ try:
62
+ json_obj = json.loads(json_str)
63
+ content = content_extractor(json_obj)
64
+ if content:
65
+ yield content
66
+ except json.JSONDecodeError:
67
+ pass # Skip malformed JSON
68
+ start_index = i # Move past the processed object
69
+ else:
70
+ # Incomplete object, wait for more data
71
+ buffer = buffer[start_index:]
72
+ break
73
+
74
+ # webscout.Provider.OPENAI.utils (Pydantic Models)
75
+ class Tool(BaseModel):
76
+ type: str = "function"
77
+ function: Dict[str, Any]
78
+
79
+ class ChatCompletionMessage(BaseModel):
80
+ role: str
81
+ content: Optional[str] = None
82
+ tool_calls: Optional[List[Dict]] = None
83
+
84
+ class Choice(BaseModel):
85
+ index: int
86
+ message: Optional[ChatCompletionMessage] = None
87
+ finish_reason: Optional[str] = None
88
+ delta: Optional[Dict] = Field(default_factory=dict)
89
+
90
+ class ChoiceDelta(BaseModel):
91
+ content: Optional[str] = None
92
+ role: Optional[str] = None
93
+
94
+ class ChoiceStreaming(BaseModel):
95
+ index: int
96
+ delta: ChoiceDelta
97
+ finish_reason: Optional[str] = None
98
+
99
+ class CompletionUsage(BaseModel):
100
+ prompt_tokens: int
101
+ completion_tokens: int
102
+ total_tokens: int
103
+
104
+ class ChatCompletion(BaseModel):
105
+ id: str
106
+ choices: List[Choice]
107
+ created: int
108
+ model: str
109
+ object: str = "chat.completion"
110
+ usage: CompletionUsage
111
+
112
+ class ChatCompletionChunk(BaseModel):
113
+ id: str
114
+ choices: List[ChoiceStreaming]
115
+ created: int
116
+ model: str
117
+ object: str = "chat.completion.chunk"
118
+ usage: Optional[CompletionUsage] = None
119
+
120
+ # webscout.Provider.OPENAI.base
121
+ class BaseCompletions:
122
+ def __init__(self, client: Any):
123
+ self._client = client
124
+
125
+ class BaseChat:
126
+ def __init__(self, client: Any):
127
+ self.completions = Completions(client)
128
+
129
+ class OpenAICompatibleProvider:
130
+ def __init__(self, **kwargs: Any):
131
+ pass
132
+
133
+ # Attempt to import LitAgent, fallback if not available
134
+ try:
135
+ from webscout.litagent import LitAgent
136
+ except ImportError:
137
+ LitAgent = None
138
+
139
+ # --- QodoAI Provider Code (from the prompt) ---
140
+
141
+ class Completions(BaseCompletions):
142
+ def create(
143
+ self,
144
+ *,
145
+ model: str,
146
+ messages: List[Dict[str, Any]],
147
+ stream: bool = False,
148
+ **kwargs: Any
149
+ ) -> Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]]:
150
+ """
151
+ Creates a model response for the given chat conversation.
152
+ Mimics openai.chat.completions.create
153
+ """
154
+ user_prompt = ""
155
+ for message in reversed(messages):
156
+ if message.get("role") == "user":
157
+ user_prompt = message.get("content", "")
158
+ break
159
+
160
+ if not user_prompt:
161
+ raise ValueError("No user message found in messages")
162
+
163
+ payload = self._client._build_payload(user_prompt, model)
164
+ payload["stream"] = stream
165
+ payload["custom_model"] = model
166
+
167
+ request_id = f"chatcmpl-{uuid.uuid4()}"
168
+ created_time = int(time.time())
169
+
170
+ if stream:
171
+ return self._create_stream(request_id, created_time, model, payload, user_prompt)
172
+ else:
173
+ return self._create_non_stream(request_id, created_time, model, payload, user_prompt)
174
+
175
+ def _create_stream(
176
+ self, request_id: str, created_time: int, model: str, payload: Dict[str, Any], user_prompt: str
177
+ ) -> Generator[ChatCompletionChunk, None, None]:
178
+ try:
179
+ response = self._client.session.post(
180
+ self._client.url,
181
+ json=payload,
182
+ stream=True,
183
+ timeout=self._client.timeout,
184
+ impersonate="chrome110"
185
+ )
186
+
187
+ if response.status_code == 401:
188
+ raise exceptions.FailedToGenerateResponseError("Invalid Qodo API key provided.")
189
+ elif response.status_code != 200:
190
+ raise IOError(f"Qodo request failed with status code {response.status_code}: {response.text}")
191
+
192
+ prompt_tokens = len(user_prompt.split())
193
+ completion_tokens = 0
194
+
195
+ processed_stream = sanitize_stream(
196
+ data=response.iter_content(chunk_size=None),
197
+ content_extractor=QodoAI._qodo_extractor
198
+ )
199
+
200
+ for content_chunk in processed_stream:
201
+ if content_chunk:
202
+ completion_tokens += len(content_chunk.split())
203
+
204
+ delta = ChoiceDelta(content=content_chunk, role="assistant")
205
+ choice = ChoiceStreaming(index=0, delta=delta, finish_reason=None)
206
+ chunk = ChatCompletionChunk(id=request_id, choices=[choice], created=created_time, model=model)
207
+ yield chunk
208
+
209
+ final_choice = ChoiceStreaming(index=0, delta=ChoiceDelta(), finish_reason="stop")
210
+ yield ChatCompletionChunk(id=request_id, choices=[final_choice], created=created_time, model=model)
211
+
212
+ except CurlError as e:
213
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}")
214
+ except Exception as e:
215
+ raise exceptions.FailedToGenerateResponseError(f"An unexpected error occurred ({type(e).__name__}): {e}")
216
+
217
+ def _create_non_stream(
218
+ self, request_id: str, created_time: int, model: str, payload: Dict[str, Any], user_prompt: str
219
+ ) -> ChatCompletion:
220
+ try:
221
+ payload["stream"] = False
222
+ response = self._client.session.post(
223
+ self._client.url,
224
+ json=payload,
225
+ timeout=self._client.timeout,
226
+ impersonate="chrome110"
227
+ )
228
+
229
+ if response.status_code == 401:
230
+ raise exceptions.FailedToGenerateResponseError("Invalid Qodo API key provided.")
231
+ elif response.status_code != 200:
232
+ raise IOError(f"Qodo request failed with status code {response.status_code}: {response.text}")
233
+
234
+ response_text = response.text
235
+ full_response = ""
236
+
237
+ # This logic parses concatenated JSON objects from the response body.
238
+ current_json = ""
239
+ brace_count = 0
240
+ json_objects = []
241
+ lines = response_text.strip().split('\n')
242
+ for line in lines:
243
+ current_json += line
244
+ brace_count += line.count('{') - line.count('}')
245
+ if brace_count == 0 and current_json:
246
+ json_objects.append(current_json)
247
+ current_json = ""
248
+
249
+ for json_str in json_objects:
250
+ try:
251
+ json_obj = json.loads(json_str)
252
+ content = QodoAI._qodo_extractor(json_obj)
253
+ if content:
254
+ full_response += content
255
+ except json.JSONDecodeError:
256
+ pass
257
+
258
+ prompt_tokens = len(user_prompt.split())
259
+ completion_tokens = len(full_response.split())
260
+ total_tokens = prompt_tokens + completion_tokens
261
+
262
+ message = ChatCompletionMessage(role="assistant", content=full_response)
263
+ choice = Choice(index=0, message=message, finish_reason="stop")
264
+ usage = CompletionUsage(prompt_tokens=prompt_tokens, completion_tokens=completion_tokens, total_tokens=total_tokens)
265
+ return ChatCompletion(id=request_id, choices=[choice], created=created_time, model=model, usage=usage)
266
+
267
+ except CurlError as e:
268
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}")
269
+ except Exception as e:
270
+ raise exceptions.FailedToGenerateResponseError(f"Request failed ({type(e).__name__}): {e}")
271
+
272
+ class Chat(BaseChat):
273
+ def __init__(self, client: 'QodoAI'):
274
+ self.completions = Completions(client)
275
+
276
+ class QodoAI(OpenAICompatibleProvider):
277
+ AVAILABLE_MODELS = ["gpt-4.1", "gpt-4o", "o3", "o4-mini", "claude-4-sonnet", "gemini-2.5-pro"]
278
+
279
+ def __init__(self, api_key: str, **kwargs: Any):
280
+ super().__init__(api_key=api_key, **kwargs)
281
+
282
+ self.url = QODO_URL
283
+ self.info_url = QODO_INFO_URL
284
+ self.timeout = 600
285
+ self.api_key = api_key
286
+
287
+ self.user_agent = "axios/1.10.0"
288
+ self.session_id = self._get_session_id()
289
+ self.request_id = str(uuid.uuid4())
290
+
291
+ self.headers = {
292
+ "Accept": "text/plain", "Accept-Encoding": "gzip, deflate, br, zstd",
293
+ "Accept-Language": "en-US,en;q=0.9", "Authorization": f"Bearer {self.api_key}",
294
+ "Connection": "close", "Content-Type": "application/json",
295
+ "host": "api.cli.qodo.ai", "Request-id": self.request_id,
296
+ "Session-id": self.session_id, "User-Agent": self.user_agent,
297
+ }
298
+
299
+ self.session = Session()
300
+ self.session.headers.update(self.headers)
301
+ self.chat = Chat(self)
302
+
303
+ @staticmethod
304
+ def _qodo_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
305
+ if isinstance(chunk, dict):
306
+ data = chunk.get("data", {})
307
+ if isinstance(data, dict):
308
+ tool_args = data.get("tool_args", {})
309
+ if isinstance(tool_args, dict) and "content" in tool_args:
310
+ return tool_args.get("content")
311
+ if "content" in data:
312
+ return data["content"]
313
+ return None
314
+
315
+ def _get_session_id(self) -> str:
316
+ try:
317
+ temp_session = Session()
318
+ temp_headers = {
319
+ "Authorization": f"Bearer {self.api_key}",
320
+ "User-Agent": self.user_agent,
321
+ }
322
+ temp_session.headers.update(temp_headers)
323
+
324
+ response = temp_session.get(self.info_url, timeout=self.timeout, impersonate="chrome110")
325
+
326
+ if response.status_code == 200:
327
+ return response.json().get("session-id", f"fallback-{uuid.uuid4()}")
328
+ elif response.status_code == 401:
329
+ raise exceptions.FailedToGenerateResponseError("Invalid Qodo API key. Please check your QODO_API_KEY environment variable.")
330
+ else:
331
+ raise exceptions.FailedToGenerateResponseError(f"Failed to get session_id from Qodo: HTTP {response.status_code}")
332
+ except Exception as e:
333
+ raise exceptions.FailedToGenerateResponseError(f"Failed to connect to Qodo API to get session_id: {e}")
334
+
335
+ def _build_payload(self, prompt: str, model: str) -> Dict[str, Any]:
336
+ return {
337
+ "agent_type": "cli", "session_id": self.session_id,
338
+ "user_data": {"extension_version": "0.7.2", "os_platform": "win32"},
339
+ "tools": {"web_search": []}, "user_request": prompt,
340
+ "execution_strategy": "act", "custom_model": model, "stream": True
341
+ }
342
+
343
+ # --- FastAPI Application ---
344
+
345
+ app = FastAPI(
346
+ title="QodoAI OpenAI-Compatible API",
347
+ description="Provides an OpenAI-compatible interface for the QodoAI service.",
348
+ version="1.0.0"
349
+ )
350
+
351
+ # Initialize the client at startup
352
+ try:
353
+ client = QodoAI(api_key=QODO_API_KEY)
354
+ except exceptions.FailedToGenerateResponseError as e:
355
+ print(f"FATAL: Could not initialize QodoAI client: {e}")
356
+ print("Please ensure the QODO_API_KEY environment variable is set correctly.")
357
+ client = None
358
+
359
+ # --- API Models ---
360
+
361
+ class Model(BaseModel):
362
+ id: str
363
+ object: str = "model"
364
+ created: int = Field(default_factory=lambda: int(time.time()))
365
+ owned_by: str = "qodoai"
366
+
367
+ class ModelList(BaseModel):
368
+ object: str = "list"
369
+ data: List[Model]
370
+
371
+ class ChatCompletionRequest(BaseModel):
372
+ model: str
373
+ messages: List[Dict[str, Any]]
374
+ max_tokens: Optional[int] = 2049
375
+ stream: bool = False
376
+ temperature: Optional[float] = None
377
+ top_p: Optional[float] = None
378
+ tools: Optional[List[Dict[str, Any]]] = None
379
+ tool_choice: Optional[str] = None
380
+
381
+ # --- API Endpoints ---
382
+
383
+ @app.on_event("startup")
384
+ async def startup_event():
385
+ if client is None:
386
+ # This will prevent the app from starting if the client failed to init
387
+ raise RuntimeError("QodoAI client could not be initialized. Check API key and connectivity.")
388
+ print("QodoAI client initialized successfully.")
389
+
390
+ @app.get("/v1/models", response_model=ModelList)
391
+ async def list_models():
392
+ """Lists the available models from the QodoAI provider."""
393
+ model_data = [Model(id=model_id) for model_id in QodoAI.AVAILABLE_MODELS]
394
+ return ModelList(data=model_data)
395
+
396
+ @app.post("/v1/chat/completions")
397
+ async def create_chat_completion(request: ChatCompletionRequest):
398
+ """Creates a chat completion, supporting both streaming and non-streaming modes."""
399
+ if client is None:
400
+ raise HTTPException(status_code=500, detail="QodoAI client is not available.")
401
+
402
+ params = request.model_dump(exclude_none=True)
403
+
404
+ try:
405
+ if request.stream:
406
+ async def stream_generator():
407
+ try:
408
+ generator = client.chat.completions.create(**params)
409
+ for chunk in generator:
410
+ yield f"data: {chunk.model_dump_json()}\n\n"
411
+ yield "data: [DONE]\n\n"
412
+ except exceptions.FailedToGenerateResponseError as e:
413
+ error_payload = {"error": {"message": str(e), "type": "api_error"}}
414
+ yield f"data: {json.dumps(error_payload)}\n\n"
415
+ yield "data: [DONE]\n\n"
416
+
417
+ return StreamingResponse(stream_generator(), media_type="text/event-stream")
418
+ else:
419
+ response = client.chat.completions.create(**params)
420
+ return JSONResponse(content=response.model_dump())
421
+
422
+ except exceptions.FailedToGenerateResponseError as e:
423
+ raise HTTPException(status_code=500, detail=str(e))
424
+ except ValueError as e:
425
+ raise HTTPException(status_code=400, detail=str(e))
426
+
427
+ if __name__ == "__main__":
428
+ uvicorn.run(app, host="0.0.0.0", port=8000)