XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

perf(core): implement lazy loading for providers and metadata to slash startup time

- Implemented PEP 562 in to defer heavy module imports. - Refactored to use string identifiers for default providers instead of eager class dependencies. - Lazy-loaded in to save ~250ms on startup. - Upgraded and the GUI API to dynamically resolve string-based provider maps at runtime. - Reduced baseline CLI import latency from ~3.1s to ~0.4s.

d9fd7862
kqlio67 <kqlio67@users.noreply.github.com>
提交于

代码差异

17 个文件 +558 -346
Modified .gitignore +2 -1
@@ -11,4 +11,5 @@ pyvenv.cfg
11 11 lib64
12 12 /.idea
13 13 container/
14 g4f.dev
14 g4f.dev
15 har_and_cookies
Modified g4f/Provider/OllamaSwarm.py +3 -0
@@ -600,6 +600,9 @@ class OllamaSwarm(OpenaiTemplate):
600 600 if not cls.models:
601 601 cls.get_models()
602 602
603 if not model or model == "default":
604 model = getattr(cls, "default_model", "qwen3:14b")
605
603 606 server_urls = cls.model_to_servers.get(model)
604 607 if server_urls is None:
605 608 resolved = cls.get_model(model)
Modified g4f/Provider/__init__.py +183 -67
@@ -1,87 +1,203 @@
1 1 from __future__ import annotations
2 2
3 import importlib
3 4 from ..providers.types import BaseProvider, ProviderType
4 5 from ..providers.retry_provider import RetryProvider, IterListProvider, RotatedProvider
5 6 from ..providers.base_provider import AsyncProvider, AsyncGeneratorProvider
6 7 from ..providers.create_images import CreateImagesProvider
7 8 from .. import debug
8 9
9 from .needs_auth import *
10 from .needs_auth.hf import HuggingFace, HuggingChat, HuggingFaceAPI, HuggingFaceInference, HuggingFaceMedia
11 try:
12 from .needs_auth.mini_max import HailuoAI, MiniMax
13 except ImportError as e:
14 debug.error("MiniMax providers not loaded:", e)
15 try:
16 from .local import *
17 except ImportError as e:
18 debug.error("Local providers not loaded:", e)
19 try:
20 from .hf_space import *
21 except ImportError as e:
22 debug.error("HuggingFace Space providers not loaded:", e)
23 try:
24 from .search import *
25 except ImportError as e:
26 debug.error("Search providers not loaded:", e)
10 __map_paths__ = {
11 "AIBadgr": "g4f.Provider.needs_auth.AIBadgr",
12 "Anthropic": "g4f.Provider.needs_auth.Anthropic",
13 "Antigravity": "g4f.Provider.needs_auth.Antigravity",
14 "ApiAirforce": "g4f.Provider.needs_auth.ApiAirforce",
15 "AsyncGeneratorProvider": "g4f.providers.base_provider",
16 "AsyncProvider": "g4f.providers.base_provider",
17 "Azure": "g4f.Provider.needs_auth.Azure",
18 "BackendApi": "g4f.Provider.template.BackendApi",
19 "BaseProvider": "g4f.providers.types",
20 "BingCreateImages": "g4f.Provider.needs_auth.BingCreateImages",
21 "BlackForestLabs_Flux1Dev": "g4f.Provider.hf_space.BlackForestLabs_Flux1Dev",
22 "BlackForestLabs_Flux1KontextDev": "g4f.Provider.hf_space.BlackForestLabs_Flux1KontextDev",
23 "BlackboxPro": "g4f.Provider.needs_auth.BlackboxPro",
24 "CablyAI": "g4f.Provider.needs_auth.CablyAI",
25 "CachedSearch": "g4f.Provider.search.CachedSearch",
26 "Cerebras": "g4f.Provider.needs_auth.Cerebras",
27 "Claude": "g4f.Provider.needs_auth.Claude",
28 "Cloudflare": "g4f.Provider.Cloudflare",
29 "Cohere": "g4f.Provider.needs_auth.Cohere",
30 "CohereForAI_C4AI_Command": "g4f.Provider.hf_space.CohereForAI_C4AI_Command",
31 "Copilot": "g4f.Provider.Copilot",
32 "CopilotAccount": "g4f.Provider.needs_auth.CopilotAccount",
33 "CopilotApp": "g4f.Provider.CopilotApp",
34 "CopilotSession": "g4f.Provider.CopilotSession",
35 "CreateImagesProvider": "g4f.providers.create_images",
36 "Custom": "g4f.Provider.needs_auth.Custom",
37 "DeepInfra": "g4f.Provider.DeepInfra",
38 "DeepSeek": "g4f.Provider.needs_auth.DeepSeek",
39 "DeepSeekAPI": "g4f.Provider.needs_auth.DeepSeekAPI",
40 "EasyChat": "g4f.Provider.EasyChat",
41 "EdgeTTS": "g4f.Provider.audio.EdgeTTS",
42 "Feature": "g4f.Provider.needs_auth.Custom",
43 "Felo": "g4f.Provider.Felo",
44 "FenayAI": "g4f.Provider.needs_auth.FenayAI",
45 "GLM": "g4f.Provider.GLM",
46 "Gemini": "g4f.Provider.needs_auth.Gemini",
47 "GeminiCLI": "g4f.Provider.needs_auth.GeminiCLI",
48 "GeminiPro": "g4f.Provider.needs_auth.GeminiPro",
49 "GigaChat": "g4f.Provider.needs_auth.GigaChat",
50 "GithubCopilot": "g4f.Provider.github.GithubCopilot",
51 "GithubCopilotAPI": "g4f.Provider.needs_auth.GithubCopilotAPI",
52 "GlhfChat": "g4f.Provider.needs_auth.GlhfChat",
53 "GoogleSearch": "g4f.Provider.search.GoogleSearch",
54 "GradientNetwork": "g4f.Provider.GradientNetwork",
55 "Grok": "g4f.Provider.needs_auth.Grok",
56 "Groq": "g4f.Provider.needs_auth.Groq",
57 "HailuoAI": "g4f.Provider.needs_auth.mini_max.HailuoAI",
58 "HuggingChat": "g4f.Provider.needs_auth.hf.HuggingChat",
59 "HuggingFace": "g4f.Provider.needs_auth.hf",
60 "HuggingFaceAPI": "g4f.Provider.needs_auth.hf.HuggingFaceAPI",
61 "HuggingFaceInference": "g4f.Provider.needs_auth.hf.HuggingFaceInference",
62 "HuggingFaceMedia": "g4f.Provider.needs_auth.hf.HuggingFaceMedia",
63 "HuggingSpace": "g4f.Provider.hf_space",
64 "IterListProvider": "g4f.providers.retry_provider",
65 "LMArena": "g4f.Provider.needs_auth.LMArena",
66 "Local": "g4f.Provider.local.Local",
67 "MarkItDown": "g4f.Provider.audio.MarkItDown",
68 "MetaAI": "g4f.Provider.needs_auth.MetaAI",
69 "MetaAIAccount": "g4f.Provider.needs_auth.MetaAIAccount",
70 "MicrosoftDesigner": "g4f.Provider.needs_auth.MicrosoftDesigner",
71 "MiniMax": "g4f.Provider.needs_auth.mini_max.MiniMax",
72 "Nvidia": "g4f.Provider.needs_auth.Nvidia",
73 "Ollama": "g4f.Provider.local.Ollama",
74 "OllamaSwarm": "g4f.Provider.OllamaSwarm",
75 "OpenAIFM": "g4f.Provider.audio.OpenAIFM",
76 "OpenRouter": "g4f.Provider.needs_auth.OpenRouter",
77 "OpenRouterFree": "g4f.Provider.needs_auth.OpenRouter",
78 "OpenaiAPI": "g4f.Provider.needs_auth.OpenaiAPI",
79 "OpenaiAccount": "g4f.Provider.needs_auth.OpenaiAccount",
80 "OpenaiChat": "g4f.Provider.needs_auth.OpenaiChat",
81 "OpenaiTemplate": "g4f.Provider.template.OpenaiTemplate",
82 "OperaAria": "g4f.Provider.OperaAria",
83 "Perplexity": "g4f.Provider.Perplexity",
84 "PerplexityApi": "g4f.Provider.needs_auth.PerplexityApi",
85 "PhindAi": "g4f.Provider.PhindAi",
86 "Pi": "g4f.Provider.needs_auth.Pi",
87 "PollinationsAI": "g4f.Provider.PollinationsAI",
88 "PollinationsAudio": "g4f.Provider.audio.PollinationsAudio",
89 "PollinationsImage": "g4f.Provider.PollinationsImage",
90 "PuterJS": "g4f.Provider.needs_auth.PuterJS",
91 "Qwen": "g4f.Provider.Qwen",
92 "QwenCode": "g4f.Provider.qwen.QwenCode",
93 "Reka": "g4f.Provider.needs_auth.Reka",
94 "Replicate": "g4f.Provider.needs_auth.Replicate",
95 "RetryProvider": "g4f.providers.retry_provider",
96 "RotatedProvider": "g4f.providers.retry_provider",
97 "SearXNG": "g4f.Provider.search.SearXNG",
98 "StabilityAI_SD35Large": "g4f.Provider.hf_space.StabilityAI_SD35Large",
99 "TeachAnything": "g4f.Provider.TeachAnything",
100 "ThebApi": "g4f.Provider.needs_auth.ThebApi",
101 "Together": "g4f.Provider.needs_auth.Together",
102 "Video": "g4f.Provider.needs_auth.Video",
103 "WeWordle": "g4f.Provider.WeWordle",
104 "WhiteRabbitNeo": "g4f.Provider.needs_auth.WhiteRabbitNeo",
105 "You": "g4f.Provider.needs_auth.You",
106 "YouTube": "g4f.Provider.search.YouTube",
107 "Yqcloud": "g4f.Provider.Yqcloud",
108 "Yupp": "g4f.Provider.Yupp",
109 "gTTS": "g4f.Provider.audio.gTTS",
110 "xAI": "g4f.Provider.needs_auth.xAI",
111 "AnyProvider": "g4f.providers.any_provider",
112 }
27 113
28 from .audio import *
29 from .template import OpenaiTemplate, BackendApi
30 from .qwen.QwenCode import QwenCode
31 114
32 from .Cloudflare import Cloudflare
33 from .Copilot import Copilot
34 from .CopilotApp import CopilotApp
35 from .CopilotSession import CopilotSession
36 from .DeepInfra import DeepInfra
37 from .EasyChat import EasyChat
38 from .GLM import GLM
39 from .GradientNetwork import GradientNetwork
115 __all__ = [
116 "BaseProvider",
117 "ProviderType",
118 "RetryProvider",
119 "IterListProvider",
120 "RotatedProvider",
121 "AsyncProvider",
122 "AsyncGeneratorProvider",
123 "CreateImagesProvider",
124 "ProviderUtils",
125 "__providers__",
126 "__map__",
127 ] + list(__map_paths__.keys())
40 128
41 from .PhindAi import PhindAi
42 from .Felo import Felo
43 from .OllamaSwarm import OllamaSwarm
44 from .OperaAria import OperaAria
45 from .Perplexity import Perplexity
46 from .PollinationsAI import PollinationsAI
47 from .PollinationsImage import PollinationsImage
48 from .Qwen import Qwen
49 from .TeachAnything import TeachAnything
50 from .WeWordle import WeWordle
51 from .Yqcloud import Yqcloud
52 from .Yupp import Yupp
129 _loaded_providers = {}
53 130
54 import sys
131 def __getattr__(name: str):
132 if name in __map_paths__:
133 module_path = __map_paths__[name]
134 if not isinstance(module_path, str):
135 return module_path
136 if name not in _loaded_providers:
137 try:
138 module = importlib.import_module(module_path)
139 _loaded_providers[name] = getattr(module, name)
140 except ImportError as e:
141 debug.error(f"Failed to load provider {name}: {e}")
142 raise AttributeError(f"Provider {name} could not be loaded") from e
143 return _loaded_providers[name]
144 if name == "__providers__":
145 # Load all providers if specifically requested
146 providers_list = []
147 for provider_name in __map_paths__.keys():
148 try:
149 providers_list.append(__getattr__(provider_name))
150 except AttributeError:
151 pass
152 return providers_list
55 153
56 __modules__: list = [
57 getattr(sys.modules[__name__], provider) for provider in dir()
58 if not provider.startswith("__")
59 ]
60 __providers__: list[ProviderType] = [
61 provider for provider in __modules__
62 if isinstance(provider, type)
63 and issubclass(provider, BaseProvider)
64 ]
65 __all__: list[str] = [
66 provider.__name__ for provider in __providers__
67 ]
68 __map__: dict[str, ProviderType] = {
69 provider.__name__: provider for provider in __providers__
70 }
154 raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
155
156 def __dir__():
157 return __all__
158
159 class _ConvertDict(dict):
160 def __contains__(self, item):
161 return item in __map_paths__
162 def __getitem__(self, item):
163 if item in __map_paths__:
164 if not isinstance(__map_paths__[item], str):
165 return __map_paths__[item]
166 return __getattr__(item)
167 raise KeyError(item)
168 def values(self):
169 return __getattr__("__providers__")
170 def keys(self):
171 return __map_paths__.keys()
172 def items(self):
173 return [(k, self[k]) for k in __map_paths__.keys()]
174 def get(self, item, default=None):
175 try:
176 return self[item]
177 except KeyError:
178 return default
179
180 __map__ = _ConvertDict()
71 181
72 182 class ProviderUtils:
73 convert: dict[str, ProviderType] = __map__
183 convert = __map__
74 184
75 185 @classmethod
76 186 def get_by_label(cls, label: str) -> ProviderType:
77 187 if not label:
78 188 raise ValueError("Label must be provided")
79 provider = cls.convert.get(label)
80 if provider is None:
81 for provider_cls in cls.convert.values():
82 if provider_cls.working and provider_cls.__name__.lower().startswith(label.lower()):
83 provider = provider_cls
84 break
85 if provider is None:
86 raise ValueError(f"Provider with label '{label}' not found")
87 return provider
189
190 import g4f.Provider
191
192 # Check explicit map
193 if label in __map__:
194 return getattr(g4f.Provider, label)
195
196 # Fallback to search
197 for provider_name in __map_paths__.keys():
198 if provider_name.lower().startswith(label.lower()):
199 provider = getattr(g4f.Provider, provider_name)
200 if provider.working:
201 return provider
202
203 raise ValueError(f"Provider with label '{label}' not found")
Modified g4f/Provider/hf_space/__init__.py +2 -2
@@ -57,8 +57,8 @@ class HuggingSpace(AsyncGeneratorProvider, ProviderModelMixin):
57 57 async def create_async_generator(
58 58 cls, model: str, messages: Messages, media: MediaListType = None, **kwargs
59 59 ) -> AsyncResult:
60 if not model and media is not None:
61 model = cls.default_vision_model
60 if not model:
61 model = cls.default_vision_model if media is not None else cls.default_model
62 62 is_started = False
63 63 random.shuffle(cls.providers)
64 64 for provider in cls.providers:
Modified g4f/Provider/needs_auth/Antigravity.py +8 -1
@@ -704,8 +704,11 @@ class AntigravityAuthManager(AuthFileMixin):
704 704 }
705 705 async with session.post(url, headers=onboard_headers, json=onboard_request_body, timeout=timeout) as resp:
706 706 if not resp.ok:
707 text = await resp.text()
708 if resp.status == 403:
709 raise MissingAuthError("Account not eligible for Antigravity Code Assist.")
707 710 print(f"Onboarding attempt {attempt+1} at {base_url} failed with status {resp.status}")
708 print(await resp.text())
711 print(text)
709 712 # Stop attempts on this endpoint and try next base_url
710 713 break
711 714
@@ -723,6 +726,8 @@ class AntigravityAuthManager(AuthFileMixin):
723 726 return managed_id
724 727 if done and configured_project:
725 728 return configured_project
729 except MissingAuthError:
730 raise
726 731 except Exception as e:
727 732 debug.log(f"Failed to onboard managed project at {base_url}: {e}")
728 733 break
@@ -950,6 +955,8 @@ class AntigravityProvider:
950 955 raise RuntimeError(
951 956 "Project ID discovery failed - set ANTIGRAVITY_PROJECT_ID in environment."
952 957 )
958 except MissingAuthError:
959 raise
953 960 except Exception as e:
954 961 debug.error(f"Failed to discover project ID: {e}")
955 962 raise RuntimeError(
Modified g4f/Provider/needs_auth/Custom.py +13 -0
@@ -10,6 +10,19 @@ class Custom(OpenaiTemplate):
10 10 base_url = "http://localhost:8080/v1"
11 11 sort_models = False
12 12
13 @classmethod
14 def get_models(cls, api_key: str = None, base_url: str = None, **kwargs) -> list[str]:
15 if cls.models:
16 return cls.models
17 try:
18 return super().get_models(api_key=api_key, base_url=base_url, **kwargs)
19 except Exception as e:
20 # If no explicit base_url is provided and it fails (e.g. hits the Flask GUI itself -> 404),
21 # silently ignore to prevent flooding logs. If explicit base_url provided, raise the error.
22 if base_url is None:
23 return []
24 raise e
25
13 26 class Feature(Custom):
14 27 label = "Feature Provider"
15 28 working = False
Modified g4f/Provider/needs_auth/GeminiCLI.py +6 -0
@@ -501,6 +501,8 @@ class GeminiCLIProvider():
501 501 self._project_id = project
502 502 return project
503 503 raise RuntimeError("No project information found in API response.")
504 except MissingAuthError:
505 raise
504 506 except Exception as e:
505 507 debug.error(f"Failed to discover project ID: {e}")
506 508 raise RuntimeError(
@@ -555,8 +557,12 @@ class GeminiCLIProvider():
555 557 return project_id
556 558 else:
557 559 text = await response.text()
560 if response.status == 403:
561 raise MissingAuthError("Account not eligible for Gemini Code Assist.")
558 562 debug.error(f"Onboarding attempt {attempt + 1} failed with status {response.status}: {text}")
559 563 response.raise_for_status()
564 except MissingAuthError:
565 raise
560 566 except Exception as e:
561 567 debug.error(f"Failed to onboard managed project: {e}")
562 568
Modified g4f/cli/__init__.py +11 -17
@@ -17,16 +17,7 @@ import os
17 17 import sys
18 18 from argparse import ArgumentParser
19 19
20 from .client import get_parser, run_client_args
21 from ..requests import BrowserConfig
22 from ..gui.run import gui_parser, run_gui_args
23 20 from ..config import DEFAULT_PORT, DEFAULT_TIMEOUT, DEFAULT_STREAM_TIMEOUT
24 from ..Provider.needs_auth.Antigravity import cli_main as antigravity_cli_main
25 from ..Provider.qwen.QwenCode import cli_main as qwen_cli_main
26 from ..Provider.github.GithubCopilot import cli_main as github_cli_main
27 from ..Provider.needs_auth.GeminiCLI import cli_main as gemini_cli_main
28 from .. import Provider
29 from .. import cookies
30 21
31 22
32 23 # --------------------------------------------------------------
@@ -79,18 +70,14 @@ def get_api_parser(exit_on_error: bool = True) -> ArgumentParser:
79 70 # Providers for chat completion
80 71 api_parser.add_argument(
81 72 "--provider",
82 choices=[p.__name__ for p in Provider.__providers__ if p.working],
73 type=str,
83 74 default=None,
84 75 help="Default provider for chat completion."
85 76 )
86 77
87 # Providers for image generation
88 78 api_parser.add_argument(
89 79 "--media-provider",
90 choices=[
91 p.__name__ for p in Provider.__providers__
92 if p.working and bool(getattr(p, "image_models", False))
93 ],
80 type=str,
94 81 default=None,
95 82 help="Default provider for image generation."
96 83 )
@@ -137,7 +124,6 @@ def get_api_parser(exit_on_error: bool = True) -> ArgumentParser:
137 124 api_parser.add_argument(
138 125 "--ignored-providers",
139 126 nargs="+",
140 choices=[p.__name__ for p in Provider.__providers__ if p.working],
141 127 default=[],
142 128 help="Providers to ignore during request processing."
143 129 )
@@ -145,7 +131,6 @@ def get_api_parser(exit_on_error: bool = True) -> ArgumentParser:
145 131 api_parser.add_argument(
146 132 "--cookie-browsers",
147 133 nargs="+",
148 choices=[browser.__name__ for browser in cookies.BROWSERS],
149 134 default=[],
150 135 help="Browsers to fetch cookies from."
151 136 )
@@ -197,6 +182,8 @@ def run_api_args(args):
197 182 Runs the API server using the parsed CLI arguments.
198 183 """
199 184 from g4f.api import AppConfig, run_api
185 from ..requests import BrowserConfig
186 from .. import cookies
200 187
201 188 # Apply configuration
202 189 AppConfig.set_config(
@@ -332,10 +319,12 @@ def main():
332 319 args = parser.parse_args(remaining)
333 320 run_api_args(args)
334 321 elif args.mode == "gui":
322 from ..gui.run import gui_parser, run_gui_args
335 323 parser = gui_parser()
336 324 args = parser.parse_args(remaining)
337 325 run_gui_args(args)
338 326 elif args.mode == "client":
327 from .client import get_parser, run_client_args
339 328 parser = get_parser()
340 329 args = parser.parse_args(remaining)
341 330 run_client_args(args)
@@ -352,6 +341,7 @@ def main():
352 341
353 342 except argparse.ArgumentError:
354 343 # Try client mode
344 from .client import get_parser, run_client_args
355 345 run_client_args(
356 346 get_parser(exit_on_error=False).parse_args(),
357 347 exit_on_error=False
@@ -402,12 +392,16 @@ complete -F _g4f_completions g4f
402 392
403 393 def handle_auth(provider, action, remaining):
404 394 if provider == "gemini-cli":
395 from ..Provider.needs_auth.GeminiCLI import cli_main as gemini_cli_main
405 396 sys.exit(gemini_cli_main([action] + remaining))
406 397 elif provider == "antigravity":
398 from ..Provider.needs_auth.Antigravity import cli_main as antigravity_cli_main
407 399 sys.exit(antigravity_cli_main([action] + remaining))
408 400 elif provider == "qwencode":
401 from ..Provider.qwen.QwenCode import cli_main as qwen_cli_main
409 402 sys.exit(qwen_cli_main([action] + remaining))
410 403 elif provider == "github-copilot":
404 from ..Provider.github.GithubCopilot import cli_main as github_cli_main
411 405 sys.exit(github_cli_main([action] + remaining))
412 406 else:
413 407 print(f"Provider {provider} not supported yet.")
Modified g4f/cli/client.py +80 -51
@@ -6,24 +6,20 @@ import asyncio
6 6 import json
7 7 import argparse
8 8 import traceback
9 import requests
10 9
10
11 from pathlib import Path
12 from typing import Optional, List, Dict
11 13 from pathlib import Path
12 14 from typing import Optional, List, Dict
13 from g4f.client import AsyncClient, ClientFactory
14 from g4f.providers.response import JsonConversation, MediaResponse, is_content
15 from g4f.cookies import set_cookies_dir, read_cookie_files
16 from g4f.Provider import ProviderUtils
17 from g4f.image import extract_data_uri, is_accepted_format
18 from g4f.image.copy_images import get_media_dir
19 from g4f.client.helper import filter_markdown
20 from g4f.errors import MissingRequirementsError
21 15
22 16 try:
23 from g4f.integration.markitdown import MarkItDown
24 has_markitdown = True
17 import aiohttp
25 18 except ImportError:
26 has_markitdown = False
19 aiohttp = None
20
21 from g4f.config import CONFIG_DIR, COOKIES_DIR
22 from g4f import debug
27 23
28 24 from g4f.config import CONFIG_DIR, COOKIES_DIR
29 25 from g4f import debug
@@ -44,7 +40,7 @@ class ConversationManager:
44 40 self.model = model
45 41 self.provider = provider
46 42 self.max_messages = max_messages
47 self.conversation: Optional[JsonConversation] = None
43 self.conversation: Optional['JsonConversation'] = None
48 44 self.history: List[Dict[str, str]] = []
49 45 self.data: Dict = {}
50 46 self._load()
@@ -60,6 +56,7 @@ class ConversationManager:
60 56 if self.provider is None:
61 57 self.provider = data.get("provider")
62 58 self.data = data.get("data", {})
59 from g4f.providers.response import JsonConversation
63 60 if self.provider and self.data.get(self.provider):
64 61 self.conversation = JsonConversation(**self.data[self.provider])
65 62 elif not self.provider and self.data:
@@ -98,7 +95,7 @@ class ConversationManager:
98 95 return result
99 96
100 97 async def stream_response(
101 client: AsyncClient,
98 client: 'AsyncClient',
102 99 input_text,
103 100 conversation: ConversationManager,
104 101 output_file: Optional[Path] = None,
@@ -121,6 +118,7 @@ async def stream_response(
121 118 "conversation": conversation.conversation,
122 119 }
123 120
121 from g4f.providers.response import MediaResponse, is_content
124 122 response_tokens = []
125 123 last_chunk = None
126 124 async for chunk in client.chat.completions.create(**create_args):
@@ -148,7 +146,7 @@ async def stream_response(
148 146 text_response = "".join(str(t) for t in response_tokens)
149 147
150 148 if output_file:
151 if save_content(text_response, media_chunk, str(output_file)):
149 if await save_content(text_response, media_chunk, str(output_file)):
152 150 print(f"\n→ Response saved to '{output_file}'")
153 151
154 152 if text_response:
@@ -158,16 +156,19 @@ async def stream_response(
158 156 raise RuntimeError("No response received")
159 157
160 158
161 def save_content(content, media: Optional[MediaResponse], filepath: str, allowed_types=None) -> bool:
159 async def save_content(content, media: Optional['MediaResponse'], filepath: str, allowed_types=None) -> bool:
162 160 if media:
163 161 for url in media.get_list():
164 162 if url.startswith(("http://", "https://")):
165 163 try:
166 resp = requests.get(url, cookies=media.get("cookies"), headers=media.get("headers"))
167 if resp.status_code == 200:
168 with open(filepath, "wb") as f:
169 f.write(resp.content)
170 return True
164 if aiohttp is None:
165 import aiohttp
166 async with aiohttp.ClientSession(cookies=media.get("cookies"), headers=media.get("headers")) as session:
167 async with session.get(url) as resp:
168 if resp.status == 200:
169 with open(filepath, "wb") as f:
170 f.write(await resp.read())
171 return True
171 172 except Exception as e:
172 173 print(f"Error fetching media '{url}': {e}", file=sys.stderr)
173 174 return False
@@ -180,13 +181,16 @@ def save_content(content, media: Optional[MediaResponse], filepath: str, allowed
180 181 print("\nNo content to save.", file=sys.stderr)
181 182 return False
182 183 if content.startswith("data:"):
184 from g4f.image import extract_data_uri
183 185 with open(filepath, "wb") as f:
184 186 f.write(extract_data_uri(content))
185 187 return True
186 188 if content.startswith("/media/"):
189 from g4f.image.copy_images import get_media_dir
187 190 src = content.replace("/media", get_media_dir()).split("?")[0]
188 191 os.rename(src, filepath)
189 192 return True
193 from g4f.client.helper import filter_markdown
190 194 filtered = filter_markdown(content, allowed_types)
191 195 if filtered:
192 196 with open(filepath, "w", encoding="utf-8") as f:
@@ -203,7 +207,7 @@ def get_parser(exit_on_error=True):
203 207 )
204 208 parser.add_argument('-d', '--debug', action='store_true', help="Verbose debug")
205 209 parser.add_argument('-p', '--provider', default=None,
206 help=f"Provider to use: {', '.join(k for k,v in ProviderUtils.convert.items() if v.working)}")
210 help="Provider to use")
207 211 parser.add_argument('-m', '--model', help="Model name")
208 212 parser.add_argument('-O', '--output', type=Path,
209 213 help="Save assistant output to FILE (text or media)")
@@ -250,9 +254,11 @@ async def run_args(input_val, args):
250 254 conv.history = []
251 255 conv.conversation = None
252 256
257 from g4f.cookies import set_cookies_dir, read_cookie_files
253 258 set_cookies_dir(str(args.cookies_dir))
254 259 read_cookie_files()
255 260
261 from g4f.client import ClientFactory
256 262 client = ClientFactory.create_async_client(provider=conv.provider)
257 263
258 264 if input_val == "models":
@@ -283,40 +289,58 @@ async def run_args(input_val, args):
283 289 sys.exit(1)
284 290
285 291
286 def run_client_args(args, exit_on_error=True):
292 async def async_run_client_args(args, exit_on_error=True):
287 293 input_txt = ""
288 294 media = []
289 295 rest = 0
290 296
291 for idx, tok in enumerate(args.input):
292 if tok.startswith(("http://","https://")):
293 # same URL logic...
294 resp = requests.head(tok, allow_redirects=True)
295 if resp.ok and resp.headers.get("Content-Type","").startswith("image"):
296 media.append(tok)
297 else:
298 if not has_markitdown:
299 raise MissingRequirementsError("Install markitdown")
300 md = MarkItDown()
301 txt = md.convert_url(tok).text_content
302 input_txt += f"\n```source: {tok}\n{txt}\n```\n"
303 elif os.path.isfile(tok):
304 head = Path(tok).read_bytes()[:12]
305 try:
306 if is_accepted_format(head):
307 media.append(Path(tok))
308 is_img = True
297 if aiohttp is None:
298 import aiohttp
299
300 async with aiohttp.ClientSession() as session:
301 for idx, tok in enumerate(args.input):
302 if tok.startswith(("http://","https://")):
303 try:
304 async with session.head(tok, allow_redirects=True) as resp:
305 is_ok = resp.status == 200
306 content_type = resp.headers.get("Content-Type", "")
307 except Exception:
308 is_ok = False
309 content_type = ""
310
311 if is_ok and content_type.startswith("image"):
312 media.append(tok)
309 313 else:
314 try:
315 from g4f.integration.markitdown import MarkItDown
316 except ImportError:
317 from g4f.errors import MissingRequirementsError
318 raise MissingRequirementsError("Install markitdown")
319
320 def run_markitdown(url):
321 md = MarkItDown()
322 return md.convert_url(url).text_content
323
324 txt = await asyncio.to_thread(run_markitdown, tok)
325 input_txt += f"\n```source: {tok}\n{txt}\n```\n"
326 elif os.path.isfile(tok):
327 from g4f.image import is_accepted_format
328 head = Path(tok).read_bytes()[:12]
329 try:
330 if is_accepted_format(head):
331 media.append(Path(tok))
332 is_img = True
333 else:
334 is_img = False
335 except ValueError:
310 336 is_img = False
311 except ValueError:
312 is_img = False
313 if not is_img:
314 txt = Path(tok).read_text(encoding="utf-8")
315 input_txt += f"\n```file: {tok}\n{txt}\n```\n"
316 else:
317 rest = idx
318 break
319 rest = idx + 1
337 if not is_img:
338 txt = Path(tok).read_text(encoding="utf-8")
339 input_txt += f"\n```file: {tok}\n{txt}\n```\n"
340 else:
341 rest = idx
342 break
343 rest = idx + 1
320 344
321 345 tail = args.input[rest:]
322 346 if tail:
@@ -334,9 +358,14 @@ def run_client_args(args, exit_on_error=True):
334 358 print("No input provided. Use -h.", file=sys.stderr)
335 359 sys.exit(1)
336 360 elif not val:
361 import argparse
337 362 raise argparse.ArgumentError(None, "No input provided. Use -h for help.")
338 363
339 asyncio.run(run_args(val, args))
364 await run_args(val, args)
365
366
367 def run_client_args(args, exit_on_error=True):
368 asyncio.run(async_run_client_args(args, exit_on_error))
340 369
341 370
342 371 if __name__ == "__main__":
Modified g4f/client/service.py +1 -1
@@ -105,7 +105,7 @@ def get_model_and_provider(model : Union[Model, str],
105 105
106 106 if isinstance(provider, BaseRetryProvider):
107 107 if not ignore_working:
108 provider.providers = [p for p in provider.providers if p.working]
108 provider.providers = [p for p in getattr(provider, "get_providers", lambda x: provider.providers)([]) if getattr(p, "working", False)]
109 109
110 110 if not ignore_stream and not provider.supports_stream and stream:
111 111 raise StreamNotSupportedError(f'{provider_name} does not support "stream" argument')
Modified g4f/gui/server/api.py +16 -8
@@ -100,17 +100,25 @@ class Api:
100 100 } for provider in Provider.__providers__ if provider.working and safe_get_models(provider)]
101 101
102 102 def get_all_models(self) -> dict[str, list]:
103 def safe_get_provider_models(provider: ProviderModelMixin) -> list[str]:
103 import concurrent.futures
104
105 def safe_get_provider_models(provider) -> tuple[str, list[str]]:
104 106 try:
105 return list(provider.get_models(timeout=10))
107 return provider.__name__, list(provider.get_models(timeout=10))
106 108 except Exception as e:
107 109 debug.error(f"{provider.__name__}: get_models error:", e)
108 return []
109 return {
110 provider.__name__: safe_get_provider_models(provider)
111 for provider in Provider.__providers__
112 if provider.working and hasattr(provider, "get_models")
113 }
110 return provider.__name__, []
111
112 providers = [p for p in Provider.__providers__ if p.working and hasattr(p, "get_models")]
113 results = {}
114
115 with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
116 futures = {executor.submit(safe_get_provider_models, p): p for p in providers}
117 for future in concurrent.futures.as_completed(futures):
118 name, models = future.result()
119 results[name] = models
120
121 return results
114 122
115 123 @staticmethod
116 124 def get_version() -> dict:
Modified g4f/gui/server/app.py +23 -1
@@ -1,5 +1,27 @@
1 from flask import Flask
1 import gzip
2 from flask import Flask, request
2 3
3 4 def create_app() -> Flask:
4 5 app = Flask(__name__)
6
7 @app.after_request
8 def compress_response(response):
9 accept_encoding = request.headers.get('Accept-Encoding', '')
10 if 'gzip' not in accept_encoding.lower():
11 return response
12 if response.status_code < 200 or response.status_code >= 300:
13 return response
14 if 'Content-Encoding' in response.headers:
15 return response
16 content_type = response.headers.get('Content-Type', '')
17 if not content_type.startswith(('text/', 'application/javascript', 'application/json')):
18 return response
19
20 response.direct_passthrough = False
21 response.data = gzip.compress(response.data)
22 response.headers['Content-Encoding'] = 'gzip'
23 response.headers['Vary'] = 'Accept-Encoding'
24 response.headers['Content-Length'] = len(response.data)
25 return response
26
5 27 return app
Modified g4f/gui/server/backend_api.py +13 -0
Modified g4f/gui/server/website.py +14 -6
Modified g4f/models.py +147 -177
Modified g4f/providers/retry_provider.py +18 -4
Modified g4f/version.py +18 -10