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

XFEstudio/gpt4free

Refactor build scripts and API to enhance model handling and improve timeout functionality

25b35ddf
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

8 个文件 +131 -117
Modified .github/workflows/build-packages.yml +9 -7
@@ -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
Modified g4f/Provider/needs_auth/LMArena.py +29 -15
@@ -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\')'):
Modified g4f/api/__init__.py +42 -63
@@ -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,
Modified g4f/gui/server/api.py +14 -10
@@ -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:
Modified g4f/gui/server/backend_api.py +13 -11
@@ -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"] = {}
Modified g4f/providers/any_model_map.py +0 -2
@@ -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",
Modified g4f/providers/base_provider.py +21 -5
@@ -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:
Modified scripts/build-nuitka.sh +3 -4
@@ -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