返回提交历史
Modified
.github/workflows/build-packages.yml
+9
-7
Modified
g4f/Provider/needs_auth/LMArena.py
+29
-15
Modified
g4f/api/__init__.py
+42
-63
Modified
g4f/gui/server/api.py
+14
-10
Modified
g4f/gui/server/backend_api.py
+13
-11
Modified
g4f/providers/any_model_map.py
+0
-2
Modified
g4f/providers/base_provider.py
+21
-5
Modified
scripts/build-nuitka.sh
+3
-4
XFEstudio/gpt4free
Refactor build scripts and API to enhance model handling and improve timeout functionality
25b35ddf
代码差异
8 个文件
+131
-117
@@ -64,7 +64,7 @@ jobs:
64
64
name: pypi-package
65
65
path: dist/
66
66
67
# Windows Executables with Nuitka
67
# Windows Executables
68
68
build-windows-exe:
69
69
runs-on: windows-latest
70
70
needs: prepare
@@ -128,7 +128,7 @@ jobs:
128
128
name: windows-exe-${{ matrix.architecture }}
129
129
path: dist/g4f-windows-*.zip
130
130
131
# Linux Executables with Nuitka
131
# Linux Executables
132
132
build-linux-exe:
133
133
runs-on: ubuntu-latest
134
134
needs: prepare
@@ -136,9 +136,11 @@ jobs:
136
136
matrix:
137
137
include:
138
138
- architecture: x64
139
runner: ubuntu-latest
139
140
runner-arch: x86_64
140
# Note: ARM64 cross-compilation requires additional setup
141
# Keeping architecture in matrix for future expansion
141
- architecture: arm64
142
runner: buildjet-4vcpu-ubuntu-2204-arm
143
runner-arch: aarch64
142
144
steps:
143
145
- uses: actions/checkout@v4
144
146
- name: Set up Python
@@ -148,7 +150,7 @@ jobs:
148
150
- name: Install dependencies
149
151
run: |
150
152
python -m pip install --upgrade pip
151
pip install -r requirements-slim.txt
153
pip install -r requirements.txt
152
154
pip install nuitka
153
155
pip install -e .
154
156
- name: Write g4f_cli.py
@@ -181,7 +183,7 @@ jobs:
181
183
name: linux-exe-${{ matrix.architecture }}
182
184
path: dist/g4f-linux-*
183
185
184
# macOS Executables with Nuitka
186
# macOS Executables
185
187
build-macos-exe:
186
188
runs-on: macos-latest
187
189
needs: prepare
@@ -234,7 +236,7 @@ jobs:
234
236
name: macos-exe-${{ matrix.architecture }}
235
237
path: dist/g4f-macos-*
236
238
237
# Docker Images (reuse existing workflow logic)
239
# Docker Images
238
240
build-docker:
239
241
runs-on: ubuntu-latest
240
242
needs: prepare
@@ -14,10 +14,16 @@ try:
14
14
except ImportError:
15
15
has_curl_cffi = False
16
16
17
try:
18
import nodriver
19
has_nodriver = True
20
except ImportError:
21
has_nodriver = False
22
17
23
from ...typing import AsyncResult, Messages, MediaListType
18
from ...requests import StreamSession, get_args_from_nodriver, raise_for_status, merge_cookies, has_nodriver
24
from ...requests import StreamSession, get_args_from_nodriver, raise_for_status, merge_cookies
19
25
from ...errors import ModelNotFoundError, CloudflareError, MissingAuthError
20
from ...providers.response import FinishReason, Usage, JsonConversation, ImageResponse
26
from ...providers.response import FinishReason, Usage, JsonConversation, ImageResponse, Reasoning
21
27
from ...tools.media import merge_media
22
28
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin,AuthFileMixin
23
29
from ..helper import get_last_user_message
@@ -416,6 +422,22 @@ text_models = {model["publicName"]: model["id"] for model in models if "text" in
416
422
image_models = {model["publicName"]: model["id"] for model in models if "image" in model["capabilities"]["outputCapabilities"]}
417
423
vision_models = [model["publicName"] for model in models if "image" in model["capabilities"]["inputCapabilities"]]
418
424
425
if has_nodriver:
426
async def click_trunstile(page: nodriver.Tab, element = 'document.getElementById("cf-turnstile")'):
427
for _ in range(3):
428
size = None
429
for idx in range(15):
430
size = await page.js_dumps(f'{element}?.getBoundingClientRect()||{{}}')
431
debug.log(f"Found size: {size.get('x'), size.get('y')}")
432
if "x" not in size:
433
break
434
await page.flash_point(size.get("x") + idx * 3, size.get("y") + idx * 3)
435
await page.mouse_click(size.get("x") + idx * 3, size.get("y") + idx * 3)
436
await asyncio.sleep(2)
437
if "x" not in size:
438
break
439
debug.log("Finished clicking trunstile.")
440
419
441
class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
420
442
label = "LMArena"
421
443
url = "https://lmarena.ai"
@@ -423,6 +445,7 @@ class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
423
445
api_endpoint = "https://lmarena.ai/nextjs-api/stream/create-evaluation"
424
446
working = True
425
447
active_by_default = True
448
use_stream_timeout = False
426
449
427
450
default_model = list(text_models.keys())[0]
428
451
models = list(text_models) + list(image_models)
@@ -496,6 +519,9 @@ class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
496
519
pass
497
520
elif has_nodriver or cls.share_url is None:
498
521
async def callback(page):
522
element = await page.select('[style="display: grid;"]')
523
if element:
524
await click_trunstile(page, 'document.querySelector(\'[style="display: grid;"]\')')
499
525
await page.find("Ask anything…", 120)
500
526
button = await page.find("Accept Cookies")
501
527
if button:
@@ -507,19 +533,7 @@ class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
507
533
await page.select('#cf-turnstile', 300)
508
534
debug.log("Found Element: 'cf-turnstile'")
509
535
await asyncio.sleep(3)
510
for _ in range(3):
511
size = None
512
for idx in range(15):
513
size = await page.js_dumps('document.getElementById("cf-turnstile")?.getBoundingClientRect()||{}')
514
debug.log("Found size:", {size.get("x"), size.get("y")})
515
if "x" not in size:
516
break
517
await page.flash_point(size.get("x") + idx * 2, size.get("y") + idx * 2)
518
await page.mouse_click(size.get("x") + idx * 2, size.get("y") + idx * 2)
519
await asyncio.sleep(1)
520
if "x" not in size:
521
break
522
debug.log("Clicked on the turnstile.")
536
await click_trunstile(page)
523
537
while not await page.evaluate('document.cookie.indexOf("arena-auth-prod-v1") >= 0'):
524
538
await asyncio.sleep(1)
525
539
while not await page.evaluate('document.querySelector(\'textarea\')'):
@@ -70,6 +70,7 @@ from g4f.cookies import read_cookie_files, get_cookies_dir
70
70
from g4f.providers.types import ProviderType
71
71
from g4f.providers.response import AudioResponse
72
72
from g4f.providers.any_provider import AnyProvider
73
from g4f.providers.any_model_map import model_map, vision_models, image_models, audio_models, video_models
73
74
from g4f import Provider
74
75
from g4f.gui import get_gui_app
75
76
from .stubs import (
@@ -356,6 +357,21 @@ class Api:
356
357
})
357
358
async def models(provider: str, credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None):
358
359
if provider not in Provider.__map__:
360
if provider in model_map:
361
return {
362
"object": "list",
363
"data": [{
364
"id": provider,
365
"object": "model",
366
"created": 0,
367
"owned_by": getattr(provider, "label", provider.__name__),
368
"image": provider in image_models,
369
"vision": provider in vision_models,
370
"audio": provider in audio_models,
371
"video": provider in video_models,
372
"type": "image" if provider in image_models else "chat",
373
}]
374
}
359
375
return ErrorResponse.from_message("The provider does not exist.", 404)
360
376
provider: ProviderType = Provider.__map__[provider]
361
377
if not hasattr(provider, "get_models"):
@@ -415,6 +431,11 @@ class Api:
415
431
conversation_id: str = None,
416
432
x_user: Annotated[str | None, Header()] = None
417
433
):
434
if provider is not None and provider not in Provider.__map__:
435
if provider in model_map:
436
config.model = provider
437
provider = None
438
return ErrorResponse.from_message("Invalid provider.", HTTP_404_NOT_FOUND)
418
439
try:
419
440
if config.provider is None:
420
441
config.provider = AppConfig.provider if provider is None else provider
@@ -500,58 +521,6 @@ class Api:
500
521
logger.exception(e)
501
522
return ErrorResponse.from_exception(e, config, HTTP_500_INTERNAL_SERVER_ERROR)
502
523
503
responses = {
504
HTTP_200_OK: {"model": ClientResponse},
505
HTTP_401_UNAUTHORIZED: {"model": ErrorResponseModel},
506
HTTP_404_NOT_FOUND: {"model": ErrorResponseModel},
507
HTTP_422_UNPROCESSABLE_ENTITY: {"model": ErrorResponseModel},
508
HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
509
}
510
@self.app.post("/v1/responses", responses=responses)
511
async def v1_responses(
512
config: ResponsesConfig,
513
credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None,
514
provider: str = None
515
):
516
try:
517
if config.provider is None:
518
config.provider = AppConfig.provider if provider is None else provider
519
if config.api_key is None and credentials is not None and credentials.credentials != "secret":
520
config.api_key = credentials.credentials
521
522
conversation = None
523
if config.conversation is not None:
524
conversation = JsonConversation(**config.conversation)
525
526
return await self.client.responses.create(
527
**filter_none(
528
**{
529
"model": AppConfig.model,
530
"proxy": AppConfig.proxy,
531
**config.dict(exclude_none=True),
532
"conversation": conversation
533
},
534
ignored=AppConfig.ignored_providers
535
),
536
)
537
except (ModelNotFoundError, ProviderNotFoundError) as e:
538
logger.exception(e)
539
return ErrorResponse.from_exception(e, config, HTTP_404_NOT_FOUND)
540
except (MissingAuthError, NoValidHarFileError) as e:
541
logger.exception(e)
542
return ErrorResponse.from_exception(e, config, HTTP_401_UNAUTHORIZED)
543
except Exception as e:
544
logger.exception(e)
545
return ErrorResponse.from_exception(e, config, HTTP_500_INTERNAL_SERVER_ERROR)
546
547
@self.app.post("/api/{provider}/responses", responses=responses)
548
async def provider_responses(
549
provider: str,
550
config: ChatCompletionsConfig,
551
credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None,
552
):
553
return await v1_responses(config, credentials, provider)
554
555
524
responses = {
556
525
HTTP_200_OK: {"model": ImagesResponse},
557
526
HTTP_401_UNAUTHORIZED: {"model": ErrorResponseModel},
@@ -568,6 +537,11 @@ class Api:
568
537
provider: str = None,
569
538
credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None
570
539
):
540
if provider is not None and provider not in Provider.__map__:
541
if provider in model_map:
542
config.model = provider
543
provider = None
544
return ErrorResponse.from_message("Invalid provider.", HTTP_404_NOT_FOUND)
571
545
if config.provider is None:
572
546
config.provider = provider
573
547
if config.provider is None:
@@ -646,6 +620,11 @@ class Api:
646
620
prompt: Annotated[Optional[str], Form()] = "Transcribe this audio"
647
621
):
648
622
provider = provider if path_provider is None else path_provider
623
if provider is not None and provider not in Provider.__map__:
624
if provider in model_map:
625
model = provider
626
provider = None
627
return ErrorResponse.from_message("Invalid provider.", HTTP_404_NOT_FOUND)
649
628
kwargs = {"modalities": ["text"]}
650
629
if provider == "MarkItDown":
651
630
kwargs = {
@@ -686,6 +665,11 @@ class Api:
686
665
api_key = None
687
666
if credentials is not None and credentials.credentials != "secret":
688
667
api_key = credentials.credentials
668
if provider is not None and provider not in Provider.__map__:
669
if provider in model_map:
670
config.model = provider
671
provider = None
672
return ErrorResponse.from_message("Invalid provider.", HTTP_404_NOT_FOUND)
689
673
try:
690
674
audio = filter_none(voice=config.voice, format=config.response_format, language=config.language)
691
675
response = await self.client.chat.completions.create(
@@ -744,11 +728,6 @@ class Api:
744
728
read_cookie_files()
745
729
return response_data
746
730
747
@self.app.post("/json/{filename}")
748
async def get_json(filename, request: Request):
749
await asyncio.sleep(30)
750
return ""
751
752
731
@self.app.get("/images/{filename}", responses={
753
732
HTTP_200_OK: {"content": {"image/*": {}}},
754
733
HTTP_404_NOT_FOUND: {}
@@ -854,7 +833,7 @@ class Api:
854
833
return await get_media(filename, request, True)
855
834
856
835
def format_exception(e: Union[Exception, str], config: Union[ChatCompletionsConfig, ImageGenerationConfig] = None, image: bool = False) -> str:
857
last_provider = {} if not image else g4f.get_last_provider(True)
836
last_provider = {}
858
837
provider = (AppConfig.media_provider if image else AppConfig.provider)
859
838
model = AppConfig.model
860
839
if config is not None:
@@ -883,23 +862,23 @@ def run_api(
883
862
**kwargs
884
863
) -> None:
885
864
print(f'Starting server... [g4f v-{g4f.version.utils.current_version}]' + (" (debug)" if debug else ""))
886
865
887
866
if use_colors is None:
888
867
use_colors = debug
889
868
890
869
if bind is not None:
891
870
host, port = bind.split(":")
892
871
893
872
if port is None:
894
873
port = DEFAULT_PORT
895
874
896
875
if AppConfig.demo and debug:
897
876
method = "create_app_with_demo_and_debug"
898
877
elif AppConfig.gui and debug:
899
878
method = "create_app_with_gui_and_debug"
900
879
else:
901
880
method = "create_app_debug" if debug else "create_app"
902
881
903
882
uvicorn.run(
904
883
f"g4f.api:{method}",
905
884
host=host,
@@ -22,8 +22,10 @@ from ...providers.base_provider import ProviderModelMixin
22
22
from ...providers.retry_provider import BaseRetryProvider
23
23
from ...providers.helper import format_media_prompt
24
24
from ...providers.response import *
25
from ...providers.any_model_map import model_map
26
from ...providers.any_provider import AnyProvider
27
from ...client.service import get_model_and_provider
25
28
from ... import version, models
26
from ... import ChatCompletion, get_model_and_provider
27
29
from ... import debug
28
30
29
31
logger = logging.getLogger(__name__)
@@ -47,11 +49,11 @@ class Api:
47
49
48
50
@staticmethod
49
51
def get_provider_models(provider: str, api_key: str = None, api_base: str = None, ignored: list = None):
50
def get_model_data(provider: ProviderModelMixin, model: str):
52
def get_model_data(provider: ProviderModelMixin, model: str, default: bool = False) -> dict:
51
53
return {
52
54
"model": model,
53
55
"label": model.split(":")[-1] if provider.__name__ == "AnyProvider" and not model.startswith("openrouter:") else model,
54
"default": model == provider.default_model,
56
"default": default or model == provider.default_model,
55
57
"vision": model in provider.vision_models,
56
58
"audio": False if provider.audio_models is None else model in provider.audio_models,
57
59
"video": model in provider.video_models,
@@ -78,6 +80,9 @@ class Api:
78
80
get_model_data(provider, model)
79
81
for model in models
80
82
]
83
elif provider in model_map:
84
return [get_model_data(AnyProvider, provider, True)]
85
81
86
return []
82
87
83
88
@staticmethod
@@ -144,10 +149,10 @@ class Api:
144
149
145
150
def _prepare_conversation_kwargs(self, json_data: dict):
146
151
kwargs = {**json_data}
147
model = json_data.get('model')
148
provider = json_data.get('provider')
149
messages = json_data.get('messages')
150
action = json_data.get('action')
152
model = kwargs.pop('model', None)
153
provider = kwargs.pop('provider', None)
154
messages = kwargs.pop('messages', None)
155
action = kwargs.get('action')
151
156
if action == "continue":
152
157
kwargs["tool_calls"].append({
153
158
"function": {
@@ -155,7 +160,7 @@ class Api:
155
160
},
156
161
"type": "function"
157
162
})
158
conversation = json_data.get("conversation")
163
conversation = kwargs.pop("conversation", None)
159
164
if isinstance(conversation, dict):
160
165
kwargs["conversation"] = JsonConversation(**conversation)
161
166
return {
@@ -174,10 +179,9 @@ class Api:
174
179
if "user" not in kwargs:
175
180
debug.log = decorated_log
176
181
proxy = os.environ.get("G4F_PROXY")
177
provider = kwargs.pop("provider", None)
178
182
try:
179
183
model, provider_handler = get_model_and_provider(
180
kwargs.get("model"), provider,
184
kwargs.get("model"), provider or AnyProvider,
181
185
has_images="media" in kwargs,
182
186
)
183
187
if "user" in kwargs:
@@ -47,6 +47,8 @@ from ...image import is_allowed_extension, process_image, MEDIA_TYPE_MAP
47
47
from ...cookies import get_cookies_dir
48
48
from ...image.copy_images import secure_filename, get_source_url, get_media_dir, copy_media
49
49
from ...client.service import get_model_and_provider
50
from ...providers.any_model_map import model_map
51
from ... import Provider
50
52
from ... import models
51
53
from .api import Api
52
54
@@ -208,11 +210,19 @@ class Backend_Api(Api):
208
210
json_data["user"] = request.headers.get("x-user", "error")
209
211
json_data["referer"] = request.headers.get("referer", "")
210
212
json_data["user-agent"] = request.headers.get("user-agent", "")
213
211
214
kwargs = self._prepare_conversation_kwargs(json_data)
215
provider = kwargs.pop("provider", None)
216
if provider and provider not in Provider.__map__:
217
if provider in model_map:
218
kwargs['model'] = provider
219
provider = None
220
else:
221
return jsonify({"error": {"message": "Provider not found"}}), 404
212
222
return self.app.response_class(
213
223
safe_iter_generator(self._create_response_stream(
214
224
kwargs,
215
json_data.get("provider"),
225
provider,
216
226
json_data.get("download_media", True),
217
227
tempfiles
218
228
)),
@@ -277,18 +287,10 @@ class Backend_Api(Api):
277
287
@app.route('/backend-api/v2/create', methods=['GET'])
278
288
def create():
279
289
try:
280
tool_calls = []
281
290
web_search = request.args.get("web_search")
282
291
if web_search:
283
292
is_true_web_search = web_search.lower() in ["true", "1"]
284
web_search = None if is_true_web_search else web_search
285
tool_calls.append({
286
"function": {
287
"name": "search_tool",
288
"arguments": {"query": web_search, "instructions": "", "max_words": 1000} if web_search != "true" else {}
289
},
290
"type": "function"
291
})
293
web_search = True if is_true_web_search else web_search
292
294
do_filter = request.args.get("filter_markdown", request.args.get("json"))
293
295
cache_id = request.args.get('cache')
294
296
model, provider_handler = get_model_and_provider(
@@ -300,7 +302,7 @@ class Backend_Api(Api):
300
302
"model": model,
301
303
"messages": [{"role": "user", "content": request.args.get("prompt")}],
302
304
"stream": not do_filter and not cache_id,
303
"tool_calls": tool_calls,
305
"web_search": web_search,
304
306
}
305
307
if request.args.get("audio_provider") or request.args.get("audio"):
306
308
parameters["audio"] = {}
@@ -179,9 +179,7 @@ model_map = {
179
179
},
180
180
"gpt-oss-120b": {
181
181
"Together": "openai/gpt-oss-120b",
182
"DeepInfra": "openai/gpt-oss-120b",
183
182
"HuggingFace": "openai/gpt-oss-120b",
184
"OpenRouter": "openai/gpt-oss-120b:free",
185
183
"Groq": "openai/gpt-oss-120b",
186
184
"Azure": "gpt-oss-120b",
187
185
"OpenRouterFree": "openai/gpt-oss-120b",
@@ -284,6 +284,7 @@ class AsyncGeneratorProvider(AbstractProvider):
284
284
Provides asynchronous generator functionality for streaming results.
285
285
"""
286
286
supports_stream = True
287
use_stream_timeout = True
287
288
288
289
@classmethod
289
290
def create_completion(
@@ -309,7 +310,7 @@ class AsyncGeneratorProvider(AbstractProvider):
309
310
"""
310
311
return to_sync_generator(
311
312
cls.create_async_generator(model, messages, **kwargs),
312
timeout=timeout if stream_timeout is None else stream_timeout,
313
timeout=stream_timeout if cls.use_stream_timeout is None else timeout,
313
314
)
314
315
315
316
@staticmethod
@@ -336,7 +337,7 @@ class AsyncGeneratorProvider(AbstractProvider):
336
337
raise NotImplementedError()
337
338
338
339
@classmethod
339
def async_create_function(cls, *args, **kwargs) -> AsyncResult:
340
async def async_create_function(cls, *args, **kwargs) -> AsyncResult:
340
341
"""
341
342
Creates a completion using the synchronous method.
342
343
@@ -346,7 +347,19 @@ class AsyncGeneratorProvider(AbstractProvider):
346
347
Returns:
347
348
CreateResult: The result of the completion creation.
348
349
"""
349
return cls.create_async_generator(*args, **kwargs)
350
response = cls.create_async_generator(*args, **kwargs)
351
if "stream_timeout" in kwargs or "timeout" in kwargs:
352
while True:
353
try:
354
yield await asyncio.wait_for(
355
response.__anext__(),
356
timeout=kwargs.get("stream_timeout") if cls.use_stream_timeout else kwargs.get("timeout")
357
)
358
except StopAsyncIteration:
359
break
360
else:
361
async for chunk in response:
362
yield chunk
350
363
351
364
class ProviderModelMixin:
352
365
default_model: str = None
@@ -501,10 +514,13 @@ class AsyncAuthedProvider(AsyncGeneratorProvider, AuthFileMixin):
501
514
try:
502
515
auth_result = cls.get_auth_result()
503
516
response = to_async_iterator(cls.create_authed(model, messages, **kwargs, auth_result=auth_result))
504
if "stream_timeout" in kwargs:
517
if "stream_timeout" in kwargs or "timeout" in kwargs:
505
518
while True:
506
519
try:
507
yield await asyncio.wait_for(response.__anext__(), timeout=kwargs["stream_timeout"])
520
yield await asyncio.wait_for(
521
response.__anext__(),
522
timeout=kwargs.get("stream_timeout") if cls.use_stream_timeout else kwargs.get("timeout")
523
)
508
524
except StopAsyncIteration:
509
525
break
510
526
else:
@@ -43,22 +43,21 @@ case "${PLATFORM}" in
43
43
;;
44
44
"darwin"|"macos")
45
45
OUTPUT_NAME="g4f-macos-${VERSION}-${ARCH}"
46
NUITKA_ARGS="--macos-create-app-bundle"
46
NUITKA_ARGS="--macos-create-app-bundle --onefile"
47
47
;;
48
48
"linux")
49
49
OUTPUT_NAME="g4f-linux-${VERSION}-${ARCH}"
50
NUITKA_ARGS=""
50
NUITKA_ARGS="--onefile"
51
51
;;
52
52
*)
53
53
OUTPUT_NAME="g4f-${PLATFORM}-${VERSION}-${ARCH}"
54
NUITKA_ARGS=""
54
NUITKA_ARGS="--onefile"
55
55
;;
56
56
esac
57
57
58
58
# Basic Nuitka arguments
59
59
NUITKA_COMMON_ARGS="
60
60
--standalone
61
--onefile
62
61
--output-filename=${OUTPUT_NAME}
63
62
--output-dir=${OUTPUT_DIR}
64
63
--remove-output