返回提交历史
Modified
.gitignore
+2
-1
Modified
g4f/Provider/OllamaSwarm.py
+3
-0
Modified
g4f/Provider/__init__.py
+183
-67
Modified
g4f/Provider/hf_space/__init__.py
+2
-2
Modified
g4f/Provider/needs_auth/Antigravity.py
+8
-1
Modified
g4f/Provider/needs_auth/Custom.py
+13
-0
Modified
g4f/Provider/needs_auth/GeminiCLI.py
+6
-0
Modified
g4f/cli/__init__.py
+11
-17
Modified
g4f/cli/client.py
+80
-51
Modified
g4f/client/service.py
+1
-1
Modified
g4f/gui/server/api.py
+16
-8
Modified
g4f/gui/server/app.py
+23
-1
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
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
代码差异
17 个文件
+558
-346
@@ -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
@@ -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)
@@ -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")
@@ -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:
@@ -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(
@@ -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
@@ -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
@@ -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.")
@@ -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__":
@@ -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')
@@ -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:
@@ -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