返回提交历史
Modified
g4f/Provider/__init__.py
+11
-0
Modified
g4f/Provider/template/OpenaiTemplate.py
+5
-2
Modified
g4f/mcp/pa_downloader.py
+78
-13
XFEstudio/gpt4free
Update pa providers download, Add LLM7 provider
a6491d7c
代码差异
3 个文件
+94
-15
@@ -242,6 +242,17 @@ def _resolve_provider(name: str) -> ProviderType:
242
242
_loaded_providers[name].active_by_default = True
243
243
_loaded_providers[name].default_model = "kilo-auto/free"
244
244
return _loaded_providers[name]
245
elif name == "LLM7":
246
from ..client.factory import AbstractClientFactory
247
_loaded_providers[name] = AbstractClientFactory.create_provider(
248
None, "https://api.llm7.io/v1"
249
)
250
_loaded_providers[name].__name__ = name
251
_loaded_providers[name].active_by_default = True
252
_loaded_providers[name].default_model = "default"
253
_loaded_providers[name].models = ["default"]
254
_loaded_providers[name].add_user = False
255
return _loaded_providers[name]
245
256
elif name == "Ollama":
246
257
from g4f.Provider.local.Ollama import Ollama
247
258
@@ -76,6 +76,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
76
76
def is_provider_api_key(cls, api_key: str) -> bool:
77
77
return (
78
78
api_key
79
and isinstance(api_key, str)
79
80
and not api_key.startswith("g4f_")
80
81
and not api_key.startswith("gfs_")
81
82
)
@@ -101,10 +102,11 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
101
102
base_url = cls.base_url
102
103
else:
103
104
if cls.backup_url is None:
104
api_key = None
105
105
base_url = cls.base_url
106
106
else:
107
107
base_url = cls.backup_url
108
if not base_url.startswith(SPACE_URL):
109
api_key = None
108
110
if base_url is None:
109
111
raise NotImplementedError(
110
112
"No base_url or backup_url specified."
@@ -237,10 +239,11 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
237
239
base_url = cls.base_url
238
240
else:
239
241
if cls.backup_url is None:
240
api_key = None
241
242
base_url = cls.base_url
242
243
else:
243
244
base_url = cls.backup_url
245
if not base_url.startswith(SPACE_URL):
246
api_key = None
244
247
if base_url is None:
245
248
raise NotImplementedError(
246
249
"No base_url or backup_url specified."
@@ -24,6 +24,7 @@ from __future__ import annotations
24
24
import os
25
25
import json
26
26
import time
27
from concurrent.futures import ThreadPoolExecutor
27
28
from pathlib import Path
28
29
from typing import List, Optional
29
30
from urllib.request import Request, urlopen
@@ -49,11 +50,16 @@ GITHUB_API = "https://api.github.com"
49
50
#: Per-request timeout (seconds) for GitHub API calls and raw downloads.
50
51
DEFAULT_TIMEOUT: float = 30.0
51
52
53
# Limit concurrent requests while keeping bulk downloads substantially faster.
54
DOWNLOAD_WORKERS = 8
55
52
56
#: Marker file written into the workspace after a successful auto-download.
53
57
#: Its mtime is used to decide when the next auto-download is allowed, so we
54
58
#: do not hit GitHub on every single server start.
55
59
AUTO_DOWNLOAD_MARKER = ".pa_auto_downloaded"
56
60
61
DELETED_FILE = ".deleted"
62
57
63
#: Minimum seconds between two automatic downloads.
58
64
AUTO_DOWNLOAD_INTERVAL: float = 6 * 60 * 60 # 6 hours
59
65
@@ -97,6 +103,10 @@ def _is_pa_file(name: str) -> bool:
97
103
Accepted: ``*.py``, ``*.wasm``, plus browser scripts named ``pa-*.js``
98
104
or ``*.pa.js``.
99
105
"""
106
if name == DELETED_FILE:
107
return True
108
if name.startswith("test_"):
109
return False
100
110
if name.endswith(".py") or name.endswith(".wasm"):
101
111
return True
102
112
if name.endswith(".js"):
@@ -142,6 +152,42 @@ def _workspace_target(directory: Optional[str] = None) -> Path:
142
152
return target
143
153
144
154
155
def _read_deleted_manifest(target: Path) -> bytes:
156
"""Return the local deletion manifest, or empty bytes if absent."""
157
try:
158
return (target / DELETED_FILE).read_bytes()
159
except OSError:
160
return b""
161
162
163
def _remove_deleted_files(target: Path, content: bytes) -> None:
164
"""Remove files named one-per-line in a deletion manifest."""
165
try:
166
names = content.decode("utf-8").splitlines()
167
except UnicodeDecodeError as e:
168
debug.error("pa-providers: invalid .deleted manifest:", e)
169
return
170
171
root = target.resolve()
172
for name in names:
173
name = name.strip()
174
if not name or name.startswith("#"):
175
continue
176
candidate = (target / name).resolve()
177
try:
178
candidate.relative_to(root)
179
except ValueError:
180
debug.error(f"pa-providers: refusing to delete outside workspace: {name}")
181
continue
182
if candidate == target / DELETED_FILE or not candidate.is_file():
183
continue
184
try:
185
candidate.unlink()
186
print(f"pa-providers: removed deleted file {name}")
187
except OSError as e:
188
debug.error(f"pa-providers: failed to remove deleted file {name}:", e)
189
190
145
191
# ---------------------------------------------------------------------------
146
192
# Public API
147
193
# ---------------------------------------------------------------------------
@@ -171,6 +217,7 @@ def run_pa_download(
171
217
"""
172
218
target = _workspace_target(directory)
173
219
written: List[Path] = []
220
deleted_manifest = _read_deleted_manifest(target)
174
221
175
222
try:
176
223
if only:
@@ -182,6 +229,7 @@ def run_pa_download(
182
229
debug.error(f"pa-providers: failed to list repository {repo}@{ref}:", e)
183
230
return written
184
231
232
pending = []
185
233
for name in names:
186
234
if not _is_pa_file(name):
187
235
continue
@@ -189,19 +237,36 @@ def run_pa_download(
189
237
if dest.exists() and not force:
190
238
debug.log(f"pa-providers: skip existing {name}")
191
239
continue
192
try:
193
content = _download_raw(repo, ref, name, timeout)
194
except (URLError, HTTPError, OSError) as e:
195
debug.error(f"pa-providers: failed to download {name}:", e)
196
continue
197
try:
198
dest.write_bytes(content)
199
written.append(dest)
200
print(f"pa-providers: downloaded {name} ({len(content)} bytes)")
201
except OSError as e:
202
debug.error(f"pa-providers: failed to write {name}:", e)
203
204
print(f"pa-providers: downloaded {len(written)} file(s) from {repo}@{ref}")
240
pending.append((name, dest))
241
242
total_size = 0
243
244
# Network requests run concurrently; handling results in submission order
245
# keeps output and the returned list stable for callers.
246
with ThreadPoolExecutor(max_workers=DOWNLOAD_WORKERS) as executor:
247
downloads = {
248
name: executor.submit(_download_raw, repo, ref, name, timeout)
249
for name, _ in pending
250
}
251
for name, dest in pending:
252
try:
253
content = downloads[name].result()
254
except (URLError, HTTPError, OSError) as e:
255
debug.error(f"pa-providers: failed to download {name}:", e)
256
continue
257
if name == DELETED_FILE:
258
deleted_manifest = content
259
continue
260
try:
261
dest.write_bytes(content)
262
written.append(dest)
263
total_size += len(content)
264
except OSError as e:
265
debug.error(f"pa-providers: failed to write {name}:", e)
266
267
_remove_deleted_files(target, deleted_manifest)
268
269
print(f"pa-providers: downloaded {len(written)} file(s) ({total_size/1e3} kb) from {repo}@{ref}")
205
270
if written:
206
271
# Touch the auto-download marker so the startup path does not re-download
207
272
# immediately after an explicit `g4f pa download`.