g4f 6.9.9__py3-none-any.whl → 7.0.0__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.
@@ -263,6 +263,8 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
263
263
  has_audio = True
264
264
  break
265
265
  model = "openai-audio" if has_audio else cls.default_model
266
+ if not api_key:
267
+ api_key = AuthManager.load_api_key(cls)
266
268
  if cls.get_models(api_key=api_key, timeout=kwargs.get("timeout", 15)):
267
269
  if model in cls.model_aliases:
268
270
  model = cls.model_aliases[model]
@@ -379,7 +381,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
379
381
  return f"{url}&seed={seed}" if seed else url
380
382
 
381
383
  headers = None
382
- if api_key and api_key.startswith("g4f_") or api_key.startswith("gfs_"):
384
+ if api_key and (api_key.startswith("g4f_") or api_key.startswith("gfs_")):
383
385
  headers = {"authorization": f"Bearer {api_key}"}
384
386
  async with ClientSession(
385
387
  headers=DEFAULT_HEADERS,
g4f/Provider/Yupp.py CHANGED
@@ -6,12 +6,15 @@ import re
6
6
  import time
7
7
  import uuid
8
8
  from concurrent.futures import ThreadPoolExecutor
9
+ from typing import Optional, Dict, Any, List
9
10
 
10
11
  try:
11
12
  import cloudscraper
12
13
  from cloudscraper import CloudScraper
14
+ has_cloudscraper = True
13
15
  except ImportError:
14
16
  from typing import Type as CloudScraper
17
+ has_cloudscraper = False
15
18
 
16
19
  from .helper import get_last_user_message
17
20
  from .yupp.models import YuppModelManager
@@ -20,17 +23,27 @@ from ..debug import log
20
23
  from ..errors import RateLimitError, ProviderException, MissingAuthError, MissingRequirementsError
21
24
  from ..image import is_accepted_format, to_bytes
22
25
  from ..providers.base_provider import AsyncGeneratorProvider, ProviderModelMixin
23
- from ..providers.response import Reasoning, PlainTextResponse, PreviewResponse, JsonConversation, ImageResponse, \
24
- ProviderInfo, FinishReason, JsonResponse, VariantResponse
26
+ from ..providers.response import (
27
+ Reasoning,
28
+ PlainTextResponse,
29
+ PreviewResponse,
30
+ JsonConversation,
31
+ ImageResponse,
32
+ ProviderInfo,
33
+ FinishReason,
34
+ JsonResponse,
35
+ VariantResponse,
36
+ )
25
37
  from ..tools.auth import AuthManager
26
38
  from ..tools.media import merge_media
27
- from ..typing import AsyncResult, Messages, Optional, Dict, Any, List
39
+ from ..typing import AsyncResult, Messages
28
40
 
29
41
  YUPP_ACCOUNTS: List[Dict[str, Any]] = []
30
42
  account_rotation_lock = asyncio.Lock()
31
43
  ImagesCache: Dict[str, dict] = {}
32
44
  _accounts_loaded = False
33
- _executor = ThreadPoolExecutor(max_workers=10)
45
+ _executor = ThreadPoolExecutor(max_workers=32)
46
+ MAX_CACHE_SIZE = 1000
34
47
 
35
48
 
36
49
  def create_scraper():
@@ -87,10 +100,10 @@ async def get_best_yupp_account() -> Optional[Dict[str, Any]]:
87
100
  acc
88
101
  for acc in YUPP_ACCOUNTS
89
102
  if acc["is_valid"]
90
- and (
91
- acc["error_count"] < max_error_count
92
- or now - acc["last_used"] > error_cooldown
93
- )
103
+ and (
104
+ acc["error_count"] < max_error_count
105
+ or now - acc["last_used"] > error_cooldown
106
+ )
94
107
  ]
95
108
 
96
109
  if not valid_accounts:
@@ -98,8 +111,8 @@ async def get_best_yupp_account() -> Optional[Dict[str, Any]]:
98
111
 
99
112
  for acc in valid_accounts:
100
113
  if (
101
- acc["error_count"] >= max_error_count
102
- and now - acc["last_used"] > error_cooldown
114
+ acc["error_count"] >= max_error_count
115
+ and now - acc["last_used"] > error_cooldown
103
116
  ):
104
117
  acc["error_count"] = 0
105
118
 
@@ -109,11 +122,11 @@ async def get_best_yupp_account() -> Optional[Dict[str, Any]]:
109
122
  return account
110
123
 
111
124
 
112
- def sync_claim_yupp_reward(scraper: CloudScraper, account: Dict[str, Any], reward_id: str):
125
+ def sync_claim_yupp_reward(scraper: CloudScraper, account: Dict[str, Any], eval_id: str):
113
126
  try:
114
- log_debug(f"Claiming reward {reward_id}...")
127
+ log_debug(f"Claiming reward {eval_id}...")
115
128
  url = "https://yupp.ai/api/trpc/reward.claim?batch=1"
116
- payload = {"0": {"json": {"rewardId": reward_id}}}
129
+ payload = {"0": {"json": {"evalId": eval_id}}}
117
130
  scraper.cookies.set("__Secure-yupp.session-token", account['token'])
118
131
  response = scraper.post(url, json=payload)
119
132
  response.raise_for_status()
@@ -122,13 +135,104 @@ def sync_claim_yupp_reward(scraper: CloudScraper, account: Dict[str, Any], rewar
122
135
  log_debug(f"Reward claimed successfully. New balance: {balance}")
123
136
  return balance
124
137
  except Exception as e:
125
- log_debug(f"Failed to claim reward {reward_id}. Error: {e}")
138
+ log_debug(f"Failed to claim reward {eval_id}. Error: {e}")
139
+ return None
140
+
141
+
142
+ async def claim_yupp_reward(scraper: CloudScraper, account: Dict[str, Any], eval_id: str):
143
+ loop = asyncio.get_event_loop()
144
+ return await loop.run_in_executor(_executor, sync_claim_yupp_reward, scraper, account, eval_id)
145
+
146
+
147
+ def sync_record_model_feedback(
148
+ scraper: CloudScraper,
149
+ account: Dict[str, Any],
150
+ turn_id: str,
151
+ left_message_id: str,
152
+ right_message_id: str
153
+ ) -> Optional[str]:
154
+ try:
155
+ log_debug(f"Recording model feedback for turn {turn_id}...")
156
+ url = "https://yupp.ai/api/trpc/evals.recordModelFeedback?batch=1"
157
+ payload = {
158
+ "0": {
159
+ "json": {
160
+ "turnId": turn_id,
161
+ "evalType": "SELECTION",
162
+ "messageEvals": [
163
+ {
164
+ "messageId": right_message_id,
165
+ "rating": "GOOD",
166
+ "reasons": ["Fast"]
167
+ },
168
+ {
169
+ "messageId": left_message_id,
170
+ "rating": "BAD",
171
+ "reasons": []
172
+ }
173
+ ],
174
+ "comment": "",
175
+ "requireReveal": False
176
+ }
177
+ }
178
+ }
179
+ scraper.cookies.set("__Secure-yupp.session-token", account['token'])
180
+ response = scraper.post(url, json=payload)
181
+ response.raise_for_status()
182
+ data = response.json()
183
+
184
+ for result in data:
185
+ json_data = result.get("result", {}).get("data", {}).get("json", {})
186
+ eval_id = json_data.get("evalId")
187
+ final_reward = json_data.get("finalRewardAmount")
188
+ log_debug(f"Feedback recorded - evalId: {eval_id}, reward: {final_reward}")
189
+
190
+ if final_reward:
191
+ return eval_id
192
+ return None
193
+ except Exception as e:
194
+ log_debug(f"Failed to record model feedback. Error: {e}")
126
195
  return None
127
196
 
128
197
 
129
- async def claim_yupp_reward(scraper: CloudScraper, account: Dict[str, Any], reward_id: str):
198
+ async def record_model_feedback(
199
+ scraper: CloudScraper,
200
+ account: Dict[str, Any],
201
+ turn_id: str,
202
+ left_message_id: str,
203
+ right_message_id: str
204
+ ) -> Optional[str]:
205
+ loop = asyncio.get_event_loop()
206
+ return await loop.run_in_executor(
207
+ _executor, sync_record_model_feedback, scraper, account, turn_id, left_message_id, right_message_id
208
+ )
209
+
210
+
211
+ def sync_delete_chat(scraper: CloudScraper, account: Dict[str, Any], chat_id: str) -> bool:
212
+ try:
213
+ log_debug(f"Deleting chat {chat_id}...")
214
+ url = "https://yupp.ai/api/trpc/chat.deleteChat?batch=1"
215
+ payload = {"0": {"json": {"chatId": chat_id}}}
216
+ scraper.cookies.set("__Secure-yupp.session-token", account['token'])
217
+ response = scraper.post(url, json=payload)
218
+ response.raise_for_status()
219
+ data = response.json()
220
+ if (
221
+ isinstance(data, list) and len(data) > 0
222
+ and data[0].get("result", {}).get("data", {}).get("json") is None
223
+ ):
224
+ log_debug(f"Chat {chat_id} deleted successfully")
225
+ return True
226
+ log_debug(f"Unexpected response while deleting chat: {data}")
227
+ return False
228
+ except Exception as e:
229
+ log_debug(f"Failed to delete chat {chat_id}: {e}")
230
+ return False
231
+
232
+
233
+ async def delete_chat(scraper: CloudScraper, account: Dict[str, Any], chat_id: str) -> bool:
130
234
  loop = asyncio.get_event_loop()
131
- return await loop.run_in_executor(_executor, sync_claim_yupp_reward, scraper, account, reward_id)
235
+ return await loop.run_in_executor(_executor, sync_delete_chat, scraper, account, chat_id)
132
236
 
133
237
 
134
238
  def sync_make_chat_private(scraper: CloudScraper, account: Dict[str, Any], chat_id: str) -> bool:
@@ -148,8 +252,8 @@ def sync_make_chat_private(scraper: CloudScraper, account: Dict[str, Any], chat_
148
252
  response.raise_for_status()
149
253
  data = response.json()
150
254
  if (
151
- isinstance(data, list) and len(data) > 0
152
- and "json" in data[0].get("result", {}).get("data", {})
255
+ isinstance(data, list) and len(data) > 0
256
+ and "json" in data[0].get("result", {}).get("data", {})
153
257
  ):
154
258
  log_debug(f"Chat {chat_id} is now PRIVATE")
155
259
  return True
@@ -205,10 +309,18 @@ def format_messages_for_yupp(messages: Messages) -> str:
205
309
  return result
206
310
 
207
311
 
312
+ def evict_cache_if_needed():
313
+ global ImagesCache
314
+ if len(ImagesCache) > MAX_CACHE_SIZE:
315
+ keys_to_remove = list(ImagesCache.keys())[:len(ImagesCache) - MAX_CACHE_SIZE + 100]
316
+ for key in keys_to_remove:
317
+ del ImagesCache[key]
318
+
319
+
208
320
  class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
209
321
  url = "https://yupp.ai"
210
322
  login_url = "https://discord.gg/qXA4Wf4Fsm"
211
- working = CloudScraper is not None
323
+ working = has_cloudscraper
212
324
  active_by_default = True
213
325
  supports_stream = True
214
326
  image_cache = True
@@ -228,8 +340,10 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
228
340
  cls.models_tags = {model.get("name"): manager.processor.generate_tags(model) for model in models}
229
341
  cls.models = [model.get("name") for model in models]
230
342
  cls.image_models = [model.get("name") for model in models if model.get("isImageGeneration")]
231
- cls.vision_models = [model.get("name") for model in models if
232
- "image/*" in model.get("supportedAttachmentMimeTypes", [])]
343
+ cls.vision_models = [
344
+ model.get("name") for model in models
345
+ if "image/*" in model.get("supportedAttachmentMimeTypes", [])
346
+ ]
233
347
  return cls.models
234
348
 
235
349
  @classmethod
@@ -271,8 +385,15 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
271
385
 
272
386
  attachment_resp = scraper.post(
273
387
  "https://yupp.ai/api/trpc/chat.createAttachmentForUploadedFile?batch=1",
274
- json={"0": {"json": {"fileName": name, "contentType": is_accepted_format(data),
275
- "fileId": upload_info["fileId"]}}},
388
+ json={
389
+ "0": {
390
+ "json": {
391
+ "fileName": name,
392
+ "contentType": is_accepted_format(data),
393
+ "fileId": upload_info["fileId"]
394
+ }
395
+ }
396
+ },
276
397
  cookies={"__Secure-yupp.session-token": account["token"]}
277
398
  )
278
399
  attachment_resp.raise_for_status()
@@ -283,6 +404,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
283
404
  "attachmentId": attachment["attachment_id"],
284
405
  "chatMessageId": ""
285
406
  }
407
+ evict_cache_if_needed()
286
408
  ImagesCache[image_hash] = file_info
287
409
  files.append(file_info)
288
410
  return files
@@ -314,23 +436,26 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
314
436
  return await loop.run_in_executor(_executor, cls.sync_get_signed_image, scraper, image_id)
315
437
 
316
438
  @classmethod
317
- def sync_stream_request(cls, scraper: CloudScraper, url: str, payload: list, headers: dict,
318
- timeout: int):
439
+ def sync_stream_request(cls, scraper: CloudScraper, url: str, payload: list, headers: dict, timeout: int):
319
440
  response = scraper.post(url, json=payload, headers=headers, stream=True, timeout=timeout)
320
441
  response.raise_for_status()
321
442
  return response
322
443
 
323
444
  @classmethod
324
445
  async def create_async_generator(
325
- cls,
326
- model: str,
327
- messages: Messages,
328
- proxy: str = None,
329
- **kwargs,
446
+ cls,
447
+ model: str,
448
+ messages: Messages,
449
+ proxy: str = None,
450
+ api_key: str = None,
451
+ **kwargs,
330
452
  ) -> AsyncResult:
331
- if CloudScraper is None:
332
- raise MissingRequirementsError("cloudscraper library is required for Yupp provider | install it via 'pip install cloudscraper'")
333
- api_key = kwargs.get("api_key")
453
+ if not has_cloudscraper:
454
+ raise MissingRequirementsError(
455
+ "cloudscraper library is required for Yupp provider | install it via 'pip install cloudscraper'"
456
+ )
457
+ if not api_key:
458
+ api_key = AuthManager.load_api_key(cls)
334
459
  if not api_key:
335
460
  api_key = get_cookies("yupp.ai", False).get("__Secure-yupp.session-token")
336
461
  if api_key:
@@ -351,7 +476,8 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
351
476
  prompt = get_last_user_message(messages, prompt)
352
477
 
353
478
  log_debug(
354
- f"Use url_uuid: {url_uuid}, Formatted prompt length: {len(prompt)}, Is new conversation: {is_new_conversation}")
479
+ f"Use url_uuid: {url_uuid}, Formatted prompt length: {len(prompt)}, Is new conversation: {is_new_conversation}"
480
+ )
355
481
 
356
482
  max_attempts = len(YUPP_ACCOUNTS)
357
483
  for attempt in range(max_attempts):
@@ -437,10 +563,13 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
437
563
  timeout
438
564
  )
439
565
 
440
- asyncio.create_task(make_chat_private(scraper, account, url_uuid))
441
-
442
- async for chunk in cls._process_stream_response(response, account, scraper, prompt, model):
443
- yield chunk
566
+ try:
567
+ async for chunk in cls._process_stream_response(response, account, scraper, prompt, model):
568
+ yield chunk
569
+ finally:
570
+ response.close()
571
+ if not kwargs.get("conversation"):
572
+ asyncio.create_task(delete_chat(scraper, account, url_uuid))
444
573
  return
445
574
 
446
575
  except RateLimitError:
@@ -462,7 +591,8 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
462
591
  log_debug(f"Unexpected error with account ...{account['token'][-4:]}: {str(e)}")
463
592
  error_str = str(e).lower()
464
593
  if "500" in error_str or "internal server error" in error_str:
465
- account["is_valid"] = False
594
+ async with account_rotation_lock:
595
+ account["error_count"] += 1
466
596
  continue
467
597
  async with account_rotation_lock:
468
598
  account["error_count"] += 1
@@ -472,12 +602,12 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
472
602
 
473
603
  @classmethod
474
604
  async def _process_stream_response(
475
- cls,
476
- response,
477
- account: Dict[str, Any],
478
- scraper: CloudScraper,
479
- prompt: str,
480
- model_id: str
605
+ cls,
606
+ response,
607
+ account: Dict[str, Any],
608
+ scraper: CloudScraper,
609
+ prompt: str,
610
+ model_id: str
481
611
  ) -> AsyncResult:
482
612
  line_pattern = re.compile(b"^([0-9a-fA-F]+):(.*)")
483
613
  target_stream_id = None
@@ -562,6 +692,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
562
692
  is_started: bool = False
563
693
  variant_image: Optional[ImageResponse] = None
564
694
  reward_id = "a"
695
+ reward_kw = {}
565
696
  routing_id = "e"
566
697
  turn_id = None
567
698
  persisted_turn_id = None
@@ -642,7 +773,8 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
642
773
  right_stream = data.get("rightStream", {})
643
774
  if data.get("quickResponse", {}) != "$undefined":
644
775
  quick_response_id = extract_ref_id(
645
- data.get("quickResponse", {}).get("stream", {}).get("next"))
776
+ data.get("quickResponse", {}).get("stream", {}).get("next")
777
+ )
646
778
 
647
779
  if data.get("turnId", {}) != "$undefined":
648
780
  turn_id = extract_ref_id(data.get("turnId", {}).get("next"))
@@ -693,20 +825,20 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
693
825
  if ref_name in image_blocks:
694
826
  img_block_text = image_blocks[ref_name]
695
827
  async for chunk in process_content_chunk(
696
- img_block_text,
697
- ref_name,
698
- line_count,
699
- for_target=True
828
+ img_block_text,
829
+ ref_name,
830
+ line_count,
831
+ for_target=True
700
832
  ):
701
833
  stream["target"].append(chunk)
702
834
  is_started = True
703
835
  yield chunk
704
836
  else:
705
837
  async for chunk in process_content_chunk(
706
- content,
707
- chunk_id,
708
- line_count,
709
- for_target=True
838
+ content,
839
+ chunk_id,
840
+ line_count,
841
+ for_target=True
710
842
  ):
711
843
  stream["target"].append(chunk)
712
844
  is_started = True
@@ -719,10 +851,10 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
719
851
  content = data.get("curr", "")
720
852
  if content:
721
853
  async for chunk in process_content_chunk(
722
- content,
723
- chunk_id,
724
- line_count,
725
- for_target=False
854
+ content,
855
+ chunk_id,
856
+ line_count,
857
+ for_target=False
726
858
  ):
727
859
  stream["variant"].append(chunk)
728
860
  if isinstance(chunk, ImageResponse):
@@ -738,32 +870,35 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
738
870
  content = data.get("curr", "")
739
871
  if content:
740
872
  async for chunk in process_content_chunk(
741
- content,
742
- chunk_id,
743
- line_count,
744
- for_target=False
873
+ content,
874
+ chunk_id,
875
+ line_count,
876
+ for_target=False
745
877
  ):
746
878
  stream["quick"].append(chunk)
747
879
  quick_content += content
748
880
  yield PreviewResponse(content)
749
881
 
750
- elif chunk_id in [turn_id, persisted_turn_id]:
882
+ elif chunk_id == turn_id:
883
+ reward_kw["turn_id"] = data.get("curr", "")
884
+
885
+ elif chunk_id == persisted_turn_id:
751
886
  pass
752
887
 
753
888
  elif chunk_id == right_message_id:
754
- pass
889
+ reward_kw["right_message_id"] = data.get("curr", "")
755
890
 
756
891
  elif chunk_id == left_message_id:
757
- pass
892
+ reward_kw["left_message_id"] = data.get("curr", "")
758
893
 
759
894
  elif isinstance(data, dict) and "curr" in data:
760
895
  content = data.get("curr", "")
761
896
  if content:
762
897
  async for chunk in process_content_chunk(
763
- content,
764
- chunk_id,
765
- line_count,
766
- for_target=False
898
+ content,
899
+ chunk_id,
900
+ line_count,
901
+ for_target=False
767
902
  ):
768
903
  stream["extra"].append(chunk)
769
904
  if isinstance(chunk, str) and "<streaming stopped unexpectedly" in chunk:
@@ -779,7 +914,14 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
779
914
  log_debug(f"Finished processing {line_count} lines")
780
915
 
781
916
  finally:
782
- if reward_info and "unclaimedRewardInfo" in reward_info:
783
- rid = reward_info["unclaimedRewardInfo"].get("rewardId")
784
- if rid:
785
- await claim_yupp_reward(scraper, account, rid)
917
+ log_debug(f"Get Reward: {reward_kw}")
918
+ if reward_kw.get("turn_id") and reward_kw.get("left_message_id") and reward_kw.get("right_message_id"):
919
+ eval_id = await record_model_feedback(
920
+ scraper,
921
+ account,
922
+ reward_kw["turn_id"],
923
+ reward_kw["left_message_id"],
924
+ reward_kw["right_message_id"]
925
+ )
926
+ if eval_id:
927
+ await claim_yupp_reward(scraper, account, eval_id)
g4f/errors.py CHANGED
@@ -49,7 +49,7 @@ class MissingRequirementsError(G4FError):
49
49
 
50
50
 
51
51
  class NestAsyncioError(MissingRequirementsError):
52
- """Raised when 'nest_asyncio' is missing."""
52
+ """Raised when 'nest_asyncio2' is missing."""
53
53
  pass
54
54
 
55
55
 
@@ -331,7 +331,7 @@ class Backend_Api(Api):
331
331
  for chunk in response:
332
332
  if isinstance(chunk, FinishReason):
333
333
  yield f"[{chunk.reason}]" if chunk.reason != "stop" else ""
334
- elif not isinstance(chunk, Exception):
334
+ elif not isinstance(chunk, (Exception, JsonResponse)):
335
335
  chunk = str(chunk)
336
336
  if chunk:
337
337
  yield chunk
g4f/mcp/server.py CHANGED
@@ -10,13 +10,21 @@ The server exposes tools for:
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import os
14
+ import re
13
15
  import sys
14
16
  import json
15
17
  import asyncio
18
+ import hashlib
19
+ from email.utils import formatdate
16
20
  from typing import Any, Dict, List, Optional, Union
17
21
  from dataclasses import dataclass
22
+ from urllib.parse import unquote_plus
18
23
 
19
24
  from ..debug import enable_logging
25
+ from ..cookies import read_cookie_files
26
+ from ..image import EXTENSIONS_MAP
27
+ from ..image.copy_images import get_media_dir, copy_media, get_source_url
20
28
 
21
29
  from .tools import MarkItDownTool, TextToAudioTool, WebSearchTool, WebScrapeTool, ImageGenerationTool
22
30
  from .tools import WebSearchTool, WebScrapeTool, ImageGenerationTool
@@ -214,6 +222,7 @@ class MCPServer:
214
222
  sys.exit(1)
215
223
 
216
224
  enable_logging()
225
+ read_cookie_files()
217
226
 
218
227
  async def handle_mcp_request(request: web.Request) -> web.Response:
219
228
  nonlocal origin
@@ -273,17 +282,120 @@ class MCPServer:
273
282
  "server": self.server_info
274
283
  })
275
284
 
285
+ async def handle_media(request: web.Request) -> web.Response:
286
+ """Serve media files from generated_media directory"""
287
+ filename = request.match_info.get('filename', '')
288
+ if not filename:
289
+ return web.Response(status=404, text="File not found")
290
+
291
+ def get_timestamp(s):
292
+ m = re.match("^[0-9]+", s)
293
+ return int(m.group(0)) if m else 0
294
+
295
+ target = os.path.join(get_media_dir(), os.path.basename(filename))
296
+
297
+ # Try URL-decoded filename if not found
298
+ if not os.path.isfile(target):
299
+ other_name = os.path.join(get_media_dir(), os.path.basename(unquote_plus(filename)))
300
+ if os.path.isfile(other_name):
301
+ target = other_name
302
+
303
+ # Get file extension and mime type
304
+ ext = os.path.splitext(filename)[1][1:].lower()
305
+ mime_type = EXTENSIONS_MAP.get(ext, "application/octet-stream")
306
+
307
+ # Try to fetch from backend if file doesn't exist
308
+ if not os.path.isfile(target) and mime_type != "application/octet-stream":
309
+ source_url = get_source_url(str(request.query_string))
310
+ ssl = None
311
+ if source_url is not None:
312
+ try:
313
+ await copy_media([source_url], target=target, ssl=ssl)
314
+ sys.stderr.write(f"File copied from {source_url}\n")
315
+ except Exception as e:
316
+ sys.stderr.write(f"Download failed: {source_url} - {e}\n")
317
+ raise web.HTTPFound(location=source_url)
318
+
319
+ if not os.path.isfile(target):
320
+ return web.Response(status=404, text="File not found")
321
+
322
+ # Build response headers
323
+ stat_result = os.stat(target)
324
+ headers = {
325
+ "cache-control": "public, max-age=31536000",
326
+ "last-modified": formatdate(get_timestamp(filename), usegmt=True),
327
+ "etag": f'"{hashlib.md5(filename.encode()).hexdigest()}"',
328
+ "content-length": str(stat_result.st_size),
329
+ "content-type": mime_type,
330
+ "access-control-allow-origin": "*",
331
+ }
332
+
333
+ # Check for conditional request
334
+ if_none_match = request.headers.get("if-none-match")
335
+ if if_none_match:
336
+ etag = headers["etag"]
337
+ if etag in [tag.strip(" W/") for tag in if_none_match.split(",")]:
338
+ return web.Response(status=304, headers=headers)
339
+
340
+ # Serve the file
341
+ return web.FileResponse(target, headers=headers)
342
+
343
+ async def handle_synthesize(request: web.Request) -> web.Response:
344
+ """Handle synthesize requests for text-to-speech"""
345
+ provider_name = request.match_info.get('provider', '')
346
+ if not provider_name:
347
+ return web.Response(status=400, text="Provider not specified")
348
+
349
+ try:
350
+ from ..Provider import ProviderUtils
351
+ provider_handler = ProviderUtils.convert.get(provider_name)
352
+ if provider_handler is None:
353
+ return web.Response(status=404, text=f"Provider not found: {provider_name}")
354
+ except Exception as e:
355
+ return web.Response(status=404, text=f"Provider not found: {provider_name}")
356
+
357
+ if not hasattr(provider_handler, "synthesize"):
358
+ return web.Response(status=500, text=f"Provider doesn't support synthesize: {provider_name}")
359
+
360
+ # Get query parameters
361
+ params = dict(request.query)
362
+
363
+ try:
364
+ # Call the synthesize method
365
+ response_data = provider_handler.synthesize(params)
366
+
367
+ # Handle async generator
368
+ async def generate():
369
+ async for chunk in response_data:
370
+ yield chunk
371
+
372
+ content_type = getattr(provider_handler, "synthesize_content_type", "application/octet-stream")
373
+ return web.Response(
374
+ body=b"".join([chunk async for chunk in generate()]),
375
+ content_type=content_type,
376
+ headers={
377
+ "cache-control": "max-age=604800",
378
+ "access-control-allow-origin": "*",
379
+ }
380
+ )
381
+ except Exception as e:
382
+ sys.stderr.write(f"Synthesize error: {e}\n")
383
+ return web.Response(status=500, text=f"Synthesize error: {str(e)}")
384
+
276
385
  # Create aiohttp application
277
386
  app = web.Application()
278
387
  app.router.add_options('/mcp', lambda request: web.Response(headers={"access-control-allow-origin": "*", "access-control-allow-methods": "POST, OPTIONS", "access-control-allow-headers": "Content-Type"}))
279
388
  app.router.add_post('/mcp', handle_mcp_request)
280
389
  app.router.add_get('/health', handle_health)
390
+ app.router.add_get('/media/{filename:.*}', handle_media)
391
+ app.router.add_get('/backend-api/v2/synthesize/{provider}', handle_synthesize)
281
392
 
282
393
  # Start server
283
394
  sys.stderr.write(f"Starting {self.server_info['name']} v{self.server_info['version']} (HTTP mode)\n")
284
395
  sys.stderr.write(f"Listening on http://{host}:{port}\n")
285
396
  sys.stderr.write(f"MCP endpoint: http://{host}:{port}/mcp\n")
286
397
  sys.stderr.write(f"Health check: http://{host}:{port}/health\n")
398
+ sys.stderr.write(f"Media files: http://{host}:{port}/media/{{filename}}\n")
287
399
  sys.stderr.flush()
288
400
 
289
401
  runner = web.AppRunner(app)
g4f/mcp/tools.py CHANGED
@@ -241,8 +241,7 @@ class ImageGenerationTool(MCPTool):
241
241
  model=model,
242
242
  prompt=prompt,
243
243
  width=width,
244
- height=height,
245
- response_format="url"
244
+ height=height
246
245
  )
247
246
 
248
247
  # Get the image data with proper validation
g4f/providers/asyncio.py CHANGED
@@ -7,7 +7,7 @@ from typing import Optional, Callable, AsyncIterator, Iterator
7
7
  from ..errors import NestAsyncioError
8
8
 
9
9
  try:
10
- import nest_asyncio
10
+ import nest_asyncio2 as nest_asyncio
11
11
  has_nest_asyncio = True
12
12
  except ImportError:
13
13
  has_nest_asyncio = False
@@ -28,7 +28,7 @@ def get_running_loop(check_nested: bool) -> Optional[AbstractEventLoop]:
28
28
  if has_nest_asyncio:
29
29
  nest_asyncio.apply(loop)
30
30
  elif check_nested:
31
- raise NestAsyncioError('Install "nest_asyncio" package | pip install -U nest_asyncio')
31
+ raise NestAsyncioError('Install "nest-asyncio2" package | pip install -U nest-asyncio2')
32
32
  return loop
33
33
  except RuntimeError:
34
34
  pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: g4f
3
- Version: 6.9.9
3
+ Version: 7.0.0
4
4
  Summary: The official gpt4free repository | various collection of powerful language models
5
5
  Home-page: https://github.com/xtekky/gpt4free
6
6
  Author: Tekky
@@ -20,7 +20,7 @@ Requires-Dist: requests
20
20
  Requires-Dist: aiohttp
21
21
  Requires-Dist: brotli
22
22
  Requires-Dist: pycryptodome
23
- Requires-Dist: nest_asyncio
23
+ Requires-Dist: nest-asyncio2
24
24
  Provides-Extra: all
25
25
  Requires-Dist: curl_cffi>=0.6.2; extra == "all"
26
26
  Requires-Dist: certifi; extra == "all"
@@ -42,6 +42,7 @@ Requires-Dist: setuptools; extra == "all"
42
42
  Requires-Dist: markitdown[all]; extra == "all"
43
43
  Requires-Dist: python-dotenv; extra == "all"
44
44
  Requires-Dist: aiofile; extra == "all"
45
+ Requires-Dist: cloudscraper; extra == "all"
45
46
  Provides-Extra: slim
46
47
  Requires-Dist: curl_cffi>=0.6.2; extra == "slim"
47
48
  Requires-Dist: certifi; extra == "slim"
@@ -61,6 +62,7 @@ Requires-Dist: pypdf2; extra == "slim"
61
62
  Requires-Dist: python-docx; extra == "slim"
62
63
  Requires-Dist: python-dotenv; extra == "slim"
63
64
  Requires-Dist: aiofile; extra == "slim"
65
+ Requires-Dist: cloudscraper; extra == "slim"
64
66
  Provides-Extra: image
65
67
  Requires-Dist: pillow; extra == "image"
66
68
  Requires-Dist: cairosvg; extra == "image"
@@ -3,7 +3,7 @@ g4f/__main__.py,sha256=HHFLPRNjEHse-6-i_58Y9AKtEtzGJtxsZPXhymCmIJg,63
3
3
  g4f/config.py,sha256=qnhX4a2J17CLkooHoMg8mETM8SjoeqJ-Lu1WiRVzIB0,1160
4
4
  g4f/cookies.py,sha256=gx68OX5he2bYvsvvIQEESMUfwnVI18DOMJt9KvQrM8g,7915
5
5
  g4f/debug.py,sha256=Dh0cmlxUydMDfjJBiPWrO9dAuvv2sTElb3R036ax-34,1021
6
- g4f/errors.py,sha256=8Mp77UMW9_c8Flr1Z0Cvdrv0ixOF9CE0j1QX-cc5Zfc,2368
6
+ g4f/errors.py,sha256=FCD6ZkTdfxB4nBjubg7Y8cStGJ5PsYJoVwyUiHL46Z8,2369
7
7
  g4f/files.py,sha256=JzlWe3V6Xc7FN4eag7Utc4P9dHN1s1sMKFvYSaM0NQg,907
8
8
  g4f/models.py,sha256=QZV4UAis0QYaWGVO4Nv6n_3wMrLQvWHJ9V11Lo1EmBQ,24927
9
9
  g4f/typing.py,sha256=fBBb74HkxtLa358vjcd1U5yr3ThlKzP2DfE8wdx01lA,2388
@@ -23,13 +23,13 @@ g4f/Provider/Mintlify.py,sha256=3Bvy1oh6scg4xs9pw5RY4mmi8XEvQDelnNp664TefbU,7937
23
23
  g4f/Provider/OIVSCodeSer.py,sha256=clebON7Ssd9syewh3BWT59XOeB-WORXF6FPOwbzfRmo,1366
24
24
  g4f/Provider/OperaAria.py,sha256=sLnTOKzbW9BxTxNmHXJ-YDnhPU6Dj6MBdDfqDh-Zz-c,14662
25
25
  g4f/Provider/Perplexity.py,sha256=GUebbVin9hCg4FDei9RX2N6WboFcDDPm061WnnpPRaI,13236
26
- g4f/Provider/PollinationsAI.py,sha256=Mhrb-Ym9N6RXTONWOlElBOW__g4KPiwpyeUa9L72waI,21736
26
+ g4f/Provider/PollinationsAI.py,sha256=8LMR8wl3Lh-pzhUHJW71rObSHgBeo7p4LL5GD6IWe3Y,21814
27
27
  g4f/Provider/PollinationsImage.py,sha256=wdGY9kbPGlqAkyeJcjXgWOG3HLVPU4QK-JENwg3gmwk,2297
28
28
  g4f/Provider/Qwen.py,sha256=XPJHRlobijqjdDGVqA-aVyRx2SeM28zSWf-NmzxJtgE,23876
29
29
  g4f/Provider/TeachAnything.py,sha256=ST87YdOdxtIc5JMaKzGdVy9J9wlhdhYIchRtXBljIBU,2843
30
30
  g4f/Provider/WeWordle.py,sha256=ocKEfMYBKWPzv2WoDntn-WC-DkrSg5b_ipL9VYhTgsw,7007
31
31
  g4f/Provider/Yqcloud.py,sha256=xzX-G-Lv0pyOHTypSJdavmbm3GPxnL-wRoT3VmjzUIM,3213
32
- g4f/Provider/Yupp.py,sha256=StAyO9qFalvwOsj3RkkOJdkJMItvmBYwgwFx8GV0IT8,32392
32
+ g4f/Provider/Yupp.py,sha256=E3ft5HshW-IfGdE2KtdpNOmbBkBmBeyl-c8xM-BTpEo,36906
33
33
  g4f/Provider/__init__.py,sha256=2azA2zjXZjkH3yDJSoWKeB3SCGB7SIa_0_DN44Lf39g,2684
34
34
  g4f/Provider/base_provider.py,sha256=lAd80-2hO1kzs9n9TUKv6fR4zHjSdZBqVErpm_gna9o,199
35
35
  g4f/Provider/helper.py,sha256=_4fO_b2U8BnpuF6kijUM4SaFbX2Pcz6mksxK9mVXhP4,111
@@ -159,7 +159,7 @@ g4f/gui/webview.py,sha256=OcXnxG41h4RFSAQqq7pUBUv2UL3rbLlU3_RsOydabgs,1338
159
159
  g4f/gui/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
160
  g4f/gui/server/api.py,sha256=a0Yt2r-JpL6roGEZUCGKO20v1x8dY2jQ6uqCVWzd4tA,16641
161
161
  g4f/gui/server/app.py,sha256=HwJeICAyMkrSQdrUiW2DhZGwe_l40Qb21dF_HiTgMv0,92
162
- g4f/gui/server/backend_api.py,sha256=ZQv3OOtvGstky5mcOYcdH-yI1J7feJqQSBHvTDIZGrI,29960
162
+ g4f/gui/server/backend_api.py,sha256=cfgwrKaEA-efmKKt3Lm8uVfngSzHDCKgpC4ZeRFHthk,29976
163
163
  g4f/gui/server/config.py,sha256=RCpWrnnfJ5Lmq63r6_rz2cxicCadLUgCxoRFZUgUs3s,15354
164
164
  g4f/gui/server/crypto.py,sha256=6irNKqjlKfwnk4MY3TOhFlBWM2mZ6zjZaDmnanub_rc,2786
165
165
  g4f/gui/server/internet.py,sha256=NSItPuwLwIegcUmfjw5SBOtBQroNQp7osd2pCNFGx6g,109
@@ -183,12 +183,12 @@ g4f/locals/models.py,sha256=9uzKnQwLBjtjGU6G3f13jAY5Y9aI2PD4hTi4_bFMqa4,1674
183
183
  g4f/locals/provider.py,sha256=2Wk2PKzB005kYjHEABF_5a4j3qgxxIzgKMK28ewi3sg,2858
184
184
  g4f/mcp/__init__.py,sha256=9S6ReeCAJIiWTpkLRvrk1KwfEsT1GKO7_chQoue0J5Q,573
185
185
  g4f/mcp/__main__.py,sha256=zmnb03P5of2g8IZymmrRrYpMo6kKgQWD-0QemIcguOc,184
186
- g4f/mcp/server.py,sha256=8JOumIkT-GEY89LFnsgYTMpmnlZUYIo958qnbBsrPaI,11580
187
- g4f/mcp/tools.py,sha256=H_DSRryz2zM7G_XOnXw0tUc-8ExJ9ZaAkSGIxO5UpTw,15348
186
+ g4f/mcp/server.py,sha256=zZq4moxcp0sI2w3t1kt3JAC529yfbClchgWK4Y0V7Rw,16900
187
+ g4f/mcp/tools.py,sha256=G09YfVemVsPbkMLIr5iiE0jHvIUj2Of8Rc02dUaHzzU,15309
188
188
  g4f/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
189
189
  g4f/providers/any_model_map.py,sha256=Go7AaJUyvI3NFA5AwjzT7sL8Suk7B0OhtqpzHb512g0,230475
190
190
  g4f/providers/any_provider.py,sha256=EnK9VvBGEUGEe66QsDXEaxL7tvrnNea1ZKukbnMX2uc,19250
191
- g4f/providers/asyncio.py,sha256=aZTeaUYf0wZmJ6r70gK4v4fkNEhuBA0cGmjrUFICAN4,2707
191
+ g4f/providers/asyncio.py,sha256=LTbN0_S8Y5XJXIZcUhivbBpbcYjEOPxdOdEcPBOQTUs,2726
192
192
  g4f/providers/base_provider.py,sha256=AEoeJd1gkfhmlJ2AAz3QEh_8jE2xAVOp019ABBAaBIE,20010
193
193
  g4f/providers/create_images.py,sha256=yIDZvXUXqnBTQDzpq_NFmJRzxC7RI6VL5UDpN1YKbOU,6607
194
194
  g4f/providers/helper.py,sha256=L2k0BoFjNjj5DUlZUo59h6zZP6dKAKKKeBeZYAwCQHo,5603
@@ -208,9 +208,9 @@ g4f/tools/files.py,sha256=bUTbeNp8ujTZQiW3GLTtFcgl3-1AnH3cDyIKHfh6Mjc,23397
208
208
  g4f/tools/media.py,sha256=AE9hGVRxQBVZzQ_Ylzeoo2TJUGXSBXO5RbLwj1I2ZTE,4701
209
209
  g4f/tools/run_tools.py,sha256=Yb0osWdDt4wUnkvwln4R6qcLapdddC3n2giZMPXWkzM,16662
210
210
  g4f/tools/web_search.py,sha256=vAZJD-qy15llsgAbkXzoEltqdFB6TlKLbqDJ1DS-6vs,2086
211
- g4f-6.9.9.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
212
- g4f-6.9.9.dist-info/METADATA,sha256=dpbXvC6fuScpC1DzPLZsVooh8dnODUlQdonjYOzYx2U,23255
213
- g4f-6.9.9.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
214
- g4f-6.9.9.dist-info/entry_points.txt,sha256=J7Usl6dNjXJlvuzGAUEm6cuDXWpVdGq7SfK-tPoiZSI,67
215
- g4f-6.9.9.dist-info/top_level.txt,sha256=bMRlTupWYCcLWy80AnnKZkhpBsXsF8gI3BaMhSZSgRo,4
216
- g4f-6.9.9.dist-info/RECORD,,
211
+ g4f-7.0.0.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
212
+ g4f-7.0.0.dist-info/METADATA,sha256=LQ7GIFlNobe_iV344kFxuw-wDxECP2yW-hA1u5cLZcQ,23345
213
+ g4f-7.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
214
+ g4f-7.0.0.dist-info/entry_points.txt,sha256=J7Usl6dNjXJlvuzGAUEm6cuDXWpVdGq7SfK-tPoiZSI,67
215
+ g4f-7.0.0.dist-info/top_level.txt,sha256=bMRlTupWYCcLWy80AnnKZkhpBsXsF8gI3BaMhSZSgRo,4
216
+ g4f-7.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5