webscout 8.3.1__py3-none-any.whl → 8.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of webscout might be problematic. Click here for more details.
- webscout/AIutel.py +180 -78
- webscout/Bing_search.py +417 -0
- webscout/Extra/gguf.py +706 -177
- webscout/Provider/AISEARCH/__init__.py +1 -0
- webscout/Provider/AISEARCH/genspark_search.py +7 -7
- webscout/Provider/AISEARCH/stellar_search.py +132 -0
- webscout/Provider/ExaChat.py +84 -58
- webscout/Provider/GeminiProxy.py +140 -0
- webscout/Provider/HeckAI.py +85 -80
- webscout/Provider/Jadve.py +56 -50
- webscout/Provider/MCPCore.py +78 -75
- webscout/Provider/MiniMax.py +207 -0
- webscout/Provider/Nemotron.py +41 -13
- webscout/Provider/Netwrck.py +34 -51
- webscout/Provider/OPENAI/BLACKBOXAI.py +0 -4
- webscout/Provider/OPENAI/GeminiProxy.py +328 -0
- webscout/Provider/OPENAI/MiniMax.py +298 -0
- webscout/Provider/OPENAI/README.md +32 -29
- webscout/Provider/OPENAI/README_AUTOPROXY.md +238 -0
- webscout/Provider/OPENAI/TogetherAI.py +4 -17
- webscout/Provider/OPENAI/__init__.py +17 -1
- webscout/Provider/OPENAI/autoproxy.py +1067 -39
- webscout/Provider/OPENAI/base.py +17 -76
- webscout/Provider/OPENAI/deepinfra.py +42 -108
- webscout/Provider/OPENAI/e2b.py +0 -1
- webscout/Provider/OPENAI/flowith.py +179 -166
- webscout/Provider/OPENAI/friendli.py +233 -0
- webscout/Provider/OPENAI/mcpcore.py +109 -70
- webscout/Provider/OPENAI/monochat.py +329 -0
- webscout/Provider/OPENAI/pydantic_imports.py +1 -172
- webscout/Provider/OPENAI/scirachat.py +59 -51
- webscout/Provider/OPENAI/toolbaz.py +3 -9
- webscout/Provider/OPENAI/typegpt.py +1 -1
- webscout/Provider/OPENAI/utils.py +19 -42
- webscout/Provider/OPENAI/x0gpt.py +14 -2
- webscout/Provider/OPENAI/xenai.py +514 -0
- webscout/Provider/OPENAI/yep.py +8 -2
- webscout/Provider/OpenGPT.py +54 -32
- webscout/Provider/PI.py +58 -84
- webscout/Provider/StandardInput.py +32 -13
- webscout/Provider/TTI/README.md +9 -9
- webscout/Provider/TTI/__init__.py +3 -1
- webscout/Provider/TTI/aiarta.py +92 -78
- webscout/Provider/TTI/bing.py +231 -0
- webscout/Provider/TTI/infip.py +212 -0
- webscout/Provider/TTI/monochat.py +220 -0
- webscout/Provider/TTS/speechma.py +45 -39
- webscout/Provider/TeachAnything.py +11 -3
- webscout/Provider/TextPollinationsAI.py +78 -70
- webscout/Provider/TogetherAI.py +350 -0
- webscout/Provider/Venice.py +37 -46
- webscout/Provider/VercelAI.py +27 -24
- webscout/Provider/WiseCat.py +35 -35
- webscout/Provider/WrDoChat.py +22 -26
- webscout/Provider/WritingMate.py +26 -22
- webscout/Provider/XenAI.py +324 -0
- webscout/Provider/__init__.py +10 -5
- webscout/Provider/deepseek_assistant.py +378 -0
- webscout/Provider/granite.py +48 -57
- webscout/Provider/koala.py +51 -39
- webscout/Provider/learnfastai.py +49 -64
- webscout/Provider/llmchat.py +79 -93
- webscout/Provider/llmchatco.py +63 -78
- webscout/Provider/multichat.py +51 -40
- webscout/Provider/oivscode.py +1 -1
- webscout/Provider/scira_chat.py +159 -96
- webscout/Provider/scnet.py +13 -13
- webscout/Provider/searchchat.py +13 -13
- webscout/Provider/sonus.py +12 -11
- webscout/Provider/toolbaz.py +25 -8
- webscout/Provider/turboseek.py +41 -42
- webscout/Provider/typefully.py +27 -12
- webscout/Provider/typegpt.py +41 -46
- webscout/Provider/uncovr.py +55 -90
- webscout/Provider/x0gpt.py +33 -17
- webscout/Provider/yep.py +79 -96
- webscout/auth/__init__.py +55 -0
- webscout/auth/api_key_manager.py +189 -0
- webscout/auth/auth_system.py +100 -0
- webscout/auth/config.py +76 -0
- webscout/auth/database.py +400 -0
- webscout/auth/exceptions.py +67 -0
- webscout/auth/middleware.py +248 -0
- webscout/auth/models.py +130 -0
- webscout/auth/providers.py +279 -0
- webscout/auth/rate_limiter.py +254 -0
- webscout/auth/request_models.py +127 -0
- webscout/auth/request_processing.py +226 -0
- webscout/auth/routes.py +550 -0
- webscout/auth/schemas.py +103 -0
- webscout/auth/server.py +367 -0
- webscout/client.py +121 -70
- webscout/litagent/Readme.md +68 -55
- webscout/litagent/agent.py +99 -9
- webscout/scout/core/scout.py +104 -26
- webscout/scout/element.py +139 -18
- webscout/swiftcli/core/cli.py +14 -3
- webscout/swiftcli/decorators/output.py +59 -9
- webscout/update_checker.py +31 -49
- webscout/version.py +1 -1
- webscout/webscout_search.py +4 -12
- webscout/webscout_search_async.py +3 -10
- webscout/yep_search.py +2 -11
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/METADATA +141 -99
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/RECORD +109 -83
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/entry_points.txt +1 -1
- webscout/Provider/HF_space/__init__.py +0 -0
- webscout/Provider/HF_space/qwen_qwen2.py +0 -206
- webscout/Provider/OPENAI/api.py +0 -1320
- webscout/Provider/TTI/fastflux.py +0 -233
- webscout/Provider/Writecream.py +0 -246
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/WHEEL +0 -0
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import uuid
|
|
4
|
+
import base64
|
|
5
|
+
from typing import List, Dict, Optional, Union, Generator, Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
# Import base classes and utility structures
|
|
11
|
+
from .base import OpenAICompatibleProvider, BaseChat, BaseCompletions
|
|
12
|
+
from .utils import (
|
|
13
|
+
ChatCompletionChunk, ChatCompletion, Choice, ChoiceDelta,
|
|
14
|
+
ChatCompletionMessage, CompletionUsage, count_tokens
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from webscout.litagent import LitAgent
|
|
18
|
+
from webscout import exceptions
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Completions(BaseCompletions):
|
|
22
|
+
def __init__(self, client: 'GeminiProxy'):
|
|
23
|
+
self._client = client
|
|
24
|
+
|
|
25
|
+
def create(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
model: str,
|
|
29
|
+
messages: List[Dict[str, str]],
|
|
30
|
+
max_tokens: Optional[int] = None,
|
|
31
|
+
stream: bool = False,
|
|
32
|
+
temperature: Optional[float] = None,
|
|
33
|
+
top_p: Optional[float] = None,
|
|
34
|
+
timeout: Optional[int] = None,
|
|
35
|
+
proxies: Optional[dict] = None,
|
|
36
|
+
**kwargs: Any
|
|
37
|
+
) -> Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]]:
|
|
38
|
+
"""
|
|
39
|
+
Create a chat completion with GeminiProxy API.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
model: The model to use (from AVAILABLE_MODELS)
|
|
43
|
+
messages: List of message dictionaries with 'role' and 'content'
|
|
44
|
+
max_tokens: Maximum number of tokens to generate
|
|
45
|
+
stream: Whether to stream the response (not supported by GeminiProxy)
|
|
46
|
+
temperature: Sampling temperature (0-1)
|
|
47
|
+
top_p: Nucleus sampling parameter (0-1)
|
|
48
|
+
timeout: Request timeout in seconds
|
|
49
|
+
proxies: Proxy configuration
|
|
50
|
+
**kwargs: Additional parameters to pass to the API
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
If stream=False, returns a ChatCompletion object
|
|
54
|
+
If stream=True, returns a Generator yielding ChatCompletionChunk objects
|
|
55
|
+
"""
|
|
56
|
+
# Generate request ID and timestamp
|
|
57
|
+
request_id = str(uuid.uuid4())
|
|
58
|
+
created_time = int(time.time())
|
|
59
|
+
|
|
60
|
+
# Extract image URL from kwargs if present
|
|
61
|
+
img_url = kwargs.get('img_url')
|
|
62
|
+
|
|
63
|
+
# Convert messages to GeminiProxy format
|
|
64
|
+
conversation_prompt = self._format_messages(messages)
|
|
65
|
+
|
|
66
|
+
# Prepare parts for the request
|
|
67
|
+
parts = []
|
|
68
|
+
if img_url:
|
|
69
|
+
parts.append({"inline_data": self._get_image(img_url, proxies, timeout)})
|
|
70
|
+
parts.append({"text": conversation_prompt})
|
|
71
|
+
|
|
72
|
+
# Prepare the payload
|
|
73
|
+
payload = {
|
|
74
|
+
"model": model,
|
|
75
|
+
"contents": [{"parts": parts}]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# GeminiProxy doesn't support streaming, so we always return non-streaming
|
|
79
|
+
if stream:
|
|
80
|
+
return self._create_streaming_fallback(
|
|
81
|
+
request_id=request_id,
|
|
82
|
+
created_time=created_time,
|
|
83
|
+
model=model,
|
|
84
|
+
payload=payload,
|
|
85
|
+
timeout=timeout,
|
|
86
|
+
proxies=proxies
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Non-streaming implementation
|
|
90
|
+
return self._create_non_streaming(
|
|
91
|
+
request_id=request_id,
|
|
92
|
+
created_time=created_time,
|
|
93
|
+
model=model,
|
|
94
|
+
payload=payload,
|
|
95
|
+
timeout=timeout,
|
|
96
|
+
proxies=proxies
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _format_messages(self, messages: List[Dict[str, str]]) -> str:
|
|
100
|
+
"""Convert OpenAI messages format to a single conversation prompt."""
|
|
101
|
+
formatted_parts = []
|
|
102
|
+
|
|
103
|
+
for message in messages:
|
|
104
|
+
role = message.get("role", "")
|
|
105
|
+
content = message.get("content", "")
|
|
106
|
+
|
|
107
|
+
if role == "system":
|
|
108
|
+
formatted_parts.append(f"System: {content}")
|
|
109
|
+
elif role == "user":
|
|
110
|
+
formatted_parts.append(f"User: {content}")
|
|
111
|
+
elif role == "assistant":
|
|
112
|
+
formatted_parts.append(f"Assistant: {content}")
|
|
113
|
+
|
|
114
|
+
return "\n".join(formatted_parts)
|
|
115
|
+
|
|
116
|
+
def _get_image(self, img_url: str, proxies: Optional[dict] = None, timeout: Optional[int] = None) -> Dict[str, str]:
|
|
117
|
+
"""Fetch and encode image from URL."""
|
|
118
|
+
try:
|
|
119
|
+
session = requests.Session()
|
|
120
|
+
if proxies:
|
|
121
|
+
session.proxies.update(proxies)
|
|
122
|
+
|
|
123
|
+
response = session.get(
|
|
124
|
+
img_url,
|
|
125
|
+
stream=True,
|
|
126
|
+
timeout=timeout or self._client.timeout
|
|
127
|
+
)
|
|
128
|
+
response.raise_for_status()
|
|
129
|
+
|
|
130
|
+
mime_type = response.headers.get("content-type", "application/octet-stream")
|
|
131
|
+
data = base64.b64encode(response.content).decode("utf-8")
|
|
132
|
+
return {"mime_type": mime_type, "data": data}
|
|
133
|
+
except Exception as e:
|
|
134
|
+
raise exceptions.FailedToGenerateResponseError(f"Error fetching image: {e}")
|
|
135
|
+
|
|
136
|
+
def _create_non_streaming(
|
|
137
|
+
self,
|
|
138
|
+
*,
|
|
139
|
+
request_id: str,
|
|
140
|
+
created_time: int,
|
|
141
|
+
model: str,
|
|
142
|
+
payload: Dict[str, Any],
|
|
143
|
+
timeout: Optional[int] = None,
|
|
144
|
+
proxies: Optional[dict] = None
|
|
145
|
+
) -> ChatCompletion:
|
|
146
|
+
"""Implementation for non-streaming chat completions."""
|
|
147
|
+
original_proxies = self._client.session.proxies.copy()
|
|
148
|
+
if proxies is not None:
|
|
149
|
+
self._client.session.proxies.update(proxies)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
response = self._client.session.post(
|
|
153
|
+
self._client.base_url,
|
|
154
|
+
json=payload,
|
|
155
|
+
headers=self._client.headers,
|
|
156
|
+
timeout=timeout if timeout is not None else self._client.timeout
|
|
157
|
+
)
|
|
158
|
+
response.raise_for_status()
|
|
159
|
+
data = response.json()
|
|
160
|
+
|
|
161
|
+
# Extract content from GeminiProxy response
|
|
162
|
+
content = self._extract_content(data)
|
|
163
|
+
|
|
164
|
+
# Create the completion message
|
|
165
|
+
message = ChatCompletionMessage(
|
|
166
|
+
role="assistant",
|
|
167
|
+
content=content
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Create the choice
|
|
171
|
+
choice = Choice(
|
|
172
|
+
index=0,
|
|
173
|
+
message=message,
|
|
174
|
+
finish_reason="stop"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Estimate token usage
|
|
178
|
+
prompt_tokens = count_tokens([msg.get("content", "") for msg in payload.get("contents", [{}])[0].get("parts", [{}])])
|
|
179
|
+
completion_tokens = count_tokens(content)
|
|
180
|
+
usage = CompletionUsage(
|
|
181
|
+
prompt_tokens=prompt_tokens,
|
|
182
|
+
completion_tokens=completion_tokens,
|
|
183
|
+
total_tokens=prompt_tokens + completion_tokens
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Create the completion object
|
|
187
|
+
completion = ChatCompletion(
|
|
188
|
+
id=request_id,
|
|
189
|
+
choices=[choice],
|
|
190
|
+
created=created_time,
|
|
191
|
+
model=model,
|
|
192
|
+
usage=usage,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return completion
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
raise exceptions.FailedToGenerateResponseError(f"GeminiProxy request failed: {e}")
|
|
199
|
+
finally:
|
|
200
|
+
if proxies is not None:
|
|
201
|
+
self._client.session.proxies = original_proxies
|
|
202
|
+
|
|
203
|
+
def _create_streaming_fallback(
|
|
204
|
+
self,
|
|
205
|
+
*,
|
|
206
|
+
request_id: str,
|
|
207
|
+
created_time: int,
|
|
208
|
+
model: str,
|
|
209
|
+
payload: Dict[str, Any],
|
|
210
|
+
timeout: Optional[int] = None,
|
|
211
|
+
proxies: Optional[dict] = None
|
|
212
|
+
) -> Generator[ChatCompletionChunk, None, None]:
|
|
213
|
+
"""Fallback streaming implementation that simulates streaming from non-streaming response."""
|
|
214
|
+
# Get the full response first
|
|
215
|
+
completion = self._create_non_streaming(
|
|
216
|
+
request_id=request_id,
|
|
217
|
+
created_time=created_time,
|
|
218
|
+
model=model,
|
|
219
|
+
payload=payload,
|
|
220
|
+
timeout=timeout,
|
|
221
|
+
proxies=proxies
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Simulate streaming by yielding chunks
|
|
225
|
+
content = completion.choices[0].message.content
|
|
226
|
+
if content:
|
|
227
|
+
# Split content into chunks (simulate streaming)
|
|
228
|
+
chunk_size = max(1, len(content) // 10) # Split into ~10 chunks
|
|
229
|
+
for i in range(0, len(content), chunk_size):
|
|
230
|
+
chunk_content = content[i:i + chunk_size]
|
|
231
|
+
|
|
232
|
+
delta = ChoiceDelta(content=chunk_content)
|
|
233
|
+
choice = Choice(index=0, delta=delta, finish_reason=None)
|
|
234
|
+
|
|
235
|
+
chunk = ChatCompletionChunk(
|
|
236
|
+
id=request_id,
|
|
237
|
+
choices=[choice],
|
|
238
|
+
created=created_time,
|
|
239
|
+
model=model
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
yield chunk
|
|
243
|
+
|
|
244
|
+
# Final chunk with finish_reason
|
|
245
|
+
delta = ChoiceDelta(content=None)
|
|
246
|
+
choice = Choice(index=0, delta=delta, finish_reason="stop")
|
|
247
|
+
chunk = ChatCompletionChunk(
|
|
248
|
+
id=request_id,
|
|
249
|
+
choices=[choice],
|
|
250
|
+
created=created_time,
|
|
251
|
+
model=model
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
yield chunk
|
|
255
|
+
|
|
256
|
+
def _extract_content(self, response: dict) -> str:
|
|
257
|
+
"""Extract content from GeminiProxy response."""
|
|
258
|
+
try:
|
|
259
|
+
return response['candidates'][0]['content']['parts'][0]['text']
|
|
260
|
+
except (KeyError, IndexError, TypeError):
|
|
261
|
+
return str(response)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class Chat(BaseChat):
|
|
265
|
+
def __init__(self, client: 'GeminiProxy'):
|
|
266
|
+
self.completions = Completions(client)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class GeminiProxy(OpenAICompatibleProvider):
|
|
270
|
+
"""
|
|
271
|
+
OpenAI-compatible client for GeminiProxy API.
|
|
272
|
+
|
|
273
|
+
Usage:
|
|
274
|
+
client = GeminiProxy()
|
|
275
|
+
response = client.chat.completions.create(
|
|
276
|
+
model="gemini-2.0-flash-lite",
|
|
277
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
278
|
+
)
|
|
279
|
+
print(response.choices[0].message.content)
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
AVAILABLE_MODELS = [
|
|
283
|
+
"gemini-2.0-flash-lite",
|
|
284
|
+
"gemini-2.0-flash",
|
|
285
|
+
"gemini-2.5-pro-preview-06-05",
|
|
286
|
+
"gemini-2.5-pro-preview-05-06",
|
|
287
|
+
"gemini-2.5-flash-preview-04-17",
|
|
288
|
+
"gemini-2.5-flash-preview-05-20",
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
def __init__(
|
|
292
|
+
self,
|
|
293
|
+
api_key: Optional[str] = None, # Not used but included for compatibility
|
|
294
|
+
browser: str = "chrome",
|
|
295
|
+
**kwargs: Any
|
|
296
|
+
):
|
|
297
|
+
"""
|
|
298
|
+
Initialize the GeminiProxy client.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
api_key: Not used but included for compatibility with OpenAI interface
|
|
302
|
+
browser: Browser type for fingerprinting
|
|
303
|
+
**kwargs: Additional parameters
|
|
304
|
+
"""
|
|
305
|
+
super().__init__(api_key=api_key, **kwargs)
|
|
306
|
+
|
|
307
|
+
self.timeout = 30
|
|
308
|
+
self.base_url = "https://us-central1-infinite-chain-295909.cloudfunctions.net/gemini-proxy-staging-v1"
|
|
309
|
+
|
|
310
|
+
# Initialize LitAgent for fingerprinting
|
|
311
|
+
self.agent = LitAgent()
|
|
312
|
+
self.fingerprint = self.agent.generate_fingerprint(browser)
|
|
313
|
+
|
|
314
|
+
# Initialize session
|
|
315
|
+
self.session = requests.Session()
|
|
316
|
+
self.headers = self.fingerprint.copy()
|
|
317
|
+
self.session.headers.update(self.headers)
|
|
318
|
+
self.session.proxies = {}
|
|
319
|
+
|
|
320
|
+
# Initialize chat interface
|
|
321
|
+
self.chat = Chat(self)
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def models(self):
|
|
325
|
+
class _ModelList:
|
|
326
|
+
def list(inner_self):
|
|
327
|
+
return type(self).AVAILABLE_MODELS
|
|
328
|
+
return _ModelList()
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import List, Dict, Optional, Union, Generator, Any
|
|
7
|
+
|
|
8
|
+
from webscout.Provider.OPENAI.base import OpenAICompatibleProvider, BaseChat, BaseCompletions
|
|
9
|
+
from webscout.Provider.OPENAI.utils import (
|
|
10
|
+
ChatCompletionChunk, ChatCompletion, Choice, ChoiceDelta,
|
|
11
|
+
ChatCompletionMessage, CompletionUsage, count_tokens
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
class Completions(BaseCompletions):
|
|
15
|
+
def __init__(self, client: 'MiniMax'):
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
def create(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
model: str,
|
|
22
|
+
messages: List[Dict[str, str]],
|
|
23
|
+
max_tokens: Optional[int] = None,
|
|
24
|
+
stream: bool = False,
|
|
25
|
+
temperature: Optional[float] = None,
|
|
26
|
+
top_p: Optional[float] = None,
|
|
27
|
+
timeout: Optional[int] = None,
|
|
28
|
+
proxies: Optional[Dict[str, str]] = None,
|
|
29
|
+
stop: Optional[Union[str, List[str]]] = None,
|
|
30
|
+
**kwargs: Any
|
|
31
|
+
) -> Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]]:
|
|
32
|
+
"""
|
|
33
|
+
Creates a model response for the given chat conversation.
|
|
34
|
+
Mimics openai.chat.completions.create
|
|
35
|
+
"""
|
|
36
|
+
api_key = self._client.api_key
|
|
37
|
+
if not api_key:
|
|
38
|
+
raise Exception("MINIMAX_API_KEY not set in environment.")
|
|
39
|
+
model_name = self._client.convert_model_name(model)
|
|
40
|
+
payload = {
|
|
41
|
+
"model": model_name,
|
|
42
|
+
"messages": messages,
|
|
43
|
+
"stream": stream,
|
|
44
|
+
}
|
|
45
|
+
if max_tokens is not None:
|
|
46
|
+
payload["max_tokens"] = max_tokens
|
|
47
|
+
if temperature is not None:
|
|
48
|
+
payload["temperature"] = temperature
|
|
49
|
+
if top_p is not None:
|
|
50
|
+
payload["top_p"] = top_p
|
|
51
|
+
if stop is not None:
|
|
52
|
+
payload["stop"] = stop
|
|
53
|
+
payload.update(kwargs)
|
|
54
|
+
request_id = f"chatcmpl-{uuid.uuid4()}"
|
|
55
|
+
created_time = int(time.time())
|
|
56
|
+
if stream:
|
|
57
|
+
return self._create_stream(request_id, created_time, model_name, payload, timeout, proxies)
|
|
58
|
+
else:
|
|
59
|
+
return self._create_non_stream(request_id, created_time, model_name, payload, timeout, proxies)
|
|
60
|
+
|
|
61
|
+
def _create_stream(
|
|
62
|
+
self, request_id: str, created_time: int, model: str, payload: Dict[str, Any], timeout: Optional[int] = None, proxies: Optional[Dict[str, str]] = None
|
|
63
|
+
) -> Generator[ChatCompletionChunk, None, None]:
|
|
64
|
+
try:
|
|
65
|
+
headers = {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'Authorization': f'Bearer {self._client.api_key}',
|
|
68
|
+
}
|
|
69
|
+
response = self._client.session.post(
|
|
70
|
+
self._client.api_endpoint,
|
|
71
|
+
headers=headers,
|
|
72
|
+
data=json.dumps(payload),
|
|
73
|
+
stream=True,
|
|
74
|
+
timeout=timeout or self._client.timeout,
|
|
75
|
+
proxies=proxies
|
|
76
|
+
)
|
|
77
|
+
response.raise_for_status()
|
|
78
|
+
prompt_tokens = count_tokens([msg.get("content", "") for msg in payload.get("messages", [])])
|
|
79
|
+
completion_tokens = 0
|
|
80
|
+
total_tokens = prompt_tokens
|
|
81
|
+
streaming_response = ""
|
|
82
|
+
last_content = ""
|
|
83
|
+
last_reasoning = ""
|
|
84
|
+
in_think = False
|
|
85
|
+
for line in response.iter_lines():
|
|
86
|
+
if line:
|
|
87
|
+
line = line.decode('utf-8')
|
|
88
|
+
if line.startswith('data: '):
|
|
89
|
+
line = line[6:]
|
|
90
|
+
if line.strip() == '[DONE]':
|
|
91
|
+
break
|
|
92
|
+
try:
|
|
93
|
+
chunk_data = json.loads(line)
|
|
94
|
+
if 'choices' in chunk_data and chunk_data['choices']:
|
|
95
|
+
choice_data = chunk_data['choices'][0]
|
|
96
|
+
delta = choice_data.get('delta', {})
|
|
97
|
+
content = delta.get('content')
|
|
98
|
+
reasoning_content = delta.get('reasoning_content')
|
|
99
|
+
finish_reason = choice_data.get('finish_reason')
|
|
100
|
+
# Only yield <think> and reasoning_content if reasoning_content is not empty
|
|
101
|
+
if reasoning_content and reasoning_content.strip() and reasoning_content != last_reasoning:
|
|
102
|
+
if not in_think:
|
|
103
|
+
yield ChatCompletionChunk(
|
|
104
|
+
id=request_id,
|
|
105
|
+
choices=[Choice(index=0, delta=ChoiceDelta(content='<think>\n\n', role=None, tool_calls=None), finish_reason=None, logprobs=None)],
|
|
106
|
+
created=created_time,
|
|
107
|
+
model=model
|
|
108
|
+
)
|
|
109
|
+
in_think = True
|
|
110
|
+
yield ChatCompletionChunk(
|
|
111
|
+
id=request_id,
|
|
112
|
+
choices=[Choice(index=0, delta=ChoiceDelta(content=reasoning_content, role=None, tool_calls=None), finish_reason=None, logprobs=None)],
|
|
113
|
+
created=created_time,
|
|
114
|
+
model=model
|
|
115
|
+
)
|
|
116
|
+
last_reasoning = reasoning_content
|
|
117
|
+
# Only yield </think> if we were in <think> and now have new content
|
|
118
|
+
if in_think and content and content.strip() and content != last_content:
|
|
119
|
+
yield ChatCompletionChunk(
|
|
120
|
+
id=request_id,
|
|
121
|
+
choices=[Choice(index=0, delta=ChoiceDelta(content='</think>\n\n', role=None, tool_calls=None), finish_reason=None, logprobs=None)],
|
|
122
|
+
created=created_time,
|
|
123
|
+
model=model
|
|
124
|
+
)
|
|
125
|
+
in_think = False
|
|
126
|
+
# Only yield content if it is not empty
|
|
127
|
+
if content and content.strip() and content != last_content:
|
|
128
|
+
completion_tokens += count_tokens(content)
|
|
129
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
130
|
+
choice_delta = ChoiceDelta(
|
|
131
|
+
content=content,
|
|
132
|
+
role=delta.get('role', 'assistant'),
|
|
133
|
+
tool_calls=delta.get('tool_calls')
|
|
134
|
+
)
|
|
135
|
+
choice = Choice(
|
|
136
|
+
index=0,
|
|
137
|
+
delta=choice_delta,
|
|
138
|
+
finish_reason=finish_reason,
|
|
139
|
+
logprobs=None
|
|
140
|
+
)
|
|
141
|
+
chunk = ChatCompletionChunk(
|
|
142
|
+
id=request_id,
|
|
143
|
+
choices=[choice],
|
|
144
|
+
created=created_time,
|
|
145
|
+
model=model
|
|
146
|
+
)
|
|
147
|
+
chunk.usage = {
|
|
148
|
+
"prompt_tokens": prompt_tokens,
|
|
149
|
+
"completion_tokens": completion_tokens,
|
|
150
|
+
"total_tokens": total_tokens,
|
|
151
|
+
"estimated_cost": None
|
|
152
|
+
}
|
|
153
|
+
yield chunk
|
|
154
|
+
streaming_response += content
|
|
155
|
+
last_content = content
|
|
156
|
+
except Exception:
|
|
157
|
+
continue
|
|
158
|
+
# Final chunk with finish_reason="stop"
|
|
159
|
+
delta = ChoiceDelta(content=None, role=None, tool_calls=None)
|
|
160
|
+
choice = Choice(index=0, delta=delta, finish_reason="stop", logprobs=None)
|
|
161
|
+
chunk = ChatCompletionChunk(
|
|
162
|
+
id=request_id,
|
|
163
|
+
choices=[choice],
|
|
164
|
+
created=created_time,
|
|
165
|
+
model=model
|
|
166
|
+
)
|
|
167
|
+
chunk.usage = {
|
|
168
|
+
"prompt_tokens": prompt_tokens,
|
|
169
|
+
"completion_tokens": completion_tokens,
|
|
170
|
+
"total_tokens": total_tokens,
|
|
171
|
+
"estimated_cost": None
|
|
172
|
+
}
|
|
173
|
+
yield chunk
|
|
174
|
+
except Exception as e:
|
|
175
|
+
raise IOError(f"MiniMax stream request failed: {e}") from e
|
|
176
|
+
|
|
177
|
+
def _create_non_stream(
|
|
178
|
+
self, request_id: str, created_time: int, model: str, payload: Dict[str, Any], timeout: Optional[int] = None, proxies: Optional[Dict[str, str]] = None
|
|
179
|
+
) -> ChatCompletion:
|
|
180
|
+
try:
|
|
181
|
+
headers = {
|
|
182
|
+
'Content-Type': 'application/json',
|
|
183
|
+
'Authorization': f'Bearer {self._client.api_key}',
|
|
184
|
+
}
|
|
185
|
+
payload_copy = payload.copy()
|
|
186
|
+
payload_copy["stream"] = False
|
|
187
|
+
response = self._client.session.post(
|
|
188
|
+
self._client.api_endpoint,
|
|
189
|
+
headers=headers,
|
|
190
|
+
data=json.dumps(payload_copy),
|
|
191
|
+
timeout=timeout or self._client.timeout,
|
|
192
|
+
proxies=proxies
|
|
193
|
+
)
|
|
194
|
+
response.raise_for_status()
|
|
195
|
+
data = response.json()
|
|
196
|
+
full_text = ""
|
|
197
|
+
finish_reason = "stop"
|
|
198
|
+
if 'choices' in data and data['choices']:
|
|
199
|
+
choice_data = data['choices'][0]
|
|
200
|
+
# MiniMax returns content in 'message' or directly in 'delta' for streaming
|
|
201
|
+
reasoning_content = ""
|
|
202
|
+
if 'message' in choice_data and choice_data['message']:
|
|
203
|
+
full_text = choice_data['message'].get('content', '')
|
|
204
|
+
reasoning_content = choice_data['message'].get('reasoning_content', '')
|
|
205
|
+
elif 'delta' in choice_data and choice_data['delta']:
|
|
206
|
+
full_text = choice_data['delta'].get('content', '')
|
|
207
|
+
reasoning_content = choice_data['delta'].get('reasoning_content', '')
|
|
208
|
+
finish_reason = choice_data.get('finish_reason', 'stop')
|
|
209
|
+
# If both are present, concatenate with <think> ... </think>
|
|
210
|
+
if reasoning_content and reasoning_content.strip():
|
|
211
|
+
if full_text and full_text.strip():
|
|
212
|
+
full_text = f"<think>\n\n{reasoning_content}</think>\n\n{full_text}"
|
|
213
|
+
else:
|
|
214
|
+
full_text = f"<think>\n\n{reasoning_content}</think>\n\n"
|
|
215
|
+
message = ChatCompletionMessage(
|
|
216
|
+
role="assistant",
|
|
217
|
+
content=full_text
|
|
218
|
+
)
|
|
219
|
+
choice = Choice(
|
|
220
|
+
index=0,
|
|
221
|
+
message=message,
|
|
222
|
+
finish_reason=finish_reason
|
|
223
|
+
)
|
|
224
|
+
prompt_tokens = count_tokens([msg.get("content", "") for msg in payload.get("messages", [])])
|
|
225
|
+
completion_tokens = count_tokens(full_text)
|
|
226
|
+
usage = CompletionUsage(
|
|
227
|
+
prompt_tokens=prompt_tokens,
|
|
228
|
+
completion_tokens=completion_tokens,
|
|
229
|
+
total_tokens=prompt_tokens + completion_tokens
|
|
230
|
+
)
|
|
231
|
+
completion = ChatCompletion(
|
|
232
|
+
id=request_id,
|
|
233
|
+
choices=[choice],
|
|
234
|
+
created=created_time,
|
|
235
|
+
model=model,
|
|
236
|
+
usage=usage,
|
|
237
|
+
)
|
|
238
|
+
return completion
|
|
239
|
+
except Exception as e:
|
|
240
|
+
raise IOError(f"MiniMax non-stream request failed: {e}") from e
|
|
241
|
+
|
|
242
|
+
class Chat(BaseChat):
|
|
243
|
+
def __init__(self, client: 'MiniMax'):
|
|
244
|
+
self.completions = Completions(client)
|
|
245
|
+
|
|
246
|
+
class MiniMax(OpenAICompatibleProvider):
|
|
247
|
+
"""
|
|
248
|
+
OpenAI-compatible client for MiniMax API.
|
|
249
|
+
"""
|
|
250
|
+
AVAILABLE_MODELS = [
|
|
251
|
+
"MiniMax-Reasoning-01"
|
|
252
|
+
]
|
|
253
|
+
def __init__(self, timeout: int = 30):
|
|
254
|
+
self.timeout = timeout
|
|
255
|
+
self.api_endpoint = "https://api.minimaxi.chat/v1/text/chatcompletion_v2"
|
|
256
|
+
self.session = requests.Session()
|
|
257
|
+
self.api_key = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiJtbyBuaSIsIlVzZXJOYW1lIjoibW8gbmkiLCJBY2NvdW50IjoiIiwiU3ViamVjdElEIjoiMTg3NjIwMDY0ODA2NDYzNTI0MiIsIlBob25lIjoiIiwiR3JvdXBJRCI6IjE4NzYyMDA2NDgwNjA0NDA5MzgiLCJQYWdlTmFtZSI6IiIsIk1haWwiOiJuaW1vQHN1YnN1cC52aXAiLCJDcmVhdGVUaW1lIjoiMjAyNS0wMS0wNyAxMToyNzowNyIsIlRva2VuVHlwZSI6MSwiaXNzIjoibWluaW1heCJ9.Ge1ZnpFPUfXVdMini0P_qXbP_9VYwzXiffG9DsNQck4GtYEOs33LDeAiwrVsrrLZfvJ2icQZ4sRZS54wmPuWua_Dav6pYJty8ZtahmUX1IuhlUX5YErhhCRAIy3J1xB8FkLHLyylChuBHpkNz6O6BQLmPqmoa-cOYK9Qrc6IDeu8SX1iMzO9-MSkcWNvkvpCF2Pf9tekBVWNKMDK6IZoMEPbtkaPXdDyP6l0M0e2AlL_E0oM9exg3V-ohAi8OTPFyqM6dcd4TwF-b9DULxfIsRFw401mvIxcTDWa42u2LULewdATVRD2BthU65tuRqEiWeFWMvFlPj2soMze_QIiUA"
|
|
258
|
+
self.chat = Chat(self)
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def models(self):
|
|
262
|
+
class _ModelList:
|
|
263
|
+
def list(inner_self):
|
|
264
|
+
return MiniMax.AVAILABLE_MODELS
|
|
265
|
+
return _ModelList()
|
|
266
|
+
|
|
267
|
+
def convert_model_name(self, model: str) -> str:
|
|
268
|
+
if model in self.AVAILABLE_MODELS:
|
|
269
|
+
return model
|
|
270
|
+
return self.AVAILABLE_MODELS[0]
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
from rich import print
|
|
274
|
+
client = MiniMax()
|
|
275
|
+
messages = [
|
|
276
|
+
{"role": "user", "content": "What is the capital of France?"}
|
|
277
|
+
]
|
|
278
|
+
# Non-streaming example
|
|
279
|
+
response = client.chat.completions.create(
|
|
280
|
+
model="MiniMax-Reasoning-01",
|
|
281
|
+
messages=messages,
|
|
282
|
+
max_tokens=5000,
|
|
283
|
+
stream=False
|
|
284
|
+
)
|
|
285
|
+
print("Non-streaming response:")
|
|
286
|
+
print(response)
|
|
287
|
+
# Streaming example
|
|
288
|
+
print("\nStreaming response:")
|
|
289
|
+
stream = client.chat.completions.create(
|
|
290
|
+
model="MiniMax-Reasoning-01",
|
|
291
|
+
messages=messages,
|
|
292
|
+
max_tokens=5000,
|
|
293
|
+
stream=True
|
|
294
|
+
)
|
|
295
|
+
for chunk in stream:
|
|
296
|
+
if chunk.choices[0].delta and chunk.choices[0].delta.content:
|
|
297
|
+
print(chunk.choices[0].delta.content, end="")
|
|
298
|
+
print()
|