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

XFEstudio/gpt4free

Update pa provider support

3ee4635c
hlohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

7 个文件 +213 -27
Modified .gitignore +2 -1
@@ -14,4 +14,5 @@ container/
14 14 g4f.dev
15 15 g4f.exe
16 16 har_and_cookies
17 playground
17 playground
18 .vscode
Modified g4f/Provider/template/OpenaiTemplate.py +0 -1
@@ -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
Modified g4f/client/factory.py +7 -7
@@ -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 """
Modified g4f/mcp/pa_downloader.py +4 -4
@@ -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}")
Modified g4f/mcp/pa_provider.py +179 -2
@@ -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(
Modified g4f/providers/any_provider.py +7 -4
@@ -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}'"
Modified g4f/tools/run_tools.py +14 -8
@@ -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"