webscout 8.2.3__py3-none-any.whl → 8.2.4__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.

Files changed (87) hide show
  1. inferno/lol.py +589 -0
  2. webscout/AIutel.py +226 -14
  3. webscout/Bard.py +579 -206
  4. webscout/DWEBS.py +78 -35
  5. webscout/Extra/tempmail/base.py +1 -1
  6. webscout/Provider/AISEARCH/hika_search.py +4 -0
  7. webscout/Provider/AllenAI.py +163 -126
  8. webscout/Provider/ChatGPTClone.py +96 -84
  9. webscout/Provider/Deepinfra.py +95 -67
  10. webscout/Provider/ElectronHub.py +55 -0
  11. webscout/Provider/GPTWeb.py +96 -46
  12. webscout/Provider/Groq.py +194 -91
  13. webscout/Provider/HeckAI.py +89 -47
  14. webscout/Provider/HuggingFaceChat.py +113 -106
  15. webscout/Provider/Hunyuan.py +94 -83
  16. webscout/Provider/Jadve.py +107 -75
  17. webscout/Provider/LambdaChat.py +106 -64
  18. webscout/Provider/Llama3.py +94 -39
  19. webscout/Provider/MCPCore.py +318 -0
  20. webscout/Provider/Marcus.py +85 -36
  21. webscout/Provider/Netwrck.py +76 -43
  22. webscout/Provider/OPENAI/__init__.py +4 -1
  23. webscout/Provider/OPENAI/ai4chat.py +286 -0
  24. webscout/Provider/OPENAI/chatgptclone.py +35 -14
  25. webscout/Provider/OPENAI/deepinfra.py +37 -0
  26. webscout/Provider/OPENAI/groq.py +354 -0
  27. webscout/Provider/OPENAI/heckai.py +6 -2
  28. webscout/Provider/OPENAI/mcpcore.py +376 -0
  29. webscout/Provider/OPENAI/multichat.py +368 -0
  30. webscout/Provider/OPENAI/netwrck.py +3 -1
  31. webscout/Provider/OpenGPT.py +48 -38
  32. webscout/Provider/PI.py +168 -92
  33. webscout/Provider/PizzaGPT.py +66 -36
  34. webscout/Provider/TeachAnything.py +85 -51
  35. webscout/Provider/TextPollinationsAI.py +109 -51
  36. webscout/Provider/TwoAI.py +109 -60
  37. webscout/Provider/Venice.py +93 -56
  38. webscout/Provider/VercelAI.py +2 -2
  39. webscout/Provider/WiseCat.py +65 -28
  40. webscout/Provider/Writecream.py +37 -11
  41. webscout/Provider/WritingMate.py +135 -63
  42. webscout/Provider/__init__.py +3 -21
  43. webscout/Provider/ai4chat.py +6 -7
  44. webscout/Provider/copilot.py +0 -3
  45. webscout/Provider/elmo.py +101 -58
  46. webscout/Provider/granite.py +91 -46
  47. webscout/Provider/hermes.py +87 -47
  48. webscout/Provider/koala.py +1 -1
  49. webscout/Provider/learnfastai.py +104 -50
  50. webscout/Provider/llama3mitril.py +86 -51
  51. webscout/Provider/llmchat.py +88 -46
  52. webscout/Provider/llmchatco.py +74 -49
  53. webscout/Provider/meta.py +41 -37
  54. webscout/Provider/multichat.py +54 -25
  55. webscout/Provider/scnet.py +93 -43
  56. webscout/Provider/searchchat.py +82 -75
  57. webscout/Provider/sonus.py +103 -51
  58. webscout/Provider/toolbaz.py +132 -77
  59. webscout/Provider/turboseek.py +92 -41
  60. webscout/Provider/tutorai.py +82 -64
  61. webscout/Provider/typefully.py +75 -33
  62. webscout/Provider/typegpt.py +96 -35
  63. webscout/Provider/uncovr.py +112 -62
  64. webscout/Provider/x0gpt.py +69 -26
  65. webscout/Provider/yep.py +79 -66
  66. webscout/conversation.py +35 -21
  67. webscout/exceptions.py +20 -0
  68. webscout/prompt_manager.py +56 -42
  69. webscout/version.py +1 -1
  70. webscout/webscout_search.py +65 -47
  71. webscout/webscout_search_async.py +81 -126
  72. webscout/yep_search.py +93 -43
  73. {webscout-8.2.3.dist-info → webscout-8.2.4.dist-info}/METADATA +22 -10
  74. {webscout-8.2.3.dist-info → webscout-8.2.4.dist-info}/RECORD +78 -81
  75. {webscout-8.2.3.dist-info → webscout-8.2.4.dist-info}/WHEEL +1 -1
  76. webscout/Provider/C4ai.py +0 -432
  77. webscout/Provider/ChatGPTES.py +0 -237
  78. webscout/Provider/DeepSeek.py +0 -196
  79. webscout/Provider/Llama.py +0 -200
  80. webscout/Provider/Phind.py +0 -535
  81. webscout/Provider/WebSim.py +0 -228
  82. webscout/Provider/labyrinth.py +0 -340
  83. webscout/Provider/lepton.py +0 -194
  84. webscout/Provider/llamatutor.py +0 -192
  85. {webscout-8.2.3.dist-info → webscout-8.2.4.dist-info}/entry_points.txt +0 -0
  86. {webscout-8.2.3.dist-info → webscout-8.2.4.dist-info/licenses}/LICENSE.md +0 -0
  87. {webscout-8.2.3.dist-info → webscout-8.2.4.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  import time
2
2
  import uuid
3
- import cloudscraper
3
+ # import cloudscraper
4
+ from curl_cffi.requests import Session, RequestsError
4
5
  import json
5
6
  import re
6
7
  from typing import Any, Dict, Optional, Generator, Union
@@ -12,7 +13,7 @@ from webscout.AIutel import Conversation
12
13
  from webscout.AIutel import AwesomePrompts
13
14
  from webscout.AIbase import Provider
14
15
  from webscout import WEBS, exceptions
15
- from webscout.litagent import LitAgent
16
+ # from webscout.litagent import LitAgent
16
17
 
17
18
  class ChatGPTClone(Provider):
18
19
  """
@@ -22,6 +23,11 @@ class ChatGPTClone(Provider):
22
23
 
23
24
  url = "https://chatgpt-clone-ten-nu.vercel.app"
24
25
  AVAILABLE_MODELS = ["gpt-4", "gpt-3.5-turbo"]
26
+ SUPPORTED_IMPERSONATION = [
27
+ "chrome110", "chrome116", "chrome119", "chrome120",
28
+ "chrome99_android", "edge99", "edge101",
29
+ "safari15_3", "safari15_6_1", "safari17_0", "safari17_2_1"
30
+ ]
25
31
 
26
32
  def __init__(
27
33
  self,
@@ -37,15 +43,18 @@ class ChatGPTClone(Provider):
37
43
  model: str = "gpt-4",
38
44
  temperature: float = 0.6,
39
45
  top_p: float = 0.7,
40
- browser: str = "chrome",
46
+ impersonate: str = "chrome120",
41
47
  system_prompt: str = "You are a helpful assistant."
42
48
  ):
43
- """Initialize the ChatGPT Clone client."""
49
+ """Initialize the ChatGPT Clone client using curl_cffi."""
44
50
  if model not in self.AVAILABLE_MODELS:
45
51
  raise ValueError(f"Invalid model: {model}. Choose from: {self.AVAILABLE_MODELS}")
52
+ if impersonate not in self.SUPPORTED_IMPERSONATION:
53
+ raise ValueError(f"Invalid impersonate browser: {impersonate}. Choose from: {self.SUPPORTED_IMPERSONATION}")
46
54
 
47
55
  self.model = model
48
- self.session = cloudscraper.create_scraper()
56
+ self.impersonate = impersonate
57
+ self.session = Session(impersonate=self.impersonate, proxies=proxies, timeout=timeout)
49
58
  self.is_conversation = is_conversation
50
59
  self.max_tokens_to_sample = max_tokens
51
60
  self.timeout = timeout
@@ -54,28 +63,17 @@ class ChatGPTClone(Provider):
54
63
  self.top_p = top_p
55
64
  self.system_prompt = system_prompt
56
65
 
57
- # Initialize LitAgent for user agent generation
58
- self.agent = LitAgent()
59
- # Use fingerprinting to create a consistent browser identity
60
- self.fingerprint = self.agent.generate_fingerprint(browser)
61
-
62
- # Use the fingerprint for headers
63
66
  self.headers = {
64
- "Accept": self.fingerprint["accept"],
65
- "Accept-Encoding": "gzip, deflate, br, zstd",
66
- "Accept-Language": self.fingerprint["accept_language"],
67
67
  "Content-Type": "application/json",
68
- "DNT": "1",
69
68
  "Origin": self.url,
70
69
  "Referer": f"{self.url}/",
71
- "Sec-CH-UA": self.fingerprint["sec_ch_ua"] or '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
72
- "Sec-CH-UA-Mobile": "?0",
73
- "Sec-CH-UA-Platform": f'"{self.fingerprint["platform"]}"',
74
- "User-Agent": self.fingerprint["user_agent"],
70
+ "DNT": "1",
71
+ "Sec-Fetch-Dest": "empty",
72
+ "Sec-Fetch-Mode": "cors",
73
+ "Sec-Fetch-Site": "same-origin",
74
+ "TE": "trailers"
75
75
  }
76
-
77
- # Create session cookies with unique identifiers
78
- self.cookies = {"__Host-session": uuid.uuid4().hex, '__cf_bm': uuid.uuid4().hex}
76
+ self.session.headers.update(self.headers)
79
77
 
80
78
  self.__available_optimizers = (
81
79
  method
@@ -92,34 +90,20 @@ class ChatGPTClone(Provider):
92
90
  is_conversation, self.max_tokens_to_sample, filepath, update_file
93
91
  )
94
92
  self.conversation.history_offset = history_offset
95
- self.session.proxies = proxies
96
-
97
- # Set consistent headers for the scraper session
98
- for header, value in self.headers.items():
99
- self.session.headers[header] = value
100
93
 
101
- def refresh_identity(self, browser: str = None):
102
- """Refreshes the browser identity fingerprint."""
103
- browser = browser or self.fingerprint.get("browser_type", "chrome")
104
- self.fingerprint = self.agent.generate_fingerprint(browser)
105
-
106
- # Update headers with new fingerprint
107
- self.headers.update({
108
- "Accept": self.fingerprint["accept"],
109
- "Accept-Language": self.fingerprint["accept_language"],
110
- "Sec-CH-UA": self.fingerprint["sec_ch_ua"] or self.headers["Sec-CH-UA"],
111
- "Sec-CH-UA-Platform": f'"{self.fingerprint["platform"]}"',
112
- "User-Agent": self.fingerprint["user_agent"],
113
- })
114
-
115
- # Update session headers
116
- for header, value in self.headers.items():
117
- self.session.headers[header] = value
118
-
119
- # Generate new cookies
120
- self.cookies = {"__Host-session": uuid.uuid4().hex, '__cf_bm': uuid.uuid4().hex}
121
-
122
- return self.fingerprint
94
+ def refresh_identity(self, impersonate: str = None):
95
+ """Re-initializes the curl_cffi session with a new impersonation target."""
96
+ impersonate = impersonate or self.impersonate
97
+ if impersonate not in self.SUPPORTED_IMPERSONATION:
98
+ raise ValueError(f"Invalid impersonate browser: {impersonate}. Choose from: {self.SUPPORTED_IMPERSONATION}")
99
+ self.impersonate = impersonate
100
+ self.session = Session(
101
+ impersonate=self.impersonate,
102
+ proxies=self.session.proxies,
103
+ timeout=self.timeout
104
+ )
105
+ self.session.headers.update(self.headers)
106
+ return self.impersonate
123
107
 
124
108
  def ask(
125
109
  self,
@@ -129,7 +113,7 @@ class ChatGPTClone(Provider):
129
113
  optimizer: str = None,
130
114
  conversationally: bool = False,
131
115
  ) -> Union[Dict[str, Any], Generator]:
132
- """Send a message to the ChatGPT Clone API"""
116
+ """Send a message to the ChatGPT Clone API using curl_cffi"""
133
117
  conversation_prompt = self.conversation.gen_complete_prompt(prompt)
134
118
  if optimizer:
135
119
  if optimizer in self.__available_optimizers:
@@ -149,38 +133,66 @@ class ChatGPTClone(Provider):
149
133
  "model": self.model
150
134
  }
151
135
 
152
- def for_stream():
136
+ api_url = f"{self.url}/api/chat"
137
+
138
+ def _make_request(attempt_refresh=True):
153
139
  try:
154
- with self.session.post(f"{self.url}/api/chat", headers=self.headers, cookies=self.cookies, json=payload, stream=True, timeout=self.timeout) as response:
155
- if not response.ok:
156
- # If we get a non-200 response, try refreshing our identity once
157
- if response.status_code in [403, 429]:
158
- self.refresh_identity()
159
- # Retry with new identity
160
- with self.session.post(f"{self.url}/api/chat", headers=self.headers, cookies=self.cookies, json=payload, stream=True, timeout=self.timeout) as retry_response:
161
- if not retry_response.ok:
162
- raise exceptions.FailedToGenerateResponseError(
163
- f"Failed to generate response after identity refresh - ({retry_response.status_code}, {retry_response.reason}) - {retry_response.text}"
164
- )
165
- response = retry_response
166
- else:
167
- raise exceptions.FailedToGenerateResponseError(
168
- f"Failed to generate response - ({response.status_code}, {response.reason}) - {response.text}"
169
- )
140
+ response = self.session.post(api_url, json=payload, stream=True)
141
+ response.raise_for_status()
142
+ return response
143
+ except RequestsError as e:
144
+ if attempt_refresh and e.response and e.response.status_code in [403, 429]:
145
+ self.refresh_identity()
146
+ return _make_request(attempt_refresh=False)
147
+ else:
148
+ err_msg = f"Request failed: {e}"
149
+ if e.response is not None:
150
+ err_msg = f"Failed to generate response - ({e.response.status_code}, {e.response.reason}) - {e.response.text}"
151
+ raise exceptions.FailedToGenerateResponseError(err_msg) from e
152
+ except Exception as e:
153
+ raise exceptions.FailedToGenerateResponseError(f"An unexpected error occurred: {e}") from e
170
154
 
171
- streaming_text = ""
172
- for line in response.iter_lines(decode_unicode=True):
173
- if line:
174
- match = re.search(r'0:"(.*?)"', line)
175
- if match:
176
- content = match.group(1)
177
- streaming_text += content
178
- yield content if raw else dict(text=content)
179
-
180
- self.last_response.update(dict(text=streaming_text))
181
- self.conversation.update_chat_history(prompt, streaming_text)
155
+ def for_stream():
156
+ response = _make_request()
157
+ streaming_text = ""
158
+ buffer = ""
159
+ try:
160
+ for line in response.iter_content():
161
+ if line:
162
+ # decode bytes to str before concatenation
163
+ if isinstance(line, bytes):
164
+ line = line.decode("utf-8", errors="replace")
165
+ buffer += line
166
+ match = re.search(r'0:"((?:[^\\"]|\\.)*)"', buffer)
167
+ if match:
168
+ content = match.group(1)
169
+ try:
170
+ decoded_content = json.loads(f'"{content}"')
171
+ except json.JSONDecodeError:
172
+ decoded_content = content.encode().decode('unicode_escape')
173
+ streaming_text += decoded_content
174
+ yield decoded_content if raw else dict(text=decoded_content)
175
+ buffer = ""
176
+ elif len(buffer) > 1024:
177
+ buffer = ""
178
+ if buffer:
179
+ match = re.search(r'0:"((?:[^\\"]|\\.)*)"', buffer)
180
+ if match:
181
+ content = match.group(1)
182
+ try:
183
+ decoded_content = json.loads(f'"{content}"')
184
+ except json.JSONDecodeError:
185
+ decoded_content = content.encode().decode('unicode_escape')
186
+ streaming_text += decoded_content
187
+ yield decoded_content if raw else dict(text=decoded_content)
188
+ self.last_response.update(dict(text=streaming_text))
189
+ self.conversation.update_chat_history(prompt, streaming_text)
190
+ except RequestsError as e:
191
+ raise exceptions.FailedToGenerateResponseError(f"Stream interrupted by request error: {e}") from e
182
192
  except Exception as e:
183
- raise exceptions.FailedToGenerateResponseError(f"Request failed: {e}")
193
+ raise exceptions.FailedToGenerateResponseError(f"Error processing stream: {e}") from e
194
+ finally:
195
+ response.close()
184
196
 
185
197
  def for_non_stream():
186
198
  for _ in for_stream():
@@ -202,25 +214,25 @@ class ChatGPTClone(Provider):
202
214
  prompt, True, optimizer=optimizer, conversationally=conversationally
203
215
  ):
204
216
  yield self.get_message(response)
205
-
206
217
  def for_non_stream():
207
218
  return self.get_message(
208
219
  self.ask(
209
220
  prompt, False, optimizer=optimizer, conversationally=conversationally
210
221
  )
211
222
  )
212
-
213
223
  return for_stream() if stream else for_non_stream()
214
224
 
215
225
  def get_message(self, response: dict) -> str:
216
226
  """Extract message text from response"""
217
227
  assert isinstance(response, dict)
218
- formatted_text = response["text"].replace('\\n', '\n').replace('\\n\\n', '\n\n')
228
+ if not isinstance(response, dict) or "text" not in response:
229
+ return str(response)
230
+ formatted_text = response["text"]
219
231
  return formatted_text
220
232
 
221
233
  if __name__ == "__main__":
222
234
  from rich import print
223
- ai = ChatGPTClone(timeout=5000)
235
+ ai = ChatGPTClone(timeout=120, impersonate="chrome120")
224
236
  response = ai.chat("write a poem about AI", stream=True)
225
237
  for chunk in response:
226
- print(chunk, end="", flush=True)
238
+ print(chunk, end="", flush=True)
@@ -1,4 +1,5 @@
1
- import requests
1
+ from curl_cffi.requests import Session
2
+ from curl_cffi import CurlError
2
3
  import json
3
4
  import os
4
5
  from typing import Any, Dict, Optional, Generator, Union
@@ -65,6 +66,9 @@ class DeepInfra(Provider):
65
66
  # "Qwen/Qwen2.5-7B-Instruct", # >>>> NOT WORKING
66
67
  "Qwen/Qwen2.5-72B-Instruct",
67
68
  "Qwen/Qwen2.5-Coder-32B-Instruct",
69
+ "Qwen/Qwen3-14B",
70
+ "Qwen/Qwen3-30B-A3B",
71
+ "Qwen/Qwen3-32B",
68
72
  # "Sao10K/L3.1-70B-Euryale-v2.2", # >>>> NOT WORKING
69
73
  # "Sao10K/L3.3-70B-Euryale-v2.3", # >>>> NOT WORKING
70
74
  ]
@@ -72,7 +76,7 @@ class DeepInfra(Provider):
72
76
  def __init__(
73
77
  self,
74
78
  is_conversation: bool = True,
75
- max_tokens: int = 2049, # Set a reasonable default
79
+ max_tokens: int = 2049,
76
80
  timeout: int = 30,
77
81
  intro: str = None,
78
82
  filepath: str = None,
@@ -82,7 +86,7 @@ class DeepInfra(Provider):
82
86
  act: str = None,
83
87
  model: str = "meta-llama/Llama-3.3-70B-Instruct-Turbo",
84
88
  system_prompt: str = "You are a helpful assistant.",
85
- browser: str = "chrome"
89
+ browser: str = "chrome" # Note: browser fingerprinting might be less effective with impersonate
86
90
  ):
87
91
  """Initializes the DeepInfra API client."""
88
92
  if model not in self.AVAILABLE_MODELS:
@@ -90,35 +94,31 @@ class DeepInfra(Provider):
90
94
 
91
95
  self.url = "https://api.deepinfra.com/v1/openai/chat/completions"
92
96
 
93
- # Initialize LitAgent for user agent generation
97
+ # Initialize LitAgent (keep if needed for other headers or logic)
94
98
  self.agent = LitAgent()
95
- # Use fingerprinting to create a consistent browser identity
99
+ # Fingerprint generation might be less relevant with impersonate
96
100
  self.fingerprint = self.agent.generate_fingerprint(browser)
97
101
 
98
- # Use the fingerprint for headers
102
+ # Use the fingerprint for headers (keep relevant ones)
99
103
  self.headers = {
100
- "Accept": self.fingerprint["accept"],
101
- "Accept-Encoding": "gzip, deflate, br, zstd",
102
- "Accept-Language": self.fingerprint["accept_language"],
104
+ "Accept": self.fingerprint["accept"], # Keep Accept
105
+ "Accept-Language": self.fingerprint["accept_language"], # Keep Accept-Language
103
106
  "Content-Type": "application/json",
104
- "Cache-Control": "no-cache",
105
- "Connection": "keep-alive",
106
- "Origin": "https://deepinfra.com",
107
- "Pragma": "no-cache",
108
- "Referer": "https://deepinfra.com/",
109
- "Sec-Fetch-Dest": "empty",
107
+ "Cache-Control": "no-cache", # Keep Cache-Control
108
+ "Origin": "https://deepinfra.com", # Keep Origin
109
+ "Pragma": "no-cache", # Keep Pragma
110
+ "Referer": "https://deepinfra.com/", # Keep Referer
111
+ "Sec-Fetch-Dest": "empty", # Keep Sec-Fetch-*
110
112
  "Sec-Fetch-Mode": "cors",
111
113
  "Sec-Fetch-Site": "same-site",
112
- "X-Deepinfra-Source": "web-embed",
113
- "Sec-CH-UA": self.fingerprint["sec_ch_ua"] or '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
114
- "Sec-CH-UA-Mobile": "?0",
115
- "Sec-CH-UA-Platform": f'"{self.fingerprint["platform"]}"',
116
- "User-Agent": self.fingerprint["user_agent"],
114
+ "X-Deepinfra-Source": "web-embed", # Keep custom headers
117
115
  }
118
116
 
119
- self.session = requests.Session()
117
+ # Initialize curl_cffi Session
118
+ self.session = Session()
119
+ # Update curl_cffi session headers and proxies
120
120
  self.session.headers.update(self.headers)
121
- self.session.proxies.update(proxies)
121
+ self.session.proxies = proxies # Assign proxies directly
122
122
  self.system_prompt = system_prompt
123
123
  self.is_conversation = is_conversation
124
124
  self.max_tokens_to_sample = max_tokens
@@ -154,25 +154,21 @@ class DeepInfra(Provider):
154
154
  browser = browser or self.fingerprint.get("browser_type", "chrome")
155
155
  self.fingerprint = self.agent.generate_fingerprint(browser)
156
156
 
157
- # Update headers with new fingerprint
157
+ # Update headers with new fingerprint (only relevant ones)
158
158
  self.headers.update({
159
159
  "Accept": self.fingerprint["accept"],
160
160
  "Accept-Language": self.fingerprint["accept_language"],
161
- "Sec-CH-UA": self.fingerprint["sec_ch_ua"] or self.headers["Sec-CH-UA"],
162
- "Sec-CH-UA-Platform": f'"{self.fingerprint["platform"]}"',
163
- "User-Agent": self.fingerprint["user_agent"],
164
161
  })
165
162
 
166
163
  # Update session headers
167
- for header, value in self.headers.items():
168
- self.session.headers[header] = value
164
+ self.session.headers.update(self.headers) # Update only relevant headers
169
165
 
170
166
  return self.fingerprint
171
167
 
172
168
  def ask(
173
169
  self,
174
170
  prompt: str,
175
- stream: bool = False,
171
+ stream: bool = False, # API supports streaming
176
172
  raw: bool = False,
177
173
  optimizer: str = None,
178
174
  conversationally: bool = False,
@@ -193,61 +189,82 @@ class DeepInfra(Provider):
193
189
  {"role": "system", "content": self.system_prompt},
194
190
  {"role": "user", "content": conversation_prompt},
195
191
  ],
196
- "stream": stream
192
+ "stream": stream # Pass stream argument to payload
197
193
  }
198
194
 
199
195
  def for_stream():
196
+ streaming_text = "" # Initialize outside try block
200
197
  try:
201
- with requests.post(self.url, headers=self.headers, data=json.dumps(payload), stream=True, timeout=self.timeout) as response:
202
- if response.status_code != 200:
203
- raise exceptions.FailedToGenerateResponseError(
204
- f"Request failed with status code {response.status_code}"
205
- )
198
+ # Use curl_cffi session post with impersonate
199
+ response = self.session.post(
200
+ self.url,
201
+ # headers are set on the session
202
+ data=json.dumps(payload),
203
+ stream=True,
204
+ timeout=self.timeout,
205
+ impersonate="chrome110" # Use a common impersonation profile
206
+ )
207
+ response.raise_for_status() # Check for HTTP errors
206
208
 
207
- streaming_text = ""
208
- for line in response.iter_lines(decode_unicode=True):
209
- if line:
210
- line = line.strip()
209
+ # Iterate over bytes and decode manually
210
+ for line_bytes in response.iter_lines():
211
+ if line_bytes:
212
+ try:
213
+ line = line_bytes.decode('utf-8').strip()
211
214
  if line.startswith("data: "):
212
215
  json_str = line[6:]
213
216
  if json_str == "[DONE]":
214
217
  break
215
- try:
216
- json_data = json.loads(json_str)
217
- if 'choices' in json_data:
218
- choice = json_data['choices'][0]
219
- if 'delta' in choice and 'content' in choice['delta']:
220
- content = choice['delta']['content']
218
+ json_data = json.loads(json_str)
219
+ if 'choices' in json_data:
220
+ choice = json_data['choices'][0]
221
+ if 'delta' in choice and 'content' in choice['delta']:
222
+ content = choice['delta']['content']
223
+ if content: # Ensure content is not None or empty
221
224
  streaming_text += content
222
225
  resp = dict(text=content)
223
- yield resp if raw else resp
224
- except json.JSONDecodeError:
225
- continue
226
+ # Yield dict or raw string chunk
227
+ yield resp if not raw else content
228
+ except (json.JSONDecodeError, UnicodeDecodeError):
229
+ continue # Ignore lines that are not valid JSON or cannot be decoded
226
230
 
227
- self.last_response = {"text": streaming_text}
228
- self.conversation.update_chat_history(prompt, streaming_text)
231
+ # Update history after stream finishes
232
+ self.last_response = {"text": streaming_text}
233
+ self.conversation.update_chat_history(prompt, streaming_text)
229
234
 
230
- except requests.RequestException as e:
231
- raise exceptions.FailedToGenerateResponseError(f"Request failed: {str(e)}")
235
+ except CurlError as e: # Catch CurlError
236
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {str(e)}") from e
237
+ except Exception as e: # Catch other potential exceptions (like HTTPError)
238
+ err_text = getattr(e, 'response', None) and getattr(e.response, 'text', '')
239
+ raise exceptions.FailedToGenerateResponseError(f"Request failed ({type(e).__name__}): {str(e)} - {err_text}") from e
240
+
232
241
 
233
242
  def for_non_stream():
234
243
  try:
235
- response = requests.post(self.url, headers=self.headers, data=json.dumps(payload), timeout=self.timeout)
236
- if response.status_code != 200:
237
- raise exceptions.FailedToGenerateResponseError(
238
- f"Request failed with status code {response.status_code}"
239
- )
244
+ # Use curl_cffi session post with impersonate for non-streaming
245
+ response = self.session.post(
246
+ self.url,
247
+ # headers are set on the session
248
+ data=json.dumps(payload),
249
+ timeout=self.timeout,
250
+ impersonate="chrome110" # Use a common impersonation profile
251
+ )
252
+ response.raise_for_status() # Check for HTTP errors
240
253
 
241
254
  response_data = response.json()
242
255
  if 'choices' in response_data and len(response_data['choices']) > 0:
243
256
  content = response_data['choices'][0].get('message', {}).get('content', '')
244
257
  self.last_response = {"text": content}
245
258
  self.conversation.update_chat_history(prompt, content)
246
- return {"text": content}
259
+ return self.last_response if not raw else content # Return dict or raw string
247
260
  else:
248
261
  raise exceptions.FailedToGenerateResponseError("No response content found")
249
- except Exception as e:
250
- raise exceptions.FailedToGenerateResponseError(f"Request failed: {e}")
262
+ except CurlError as e: # Catch CurlError
263
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
264
+ except Exception as e: # Catch other potential exceptions (like HTTPError, JSONDecodeError)
265
+ err_text = getattr(e, 'response', None) and getattr(e.response, 'text', '')
266
+ raise exceptions.FailedToGenerateResponseError(f"Request failed ({type(e).__name__}): {e} - {err_text}") from e
267
+
251
268
 
252
269
  return for_stream() if stream else for_non_stream()
253
270
 
@@ -258,20 +275,31 @@ class DeepInfra(Provider):
258
275
  optimizer: str = None,
259
276
  conversationally: bool = False,
260
277
  ) -> Union[str, Generator[str, None, None]]:
261
- def for_stream():
262
- for response in self.ask(prompt, True, optimizer=optimizer, conversationally=conversationally):
263
- yield self.get_message(response)
264
- def for_non_stream():
265
- return self.get_message(
266
- self.ask(prompt, False, optimizer=optimizer, conversationally=conversationally)
278
+ def for_stream_chat():
279
+ # ask() yields dicts or strings when streaming
280
+ gen = self.ask(
281
+ prompt, stream=True, raw=False, # Ensure ask yields dicts
282
+ optimizer=optimizer, conversationally=conversationally
267
283
  )
268
- return for_stream() if stream else for_non_stream()
284
+ for response_dict in gen:
285
+ yield self.get_message(response_dict) # get_message expects dict
286
+
287
+ def for_non_stream_chat():
288
+ # ask() returns dict or str when not streaming
289
+ response_data = self.ask(
290
+ prompt, stream=False, raw=False, # Ensure ask returns dict
291
+ optimizer=optimizer, conversationally=conversationally
292
+ )
293
+ return self.get_message(response_data) # get_message expects dict
294
+
295
+ return for_stream_chat() if stream else for_non_stream_chat()
269
296
 
270
297
  def get_message(self, response: dict) -> str:
271
298
  assert isinstance(response, dict), "Response should be of dict data-type only"
272
299
  return response["text"]
273
300
 
274
301
  if __name__ == "__main__":
302
+ # Ensure curl_cffi is installed
275
303
  print("-" * 80)
276
304
  print(f"{'Model':<50} {'Status':<10} {'Response'}")
277
305
  print("-" * 80)
@@ -15,6 +15,7 @@ class ElectronHub(Provider):
15
15
  A class to interact with the ElectronHub API with LitAgent user-agent.
16
16
  """
17
17
 
18
+ # Default models list (will be updated dynamically)
18
19
  AVAILABLE_MODELS = [
19
20
  # OpenAI GPT models
20
21
  "gpt-3.5-turbo",
@@ -498,6 +499,56 @@ class ElectronHub(Provider):
498
499
  "text-moderation-stable",
499
500
  "text-moderation-007"
500
501
  ]
502
+
503
+ @classmethod
504
+ def get_models(cls, api_key: str = None):
505
+ """Fetch available models from ElectronHub API.
506
+
507
+ Args:
508
+ api_key (str, optional): ElectronHub API key. If not provided, returns default models.
509
+
510
+ Returns:
511
+ list: List of available model IDs
512
+ """
513
+ if not api_key:
514
+ return cls.AVAILABLE_MODELS
515
+
516
+ try:
517
+ headers = {
518
+ 'Content-Type': 'application/json',
519
+ 'Accept': '*/*',
520
+ 'User-Agent': LitAgent().random(),
521
+ 'Authorization': f'Bearer {api_key}'
522
+ }
523
+
524
+ response = requests.get(
525
+ "https://api.electronhub.top/v1/models",
526
+ headers=headers,
527
+ timeout=10
528
+ )
529
+
530
+ if response.status_code != 200:
531
+ return cls.AVAILABLE_MODELS
532
+
533
+ data = response.json()
534
+ if "data" in data and isinstance(data["data"], list):
535
+ return [model["id"] for model in data["data"]]
536
+ return cls.AVAILABLE_MODELS
537
+
538
+ except Exception:
539
+ # Fallback to default models list if fetching fails
540
+ return cls.AVAILABLE_MODELS
541
+
542
+ @classmethod
543
+ def update_available_models(cls, api_key=None):
544
+ """Update the available models list from ElectronHub API"""
545
+ try:
546
+ models = cls.get_models(api_key)
547
+ if models and len(models) > 0:
548
+ cls.AVAILABLE_MODELS = models
549
+ except Exception:
550
+ # Fallback to default models list if fetching fails
551
+ pass
501
552
 
502
553
  def __init__(
503
554
  self,
@@ -515,6 +566,10 @@ class ElectronHub(Provider):
515
566
  api_key: str = None
516
567
  ):
517
568
  """Initializes the ElectronHub API client."""
569
+ # Update available models from API
570
+ self.update_available_models(api_key)
571
+
572
+ # Validate model after updating available models
518
573
  if model not in self.AVAILABLE_MODELS:
519
574
  raise ValueError(f"Invalid model: {model}. Choose from: {self.AVAILABLE_MODELS}")
520
575