From 804a80bc7cb788a77af169242b84c892ec4fc638 Mon Sep 17 00:00:00 2001 From: H Lohaus Date: Sun, 24 Nov 2024 17:43:45 +0100 Subject: [PATCH 1/3] Arm2 (#2414) * Fix arm v7 build / improve api * Update stubs.py * Fix unit tests --- docker/Dockerfile-slim | 2 +- g4f/Provider/PollinationsAI.py | 3 +- g4f/Provider/needs_auth/Gemini.py | 1 + g4f/api/__init__.py | 115 +++++++++------ g4f/cli.py | 4 +- g4f/client/__init__.py | 20 +-- g4f/client/stubs.py | 236 ++++++++++++++++-------------- g4f/gui/__init__.py | 28 ++-- g4f/gui/server/api.py | 40 +---- g4f/gui/server/backend.py | 23 ++- g4f/gui/server/website.py | 7 +- g4f/requests/raise_for_status.py | 2 +- 12 files changed, 255 insertions(+), 226 deletions(-) diff --git a/docker/Dockerfile-slim b/docker/Dockerfile-slim index 6bc15cff2a9..6cbf2254615 100644 --- a/docker/Dockerfile-slim +++ b/docker/Dockerfile-slim @@ -47,7 +47,7 @@ RUN python -m pip install --upgrade pip \ --global-option=build_ext \ --global-option=-j8 \ pydantic==${PYDANTIC_VERSION} \ - && cat requirements.txt | xargs -n 1 pip install --no-cache-dir \ + && cat requirements-slim.txt | xargs -n 1 pip install --no-cache-dir || true \ # Remove build packages && pip uninstall --yes \ Cython \ diff --git a/g4f/Provider/PollinationsAI.py b/g4f/Provider/PollinationsAI.py index a30f896d302..e82222b12ea 100644 --- a/g4f/Provider/PollinationsAI.py +++ b/g4f/Provider/PollinationsAI.py @@ -46,8 +46,7 @@ async def create_async_generator( seed: str = None, **kwargs ) -> AsyncResult: - if model: - model = cls.get_model(model) + model = cls.get_model(model) if model in cls.image_models: if prompt is None: prompt = messages[-1]["content"] diff --git a/g4f/Provider/needs_auth/Gemini.py b/g4f/Provider/needs_auth/Gemini.py index 7b84f284c75..e7c9de23ab6 100644 --- a/g4f/Provider/needs_auth/Gemini.py +++ b/g4f/Provider/needs_auth/Gemini.py @@ -313,6 +313,7 @@ def __init__(self, self.conversation_id = conversation_id self.response_id = response_id self.choice_id = choice_id + async def iter_filter_base64(response_iter: AsyncIterator[bytes]) -> AsyncIterator[bytes]: search_for = b'[["wrb.fr","XqA3Ic","[\\"' end_with = b'\\' diff --git a/g4f/api/__init__.py b/g4f/api/__init__.py index 94fc23a50a7..a8403e5cddb 100644 --- a/g4f/api/__init__.py +++ b/g4f/api/__init__.py @@ -8,21 +8,29 @@ import shutil import os.path -from fastapi import FastAPI, Response, Request, UploadFile +from fastapi import FastAPI, Response, Request, UploadFile, Depends +from fastapi.middleware.wsgi import WSGIMiddleware from fastapi.responses import StreamingResponse, RedirectResponse, HTMLResponse, JSONResponse from fastapi.exceptions import RequestValidationError from fastapi.security import APIKeyHeader from starlette.exceptions import HTTPException -from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from starlette.status import ( + HTTP_200_OK, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_404_NOT_FOUND, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN +) from fastapi.encoders import jsonable_encoder +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.middleware.cors import CORSMiddleware from starlette.responses import FileResponse -from pydantic import BaseModel -from typing import Union, Optional, List +from pydantic import BaseModel, Field +from typing import Union, Optional, List, Annotated import g4f import g4f.debug -from g4f.client import AsyncClient, ChatCompletion, convert_to_provider +from g4f.client import AsyncClient, ChatCompletion, ImagesResponse, convert_to_provider from g4f.providers.response import BaseConversation from g4f.client.helper import filter_none from g4f.image import is_accepted_format, images_dir @@ -30,6 +38,7 @@ from g4f.errors import ProviderNotFoundError from g4f.cookies import read_cookie_files, get_cookies_dir from g4f.Provider import ProviderType, ProviderUtils, __providers__ +from g4f.gui import get_gui_app logger = logging.getLogger(__name__) @@ -50,6 +59,10 @@ def create_app(g4f_api_key: str = None): api.register_authorization() api.register_validation_exception_handler() + if AppConfig.gui: + gui_app = WSGIMiddleware(get_gui_app()) + app.mount("/", gui_app) + # Read cookie files if not ignored if not AppConfig.ignore_cookie_files: read_cookie_files() @@ -61,17 +74,17 @@ def create_app_debug(g4f_api_key: str = None): return create_app(g4f_api_key) class ChatCompletionsConfig(BaseModel): - messages: Messages - model: str - provider: Optional[str] = None + messages: Messages = Field(examples=[[{"role": "system", "content": ""}, {"role": "user", "content": ""}]]) + model: str = Field(default="") + provider: Optional[str] = Field(examples=[None]) stream: bool = False - temperature: Optional[float] = None - max_tokens: Optional[int] = None - stop: Union[list[str], str, None] = None - api_key: Optional[str] = None - web_search: Optional[bool] = None - proxy: Optional[str] = None - conversation_id: str = None + temperature: Optional[float] = Field(examples=[None]) + max_tokens: Optional[int] = Field(examples=[None]) + stop: Union[list[str], str, None] = Field(examples=[None]) + api_key: Optional[str] = Field(examples=[None]) + web_search: Optional[bool] = Field(examples=[None]) + proxy: Optional[str] = Field(examples=[None]) + conversation_id: Optional[str] = Field(examples=[None]) class ImageGenerationConfig(BaseModel): prompt: str @@ -101,6 +114,9 @@ class ModelResponseModel(BaseModel): created: int owned_by: Optional[str] +class ErrorResponseModel(BaseModel): + error: str + class AppConfig: ignored_providers: Optional[list[str]] = None g4f_api_key: Optional[str] = None @@ -109,6 +125,7 @@ class AppConfig: provider: str = None image_provider: str = None proxy: str = None + gui: bool = False @classmethod def set_config(cls, **data): @@ -129,6 +146,8 @@ def __init__(self, app: FastAPI, g4f_api_key=None) -> None: self.get_g4f_api_key = APIKeyHeader(name="g4f-api-key") self.conversations: dict[str, dict[str, BaseConversation]] = {} + security = HTTPBearer(auto_error=False) + def register_authorization(self): @self.app.middleware("http") async def authorization(request: Request, call_next): @@ -192,7 +211,7 @@ async def models() -> list[ModelResponseModel]: } for model_id, model in model_list.items()] @self.app.get("/v1/models/{model_name}") - async def model_info(model_name: str): + async def model_info(model_name: str) -> ModelResponseModel: if model_name in g4f.models.ModelUtils.convert: model_info = g4f.models.ModelUtils.convert[model_name] return JSONResponse({ @@ -201,20 +220,20 @@ async def model_info(model_name: str): 'created': 0, 'owned_by': model_info.base_provider }) - return JSONResponse({"error": "The model does not exist."}, 404) - - @self.app.post("/v1/chat/completions") - async def chat_completions(config: ChatCompletionsConfig, request: Request = None, provider: str = None): + return JSONResponse({"error": "The model does not exist."}, HTTP_404_NOT_FOUND) + + @self.app.post("/v1/chat/completions", response_model=ChatCompletion) + async def chat_completions( + config: ChatCompletionsConfig, + credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None, + provider: str = None + ): try: config.provider = provider if config.provider is None else config.provider if config.provider is None: config.provider = AppConfig.provider - if config.api_key is None and request is not None: - auth_header = request.headers.get("Authorization") - if auth_header is not None: - api_key = auth_header.split(None, 1)[-1] - if api_key and api_key != "Bearer": - config.api_key = api_key + if credentials is not None: + config.api_key = credentials.credentials conversation = return_conversation = None if config.conversation_id is not None and config.provider is not None: @@ -242,8 +261,7 @@ async def chat_completions(config: ChatCompletionsConfig, request: Request = Non ) if not config.stream: - response: ChatCompletion = await response - return JSONResponse(response.to_json()) + return await response async def streaming(): try: @@ -254,7 +272,7 @@ async def streaming(): self.conversations[config.conversation_id] = {} self.conversations[config.conversation_id][config.provider] = chunk else: - yield f"data: {json.dumps(chunk.to_json())}\n\n" + yield f"data: {chunk.json()}\n\n" except GeneratorExit: pass except Exception as e: @@ -268,15 +286,15 @@ async def streaming(): logger.exception(e) return Response(content=format_exception(e, config), status_code=500, media_type="application/json") - @self.app.post("/v1/images/generate") - @self.app.post("/v1/images/generations") - async def generate_image(config: ImageGenerationConfig, request: Request): - if config.api_key is None: - auth_header = request.headers.get("Authorization") - if auth_header is not None: - api_key = auth_header.split(None, 1)[-1] - if api_key and api_key != "Bearer": - config.api_key = api_key + @self.app.post("/v1/images/generate", response_model=ImagesResponse) + @self.app.post("/v1/images/generations", response_model=ImagesResponse) + async def generate_image( + request: Request, + config: ImageGenerationConfig, + credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None + ): + if credentials is not None: + config.api_key = credentials.credentials try: response = await self.client.images.generate( prompt=config.prompt, @@ -291,7 +309,7 @@ async def generate_image(config: ImageGenerationConfig, request: Request): for image in response.data: if hasattr(image, "url") and image.url.startswith("/"): image.url = f"{request.base_url}{image.url.lstrip('/')}" - return JSONResponse(response.to_json()) + return response except Exception as e: logger.exception(e) return Response(content=format_exception(e, config, True), status_code=500, media_type="application/json") @@ -342,22 +360,29 @@ def upload_cookies(files: List[UploadFile]): file.file.close() return response_data - @self.app.get("/v1/synthesize/{provider}") + @self.app.get("/v1/synthesize/{provider}", responses={ + HTTP_200_OK: {"content": {"audio/*": {}}}, + HTTP_404_NOT_FOUND: {"model": ErrorResponseModel}, + HTTP_422_UNPROCESSABLE_ENTITY: {"model": ErrorResponseModel}, + }) async def synthesize(request: Request, provider: str): try: provider_handler = convert_to_provider(provider) except ProviderNotFoundError: - return Response("Provider not found", 404) + return JSONResponse({"error": "Provider not found"}, HTTP_404_NOT_FOUND) if not hasattr(provider_handler, "synthesize"): - return Response("Provider doesn't support synthesize", 500) + return JSONResponse({"error": "Provider doesn't support synthesize"}, HTTP_404_NOT_FOUND) if len(request.query_params) == 0: - return Response("Missing query params", 500) + return JSONResponse({"error": "Missing query params"}, HTTP_422_UNPROCESSABLE_ENTITY) response_data = provider_handler.synthesize({**request.query_params}) content_type = getattr(provider_handler, "synthesize_content_type", "application/octet-stream") return StreamingResponse(response_data, media_type=content_type) - @self.app.get("/images/{filename}") - async def get_image(filename) -> FileResponse: + @self.app.get("/images/{filename}", response_class=FileResponse, responses={ + HTTP_200_OK: {"content": {"image/*": {}}}, + HTTP_404_NOT_FOUND: {} + }) + async def get_image(filename): target = os.path.join(images_dir, filename) if not os.path.isfile(target): diff --git a/g4f/cli.py b/g4f/cli.py index 04bfb6ad3fa..90ec37fa6f0 100644 --- a/g4f/cli.py +++ b/g4f/cli.py @@ -12,6 +12,7 @@ def main(): api_parser = subparsers.add_parser("api") api_parser.add_argument("--bind", default="0.0.0.0:1337", help="The bind string.") api_parser.add_argument("--debug", action="store_true", help="Enable verbose logging.") + api_parser.add_argument("--gui", "-g", default=False, action="store_true", help="Add gui to the api.") api_parser.add_argument("--model", default=None, help="Default model for chat completion. (incompatible with --reload and --workers)") api_parser.add_argument("--provider", choices=[provider.__name__ for provider in Provider.__providers__ if provider.working], default=None, help="Default provider for chat completion. (incompatible with --reload and --workers)") @@ -48,7 +49,8 @@ def run_api_args(args): provider=args.provider, image_provider=args.image_provider, proxy=args.proxy, - model=args.model + model=args.model, + gui=args.gui, ) g4f.cookies.browsers = [g4f.cookies[browser] for browser in args.cookie_browsers] run_api( diff --git a/g4f/client/__init__.py b/g4f/client/__init__.py index f6a0f5e8eb9..05539e21eb1 100644 --- a/g4f/client/__init__.py +++ b/g4f/client/__init__.py @@ -73,7 +73,7 @@ def iter_response( finish_reason = "stop" if stream: - yield ChatCompletionChunk(chunk, None, completion_id, int(time.time())) + yield ChatCompletionChunk.model_construct(chunk, None, completion_id, int(time.time())) if finish_reason is not None: break @@ -83,12 +83,12 @@ def iter_response( finish_reason = "stop" if finish_reason is None else finish_reason if stream: - yield ChatCompletionChunk(None, finish_reason, completion_id, int(time.time())) + yield ChatCompletionChunk.model_construct(None, finish_reason, completion_id, int(time.time())) else: if response_format is not None and "type" in response_format: if response_format["type"] == "json_object": content = filter_json(content) - yield ChatCompletion(content, finish_reason, completion_id, int(time.time())) + yield ChatCompletion.model_construct(content, finish_reason, completion_id, int(time.time())) # Synchronous iter_append_model_and_provider function def iter_append_model_and_provider(response: ChatCompletionResponseType) -> ChatCompletionResponseType: @@ -137,7 +137,7 @@ async def async_iter_response( finish_reason = "stop" if stream: - yield ChatCompletionChunk(chunk, None, completion_id, int(time.time())) + yield ChatCompletionChunk.model_construct(chunk, None, completion_id, int(time.time())) if finish_reason is not None: break @@ -145,12 +145,12 @@ async def async_iter_response( finish_reason = "stop" if finish_reason is None else finish_reason if stream: - yield ChatCompletionChunk(None, finish_reason, completion_id, int(time.time())) + yield ChatCompletionChunk.model_construct(None, finish_reason, completion_id, int(time.time())) else: if response_format is not None and "type" in response_format: if response_format["type"] == "json_object": content = filter_json(content) - yield ChatCompletion(content, finish_reason, completion_id, int(time.time())) + yield ChatCompletion.model_construct(content, finish_reason, completion_id, int(time.time())) finally: if hasattr(response, 'aclose'): await safe_aclose(response) @@ -394,13 +394,13 @@ async def process_image_item(image_file: str) -> Image: if response_format == "b64_json": with open(os.path.join(images_dir, os.path.basename(image_file)), "rb") as file: image_data = base64.b64encode(file.read()).decode() - return Image(url=image_file, b64_json=image_data, revised_prompt=response.alt) - return Image(url=image_file, revised_prompt=response.alt) + return Image.model_construct(url=image_file, b64_json=image_data, revised_prompt=response.alt) + return Image.model_construct(url=image_file, revised_prompt=response.alt) images = await asyncio.gather(*[process_image_item(image) for image in images]) else: - images = [Image(url=image, revised_prompt=response.alt) for image in response.get_list()] + images = [Image.model_construct(url=image, revised_prompt=response.alt) for image in response.get_list()] last_provider = get_last_provider(True) - return ImagesResponse( + return ImagesResponse.model_construct( images, model=last_provider.get("model") if model is None else model, provider=last_provider.get("name") if provider is None else provider diff --git a/g4f/client/stubs.py b/g4f/client/stubs.py index b38c9f6c680..8e54e419f45 100644 --- a/g4f/client/stubs.py +++ b/g4f/client/stubs.py @@ -1,130 +1,150 @@ from __future__ import annotations -from typing import Union +from typing import Optional, List, Dict from time import time -class Model(): - ... +from .helper import filter_none + +try: + from pydantic import BaseModel, Field +except ImportError: + class BaseModel(): + @classmethod + def model_construct(cls, **data): + new = cls() + for key, value in data.items(): + setattr(new, key, value) + return new + class Field(): + def __init__(self, **config): + pass + +class ChatCompletionChunk(BaseModel): + id: str + object: str + created: int + model: str + provider: Optional[str] + choices: List[ChatCompletionDeltaChoice] -class ChatCompletion(Model): - def __init__( - self, + @classmethod + def model_construct( + cls, content: str, finish_reason: str, completion_id: str = None, created: int = None ): - self.id: str = f"chatcmpl-{completion_id}" if completion_id else None - self.object: str = "chat.completion" - self.created: int = created - self.model: str = None - self.provider: str = None - self.choices = [ChatCompletionChoice(ChatCompletionMessage(content), finish_reason)] - self.usage: dict[str, int] = { - "prompt_tokens": 0, #prompt_tokens, - "completion_tokens": 0, #completion_tokens, - "total_tokens": 0, #prompt_tokens + completion_tokens, - } - - def to_json(self): - return { - **self.__dict__, - "choices": [choice.to_json() for choice in self.choices] - } - -class ChatCompletionChunk(Model): - def __init__( - self, + return super().model_construct( + id=f"chatcmpl-{completion_id}" if completion_id else None, + object="chat.completion.cunk", + created=created, + model=None, + provider=None, + choices=[ChatCompletionDeltaChoice.model_construct( + ChatCompletionDelta.model_construct(content), + finish_reason + )] + ) + +class ChatCompletionMessage(BaseModel): + role: str + content: str + + @classmethod + def model_construct(cls, content: str): + return super().model_construct(role="assistant", content=content) + +class ChatCompletionChoice(BaseModel): + index: int + message: ChatCompletionMessage + finish_reason: str + + @classmethod + def model_construct(cls, message: ChatCompletionMessage, finish_reason: str): + return super().model_construct(index=0, message=message, finish_reason=finish_reason) + +class ChatCompletion(BaseModel): + id: str + object: str + created: int + model: str + provider: Optional[str] + choices: List[ChatCompletionChoice] + usage: Dict[str, int] = Field(examples=[{ + "prompt_tokens": 0, #prompt_tokens, + "completion_tokens": 0, #completion_tokens, + "total_tokens": 0, #prompt_tokens + completion_tokens, + }]) + + @classmethod + def model_construct( + cls, content: str, finish_reason: str, completion_id: str = None, created: int = None ): - self.id: str = f"chatcmpl-{completion_id}" if completion_id else None - self.object: str = "chat.completion.chunk" - self.created: int = created - self.model: str = None - self.provider: str = None - self.choices = [ChatCompletionDeltaChoice(ChatCompletionDelta(content), finish_reason)] - - def to_json(self): - return { - **self.__dict__, - "choices": [choice.to_json() for choice in self.choices] - } - -class ChatCompletionMessage(Model): - def __init__(self, content: Union[str, None]): - self.role = "assistant" - self.content = content - - def to_json(self): - return self.__dict__ - -class ChatCompletionChoice(Model): - def __init__(self, message: ChatCompletionMessage, finish_reason: str): - self.index = 0 - self.message = message - self.finish_reason = finish_reason - - def to_json(self): - return { - **self.__dict__, - "message": self.message.to_json() - } - -class ChatCompletionDelta(Model): - content: Union[str, None] = None - - def __init__(self, content: Union[str, None]): - if content is not None: - self.content = content - self.role = "assistant" - - def to_json(self): - return self.__dict__ - -class ChatCompletionDeltaChoice(Model): - def __init__(self, delta: ChatCompletionDelta, finish_reason: Union[str, None]): - self.index = 0 - self.delta = delta - self.finish_reason = finish_reason - - def to_json(self): - return { - **self.__dict__, - "delta": self.delta.to_json() - } - -class Image(Model): - def __init__(self, url: str = None, b64_json: str = None, revised_prompt: str = None) -> None: - if url is not None: - self.url = url - if b64_json is not None: - self.b64_json = b64_json - if revised_prompt is not None: - self.revised_prompt = revised_prompt - - def to_json(self): - return self.__dict__ - -class ImagesResponse(Model): + return super().model_construct( + id=f"chatcmpl-{completion_id}" if completion_id else None, + object="chat.completion", + created=created, + model=None, + provider=None, + choices=[ChatCompletionChoice.model_construct( + ChatCompletionMessage.model_construct(content), + finish_reason + )], + usage={ + "prompt_tokens": 0, #prompt_tokens, + "completion_tokens": 0, #completion_tokens, + "total_tokens": 0, #prompt_tokens + completion_tokens, + } + ) + +class ChatCompletionDelta(BaseModel): + role: str + content: str + + @classmethod + def model_construct(cls, content: Optional[str]): + return super().model_construct(role="assistant", content=content) + +class ChatCompletionDeltaChoice(BaseModel): + index: int + delta: ChatCompletionDelta + finish_reason: Optional[str] + + @classmethod + def model_construct(cls, delta: ChatCompletionDelta, finish_reason: Optional[str]): + return super().model_construct(index=0, delta=delta, finish_reason=finish_reason) + +class Image(BaseModel): + url: Optional[str] + b64_json: Optional[str] + revised_prompt: Optional[str] + + @classmethod + def model_construct(cls, url: str = None, b64_json: str = None, revised_prompt: str = None): + return super().model_construct(**filter_none( + url=url, + b64_json=b64_json, + revised_prompt=revised_prompt + )) + +class ImagesResponse(BaseModel): data: list[Image] model: str provider: str created: int - def __init__(self, data: list[Image], created: int = None, model: str = None, provider: str = None) -> None: - self.data = data + @classmethod + def model_construct(cls, data: list[Image], created: int = None, model: str = None, provider: str = None): if created is None: created = int(time()) - self.model = model - if provider is not None: - self.provider = provider - self.created = created - - def to_json(self): - return { - **self.__dict__, - "data": [image.to_json() for image in self.data] - } \ No newline at end of file + return super().model_construct( + data=data, + model=model, + provider=provider, + created=created + ) diff --git a/g4f/gui/__init__.py b/g4f/gui/__init__.py index 930a2aa063c..140711fae1b 100644 --- a/g4f/gui/__init__.py +++ b/g4f/gui/__init__.py @@ -8,22 +8,13 @@ except ImportError as e: import_error = e -def run_gui(host: str = '0.0.0.0', port: int = 8080, debug: bool = False) -> None: - if import_error is not None: - raise MissingRequirementsError(f'Install "gui" requirements | pip install -U g4f[gui]\n{import_error}') - - config = { - 'host' : host, - 'port' : port, - 'debug': debug - } - +def get_gui_app(): site = Website(app) for route in site.routes: app.add_url_rule( route, - view_func = site.routes[route]['function'], - methods = site.routes[route]['methods'], + view_func=site.routes[route]['function'], + methods=site.routes[route]['methods'], ) backend_api = Backend_Api(app) @@ -33,6 +24,19 @@ def run_gui(host: str = '0.0.0.0', port: int = 8080, debug: bool = False) -> Non view_func = backend_api.routes[route]['function'], methods = backend_api.routes[route]['methods'], ) + return app + +def run_gui(host: str = '0.0.0.0', port: int = 8080, debug: bool = False) -> None: + if import_error is not None: + raise MissingRequirementsError(f'Install "gui" requirements | pip install -U g4f[gui]\n{import_error}') + + config = { + 'host' : host, + 'port' : port, + 'debug': debug + } + + get_gui_app() print(f"Running on port {config['port']}") app.run(**config) diff --git a/g4f/gui/server/api.py b/g4f/gui/server/api.py index ecf7bc54678..0701210ddac 100644 --- a/g4f/gui/server/api.py +++ b/g4f/gui/server/api.py @@ -22,11 +22,11 @@ class Api: @staticmethod - def get_models() -> list[str]: + def get_models(): return models._all_models @staticmethod - def get_provider_models(provider: str, api_key: str = None) -> list[dict]: + def get_provider_models(provider: str, api_key: str = None): if provider in __map__: provider: ProviderType = __map__[provider] if issubclass(provider, ProviderModelMixin): @@ -46,39 +46,7 @@ def get_provider_models(provider: str, api_key: str = None) -> list[dict]: return [] @staticmethod - def get_image_models() -> list[dict]: - image_models = [] - index = [] - for provider in __providers__: - if hasattr(provider, "image_models"): - if hasattr(provider, "get_models"): - provider.get_models() - parent = provider - if hasattr(provider, "parent"): - parent = __map__[provider.parent] - if parent.__name__ not in index: - for model in provider.image_models: - image_models.append({ - "provider": parent.__name__, - "url": parent.url, - "label": parent.label if hasattr(parent, "label") else None, - "image_model": model, - "vision_model": getattr(parent, "default_vision_model", None) - }) - index.append(parent.__name__) - elif hasattr(provider, "default_vision_model") and provider.__name__ not in index: - image_models.append({ - "provider": provider.__name__, - "url": provider.url, - "label": provider.label if hasattr(provider, "label") else None, - "image_model": None, - "vision_model": provider.default_vision_model - }) - index.append(provider.__name__) - return image_models - - @staticmethod - def get_providers() -> list[str]: + def get_providers() -> dict[str, str]: return { provider.__name__: (provider.label if hasattr(provider, "label") else provider.__name__) + (" (Image Generation)" if getattr(provider, "image_models", None) else "") @@ -90,7 +58,7 @@ def get_providers() -> list[str]: } @staticmethod - def get_version(): + def get_version() -> dict: try: current_version = version.utils.current_version except VersionNotFoundError: diff --git a/g4f/gui/server/backend.py b/g4f/gui/server/backend.py index 3dcae546a82..3c87b0e282a 100644 --- a/g4f/gui/server/backend.py +++ b/g4f/gui/server/backend.py @@ -3,7 +3,7 @@ import os import logging import asyncio -from flask import request, Flask +from flask import Flask, request, jsonify from typing import Generator from werkzeug.utils import secure_filename @@ -42,17 +42,26 @@ def __init__(self, app: Flask) -> None: app (Flask): Flask application instance to attach routes to. """ self.app: Flask = app + + def jsonify_models(**kwargs): + response = self.get_models(**kwargs) + if isinstance(response, list): + return jsonify(response) + return response + + def jsonify_provider_models(**kwargs): + response = self.get_provider_models(**kwargs) + if isinstance(response, list): + return jsonify(response) + return response + self.routes = { '/backend-api/v2/models': { - 'function': self.get_models, + 'function': jsonify_models, 'methods': ['GET'] }, '/backend-api/v2/models/': { - 'function': self.get_provider_models, - 'methods': ['GET'] - }, - '/backend-api/v2/image_models': { - 'function': self.get_image_models, + 'function': jsonify_provider_models, 'methods': ['GET'] }, '/backend-api/v2/providers': { diff --git a/g4f/gui/server/website.py b/g4f/gui/server/website.py index 3cabcdf3e44..456056b1be5 100644 --- a/g4f/gui/server/website.py +++ b/g4f/gui/server/website.py @@ -1,11 +1,12 @@ import uuid from flask import render_template, redirect +def redirect_home(): + return redirect('/chat') + class Website: def __init__(self, app) -> None: self.app = app - def redirect_home(): - return redirect('/chat') self.routes = { '/': { 'function': redirect_home, @@ -35,7 +36,7 @@ def redirect_home(): def _chat(self, conversation_id): if '-' not in conversation_id: - return redirect('/chat') + return redirect_home() return render_template('index.html', chat_id=conversation_id) def _index(self): diff --git a/g4f/requests/raise_for_status.py b/g4f/requests/raise_for_status.py index 8625f552bb1..0cd09a2aca0 100644 --- a/g4f/requests/raise_for_status.py +++ b/g4f/requests/raise_for_status.py @@ -11,7 +11,7 @@ class CloudflareError(ResponseStatusError): ... def is_cloudflare(text: str) -> bool: - if "Generated by cloudfront" in text: + if "Generated by cloudfront" in text or '

' in text: return True elif "Attention Required! | Cloudflare" in text or 'id="cf-cloudflare-status"' in text: return True From 7d9a64013b6c4e03b7e403e87d55c95d9ac4c248 Mon Sep 17 00:00:00 2001 From: H Lohaus Date: Sun, 24 Nov 2024 18:42:00 +0100 Subject: [PATCH 2/3] Arm2 (#2415) * Fix arm v7 build / improve api * Update stubs.py * Fix unit tests * Fix arm build --- README.md | 12 ++++++------ docker/Dockerfile-slim | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4f61d79150d..f882b13cac0 100644 --- a/README.md +++ b/README.md @@ -105,23 +105,23 @@ docker run \ hlohaus789/g4f:latest ``` -Start the GUI without a browser requirement and in debug mode. -There's no need to update the Docker image every time. -Simply remove the g4f package from the image and install the Python package: +To run the slim docker image. Use this command: + ```bash docker run \ - -p 8080:8080 \ + -p 1337:1337 \ -v ${PWD}/har_and_cookies:/app/har_and_cookies \ -v ${PWD}/generated_images:/app/generated_images \ hlohaus789/g4f:latest-slim \ rm -r -f /app/g4f/ \ && pip install -U g4f[slim] \ - && python -m g4f.cli gui -d + && python -m g4f.cli api --gui --debug ``` +It also updates the `g4f` package at startup and installs any new required dependencies. 3. **Access the Client:** - - To use the included client, navigate to: [http://localhost:8080/chat/](http://localhost:8080/chat/) + - To use the included client, navigate to: [http://localhost:8080/chat/](http://localhost:8080/chat/) or [http://localhost:1337/chat/](http://localhost:1337/chat/) - Or set the API base for your client to: [http://localhost:1337/v1](http://localhost:1337/v1) 4. **(Optional) Provider Login:** diff --git a/docker/Dockerfile-slim b/docker/Dockerfile-slim index 6cbf2254615..d1199a2c7ec 100644 --- a/docker/Dockerfile-slim +++ b/docker/Dockerfile-slim @@ -19,7 +19,8 @@ RUN apt-get update && apt-get upgrade -y \ && useradd -rm -G sudo -u $G4F_USER_ID -g $G4F_USER_ID $G4F_USER \ && mkdir -p /var/log/supervisor \ && chown "${G4F_USER_ID}:${G4F_USER_ID}" /var/log/supervisor \ - && echo "${G4F_USER}:${G4F_USER}" | chpasswd + && echo "${G4F_USER}:${G4F_USER}" | chpasswd \ + && python -m pip install --upgrade pip USER $G4F_USER_ID WORKDIR $G4F_DIR @@ -35,15 +36,14 @@ COPY requirements-slim.txt $G4F_DIR RUN curl https://sh.rustup.rs -sSf | bash -s -- -y # Upgrade pip for the latest features and install the project's Python dependencies. -RUN python -m pip install --upgrade pip \ - && pip install --no-cache-dir \ +RUN pip install --no-cache-dir \ Cython==0.29.22 \ setuptools \ # Install PyDantic && pip install \ -vvv \ --no-cache-dir \ - --no-binary pydantic \ + --no-binary :all: \ --global-option=build_ext \ --global-option=-j8 \ pydantic==${PYDANTIC_VERSION} \ From aaedd5b3ffb850b7cdd9e702f315ac99f489dd73 Mon Sep 17 00:00:00 2001 From: H Lohaus Date: Sun, 24 Nov 2024 19:27:24 +0100 Subject: [PATCH 3/3] Update Dockerfile-slim --- docker/Dockerfile-slim | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile-slim b/docker/Dockerfile-slim index d1199a2c7ec..9e111cc839d 100644 --- a/docker/Dockerfile-slim +++ b/docker/Dockerfile-slim @@ -37,6 +37,7 @@ RUN curl https://sh.rustup.rs -sSf | bash -s -- -y # Upgrade pip for the latest features and install the project's Python dependencies. RUN pip install --no-cache-dir \ + --no-binary :all: \ Cython==0.29.22 \ setuptools \ # Install PyDantic