返回提交历史
Modified
.gitignore
+2
-1
Modified
g4f/Provider/template/OpenaiTemplate.py
+0
-1
Modified
g4f/client/factory.py
+7
-7
Modified
g4f/mcp/pa_downloader.py
+4
-4
Modified
g4f/mcp/pa_provider.py
+179
-2
Modified
g4f/providers/any_provider.py
+7
-4
Modified
g4f/tools/run_tools.py
+14
-8
XFEstudio/gpt4free
Update pa provider support
3ee4635c
代码差异
7 个文件
+213
-27
@@ -14,4 +14,5 @@ container/
14
14
g4f.dev
15
15
g4f.exe
16
16
har_and_cookies
17
playground
17
playground
18
.vscode
@@ -189,7 +189,6 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
189
189
else:
190
190
raise Exception("Unexpected content type: " + content_type)
191
191
await raise_for_status(response)
192
await raise_for_status(response)
193
192
yield ImageResponse([f"data:image/png;base64,{image['b64_json']}" if image.get("url") is None else image["url"] for image in data["data"]], prompt)
194
193
return
195
194
@@ -4,7 +4,7 @@ import json
4
4
import requests
5
5
from pathlib import Path
6
6
from datetime import datetime
7
from typing import Union, List, Dict, Type
7
from typing import Union, List, Dict, Type, Optional
8
8
9
9
from ..providers.types import ProviderType, BaseProvider
10
10
from ..errors import ProviderNotFoundError
@@ -15,11 +15,11 @@ from ..config import AppConfig
15
15
16
16
def create_custom_provider(
17
17
base_url: str,
18
api_key: str = None,
19
name: str = None,
18
api_key: Optional[str] = None,
19
name: Optional[str] = None,
20
20
working: bool = True,
21
21
default_model: str = "",
22
models: List[str] = None,
22
models: Optional[List[str]] = None,
23
23
**kwargs
24
24
) -> ProviderType:
25
25
"""
@@ -68,10 +68,10 @@ class AbstractClientFactory:
68
68
@classmethod
69
69
def create_provider(
70
70
cls,
71
name: str,
71
name: Optional[str],
72
72
provider: Union[Type[BaseProvider], str],
73
base_url: str = None,
74
api_key: str = None,
73
base_url: Optional[str] = None,
74
api_key: Optional[str] = None,
75
75
**kwargs
76
76
) -> Type[BaseProvider]:
77
77
"""
@@ -104,7 +104,7 @@ def _list_repo_files(repo: str, ref: str, timeout: float) -> List[str]:
104
104
if not isinstance(entry, dict):
105
105
continue
106
106
name = entry.get("name", "")
107
if name.endswith(".pa.py") and entry.get("type") == "file":
107
if name.endswith(".py") and entry.get("type") == "file":
108
108
files.append(name)
109
109
return files
110
110
@@ -162,7 +162,7 @@ def run_pa_download(
162
162
return written
163
163
164
164
for name in names:
165
if not name.endswith(".pa.py"):
165
if not name.endswith(".py"):
166
166
continue
167
167
dest = target / name
168
168
if dest.exists() and not force:
@@ -221,8 +221,8 @@ def run_pa_remove(filename: str, directory: Optional[str] = None) -> bool:
221
221
except ValueError:
222
222
print(f"Error: {filename} escapes the workspace directory.")
223
223
return False
224
if not candidate.name.endswith(".pa.py"):
225
print(f"Error: {filename} is not a .pa.py file.")
224
if not candidate.name.endswith(".py"):
225
print(f"Error: {filename} is not a .py file.")
226
226
return False
227
227
if not candidate.exists():
228
228
print(f"Error: {filename} not found in {target}")
@@ -57,12 +57,14 @@ Typical layout of a ``.pa.py`` file::
57
57
from __future__ import annotations
58
58
59
59
import io
60
import os as _os
60
61
import sys
61
62
import json
62
63
import hashlib
63
64
import threading
64
65
import time as _time_module
65
66
import traceback
67
import types
66
68
import builtins as _builtins
67
69
from pathlib import Path
68
70
from typing import Any, Dict, FrozenSet, List, Optional, Type
@@ -87,7 +89,7 @@ def is_hidden_file(path: str) -> bool:
87
89
88
90
#: Modules that are allowed inside the safe execution sandbox.
89
91
SAFE_MODULES: FrozenSet[str] = frozenset({
90
"__future__", "concurrent", "warnings", "urllib3", "urllib3.exceptions", "uuid",
92
"__future__", "concurrent", "warnings", "urllib3", "urllib3.exceptions", "uuid", "secrets",
91
93
# Math / numeric
92
94
"math", "cmath", "decimal", "fractions", "statistics", "random", "numbers",
93
95
# String / text
@@ -116,6 +118,10 @@ SAFE_MODULES: FrozenSet[str] = frozenset({
116
118
"aiohttp", "requests",
117
119
# gpt4free itself
118
120
"g4f",
121
# wasmtime
122
"wasmtime",
123
# Restricted os shim (only urandom and safe read-only attrs exposed)
124
"os",
119
125
})
120
126
121
127
@@ -203,6 +209,141 @@ def _exec_in_thread(
203
209
sys.setrecursionlimit(prev)
204
210
205
211
212
def _load_workspace_module(
213
name: str,
214
workspace: Path,
215
globals_dict: Optional[Dict[str, Any]],
216
fromlist: tuple = (),
217
level: int = 0,
218
) -> Optional[types.ModuleType]:
219
"""Try to load *name* as a ``.py`` file from the workspace.
220
221
Searches recursively for ``<name>.py`` (or ``<name>/__init__.py`` for
222
packages) anywhere under *workspace*. If found, executes it inside the
223
sandbox and returns the resulting module object. Returns ``None`` if no
224
matching file exists.
225
226
The directory of ``__file__`` in *globals_dict* (if set) is searched
227
first so sibling modules are found quickly, then the entire workspace is
228
searched recursively as a fallback.
229
230
The loaded module is cached in :data:`sys.modules` so subsequent imports
231
return the same object.
232
233
Args:
234
name: Top-level module name (e.g. ``"freegpt_wasm_signer"``).
235
workspace: Workspace root directory to search.
236
globals_dict: Globals dict of the calling frame (used to find
237
``__file__`` for sibling-first lookup).
238
fromlist: ``fromlist`` argument from the import statement.
239
level: Relative import level (always 0 for absolute imports).
240
"""
241
# Only handle simple top-level names (no dots).
242
if "." in name:
243
return None
244
245
# Build search directories: __file__ dir first, then workspace root
246
search_dirs: List[Path] = []
247
if globals_dict:
248
cur_file = globals_dict.get("__file__")
249
if cur_file:
250
search_dirs.append(Path(cur_file).parent)
251
search_dirs.append(workspace)
252
253
source_path: Optional[Path] = None
254
for d in search_dirs:
255
py_file = d / f"{name}.py"
256
pkg_init = d / name / "__init__.py"
257
if py_file.is_file():
258
source_path = py_file
259
break
260
elif pkg_init.is_file():
261
source_path = pkg_init
262
break
263
else:
264
# Recursive fallback: search entire workspace
265
for candidate in workspace.rglob(f"{name}.py"):
266
# Skip .pa.py files — those are providers, not importable modules
267
if not candidate.name.endswith(".pa.py"):
268
source_path = candidate
269
break
270
if source_path is None:
271
for candidate in workspace.rglob(f"{name}/__init__.py"):
272
source_path = candidate
273
break
274
275
if source_path is None:
276
return None
277
278
# Return cached module if already loaded
279
if name in sys.modules:
280
return sys.modules[name]
281
282
# Read and execute the module source in a sandbox
283
code = source_path.read_text(encoding="utf-8")
284
module = types.ModuleType(name)
285
module.__file__ = str(source_path.resolve())
286
module.__name__ = name
287
if pkg_init.is_file():
288
module.__path__ = [str((workspace / name).resolve())]
289
module.__package__ = name
290
else:
291
module.__package__ = ""
292
293
# Build sandbox globals for the module
294
module_globals = _make_safe_globals(SAFE_MODULES)
295
module_globals["__file__"] = str(source_path.resolve())
296
module_globals["__name__"] = name
297
module_globals["__package__"] = module.__package__
298
module.__dict__.update(module_globals)
299
300
try:
301
compiled = compile(code, str(source_path), "exec")
302
except SyntaxError:
303
raise ImportError(
304
f"Syntax error in workspace module '{name}' "
305
f"({source_path}):\n{traceback.format_exc()}"
306
)
307
308
# Execute in the current thread (no timeout — module loading is expected
309
# to be fast and we need the module object synchronously).
310
prev_depth = sys.getrecursionlimit()
311
sys.setrecursionlimit(MAX_RECURSION_DEPTH)
312
try:
313
exec(compiled, module.__dict__, module.__dict__) # noqa: S102
314
except Exception:
315
raise ImportError(
316
f"Failed to load workspace module '{name}' "
317
f"({source_path}):\n{traceback.format_exc()}"
318
)
319
finally:
320
sys.setrecursionlimit(prev_depth)
321
322
sys.modules[name] = module
323
return module
324
325
326
# ---------------------------------------------------------------------------
327
# Restricted os shim
328
# ---------------------------------------------------------------------------
329
330
def _make_restricted_os() -> types.ModuleType:
331
"""Return a restricted ``os`` module that only exposes safe, read-only
332
attributes (``urandom``, ``name``, ``sep``, ``linesep``, ``altsep``,
333
``pathsep``). All filesystem, process, and environment operations are
334
absent.
335
"""
336
_SAFE_OS_ATTRS = frozenset({
337
"urandom", "name", "sep", "linesep", "altsep", "pathsep",
338
})
339
shim = types.ModuleType("os")
340
for attr in _SAFE_OS_ATTRS:
341
if hasattr(_os, attr):
342
setattr(shim, attr, getattr(_os, attr))
343
shim.__name__ = "os"
344
return shim
345
346
206
347
def _make_restricted_import(allowed: FrozenSet[str]):
207
348
"""Return a ``__import__`` replacement that only allows *allowed* modules."""
208
349
original = _builtins.__import__
@@ -241,7 +382,37 @@ def _make_restricted_import(allowed: FrozenSet[str]):
241
382
"Relative imports are not allowed inside a .pa.py sandbox."
242
383
)
243
384
base = name.split(".")[0]
385
# Return the restricted os shim instead of the real os module.
386
if base == "os":
387
_os_shim = _make_restricted_os()
388
if name == "os":
389
return _os_shim
390
# Handle "os.submodule" — try to resolve from the shim
391
obj = _os_shim
392
for part in name.split(".")[1:]:
393
obj = getattr(obj, part, None)
394
if obj is None:
395
raise ImportError(
396
f"'{name}' is not available in the restricted os shim."
397
)
398
return obj
244
399
if base not in allowed:
400
# Before rejecting, check if it's a workspace module (sibling .py file).
401
workspace = get_workspace_dir()
402
ws_module = _load_workspace_module(base, workspace, globals, fromlist, level)
403
if ws_module is not None:
404
# Handle submodule imports (e.g. "pkg.sub")
405
if name != base:
406
# Try to resolve the full dotted path from the loaded module
407
obj = ws_module
408
for part in name.split(".")[1:]:
409
obj = getattr(obj, part, None)
410
if obj is None:
411
raise ImportError(
412
f"Cannot find submodule '{name}' in workspace module '{base}'."
413
)
414
return obj
415
return ws_module
245
416
raise ImportError(
246
417
f"Import of '{name}' is not allowed in safe execution mode.\n"
247
418
f"Allowed top-level modules: {', '.join(sorted(allowed))}"
@@ -377,6 +548,7 @@ def execute_safe_code(
377
548
allowed_modules: FrozenSet[str] = SAFE_MODULES,
378
549
timeout: Optional[float] = MAX_EXEC_TIMEOUT,
379
550
max_depth: int = MAX_RECURSION_DEPTH,
551
file_path: "Optional[str | Path]" = None,
380
552
) -> SafeExecutionResult:
381
553
"""Execute *code* inside a safe sandbox with whitelisted module imports.
382
554
@@ -393,6 +565,9 @@ def execute_safe_code(
393
565
``None`` to disable. Defaults to :data:`MAX_EXEC_TIMEOUT`.
394
566
max_depth: Maximum recursion depth inside the sandbox. Defaults to
395
567
:data:`MAX_RECURSION_DEPTH`.
568
file_path: When provided, sets ``__file__`` in the sandbox globals so
569
the executed code can reference its own location (e.g. to load
570
sibling files relative to the ``.pa.py`` file).
396
571
397
572
Returns:
398
573
:class:`SafeExecutionResult` containing captured stdout/stderr, any
@@ -402,6 +577,8 @@ def execute_safe_code(
402
577
stderr_buf = _LimitedStringIO(MAX_OUTPUT_BYTES)
403
578
404
579
safe_globals = _make_safe_globals(allowed_modules, stdout_buf=stdout_buf, stderr_buf=stderr_buf)
580
if file_path is not None:
581
safe_globals["__file__"] = str(Path(file_path).resolve())
405
582
if extra_globals:
406
583
safe_globals.update(extra_globals)
407
584
@@ -499,7 +676,7 @@ def load_pa_provider(file_path: "str | Path") -> Optional[Type]:
499
676
raise ValueError(f"File must have .pa.py extension: {file_path}")
500
677
501
678
code = file_path.read_text(encoding="utf-8")
502
result = execute_safe_code(code)
679
result = execute_safe_code(code, file_path=file_path)
503
680
504
681
if not result.success:
505
682
raise RuntimeError(
@@ -487,12 +487,15 @@ class AnyProvider(AsyncGeneratorProvider, AnyModelProviderMixin):
487
487
if not has_api_key:
488
488
providers.sort(key=lambda p: bool(getattr(p, "needs_auth", False)))
489
489
490
providers.append(AbstractClientFactory.create_provider(None, "default"))
491
490
492
491
if len(providers) == 0:
493
raise ModelNotFoundError(
494
f"AnyProvider: Model {model} not found in any provider."
495
)
492
provider: AsyncGeneratorProvider = AbstractClientFactory.create_provider(None, "default")
493
async for chunk in provider.create_async_generator(model, messages, stream=stream, media=media, api_key=api_key, **kwargs):
494
yield chunk
495
return
496
# raise ModelNotFoundError(
497
# f"AnyProvider: Model {model} not found in any provider."
498
# )
496
499
497
500
debug.log(
498
501
f"AnyProvider: Using providers: {[provider.__name__ for provider in providers]} for model '{model}'"
@@ -411,19 +411,25 @@ async def async_iter_run_tools(
411
411
}
412
412
if saved_tokens:
413
413
usage_dict["saved_tokens"] = saved_tokens
414
old_tokens = usage_dict.get("prompt_tokens", 0) + saved_tokens
415
saved_percent = round(saved_tokens / old_tokens * 100) if old_tokens > 0 and saved_tokens > 0 else 0
416
debug.log(f"Token savings: {saved_tokens}/{old_tokens} tokens ({saved_percent}%)")
414
prompt_tokens = usage_dict.get("prompt_tokens", 0) + saved_tokens
415
saved_percent = round(saved_tokens / prompt_tokens * 100) if prompt_tokens > 0 and saved_tokens > 0 else 0
416
debug.log(f"Token savings:", (f"{int(saved_tokens/1000)}k" if saved_tokens >= 1000 else str(saved_tokens)) + f"/{prompt_tokens} tokens ({saved_percent}%)")
417
cached_tokens = usage_dict.get("prompt_tokens_details", usage_dict).get("cached_tokens", 0)
418
if cached_tokens > 0:
419
debug.log(f"Cached tokens:", (f"{int(cached_tokens/1000)}k" if cached_tokens >= 1000 else str(cached_tokens)) + f"/{usage_dict.get('prompt_tokens', 0)} tokens ({round(cached_tokens / usage_dict.get('prompt_tokens', 1) * 100)}%)")
417
420
usage = usage_dict
418
421
usage_dir = Path(get_cookies_dir()) / ".usage"
419
422
usage_file = usage_dir / f"{datetime.date.today()}.jsonl"
420
423
usage_dir.mkdir(parents=True, exist_ok=True)
421
424
if has_aiofile:
422
425
async with async_open(usage_file, "a") as f:
423
asyncio.create_task(f.write(f"{json.dumps(usage)}\n"))
426
async def write_usage():
427
await f.write(f"{json.dumps(usage)}\n")
428
asyncio.create_task(write_usage())
424
429
else:
425
430
with usage_file.open("a") as f:
426
f.write(f"{json.dumps(usage)}\n")
431
json.dump(usage, f)
432
f.write("\n")
427
433
if completion_tokens > 0:
428
434
provider.live += 1
429
435
except Exception:
@@ -630,9 +636,9 @@ def iter_run_tools(
630
636
}
631
637
if saved_tokens:
632
638
usage_dict["saved_tokens"] = saved_tokens
633
old_tokens = usage_dict.get("prompt_tokens", 0) + saved_tokens
634
saved_percent = round(saved_tokens / old_tokens * 100) if old_tokens > 0 and saved_tokens > 0 else 0
635
debug.log(f"Token savings: {saved_tokens}/{old_tokens} tokens ({saved_percent}%)")
639
prompt_tokens = usage_dict.get("prompt_tokens", 0) + saved_tokens
640
saved_percent = round(saved_tokens / prompt_tokens * 100) if prompt_tokens > 0 and saved_tokens > 0 else 0
641
debug.log(f"Token savings: {saved_tokens}/{prompt_tokens} tokens ({saved_percent}%)")
636
642
usage = usage_dict
637
643
usage_dir = Path(get_cookies_dir()) / ".usage"
638
644
usage_file = usage_dir / f"{datetime.date.today()}.jsonl"