From 2114520ed2d5912b7027a99b013d27104e0f5079 Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Sun, 24 Nov 2024 22:20:08 +0100 Subject: [PATCH 1/4] Improve slim docker build, Add openapi.json to release --- .github/workflows/publish-workflow.yaml | 17 +++ docker/Dockerfile-slim | 8 +- etc/tool/openapi.py | 11 ++ g4f/api/__init__.py | 139 ++++++++++++++++++------ 4 files changed, 137 insertions(+), 38 deletions(-) create mode 100644 etc/tool/openapi.py diff --git a/.github/workflows/publish-workflow.yaml b/.github/workflows/publish-workflow.yaml index be479f66739..49ff03a57f8 100644 --- a/.github/workflows/publish-workflow.yaml +++ b/.github/workflows/publish-workflow.yaml @@ -6,6 +6,23 @@ on: - '**' jobs: + openapi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: "3.8" + cache: 'pip' + - name: Install requirements + run: pip install fastapi uvicorn python-multipart + - name: Generate openapi.json + run: | + python -m etc.tool.openapi + - uses: actions/upload-artifact@v4 + with: + path: openapi.json publish: runs-on: ubuntu-latest steps: diff --git a/docker/Dockerfile-slim b/docker/Dockerfile-slim index 9e111cc839d..0a09395be2a 100644 --- a/docker/Dockerfile-slim +++ b/docker/Dockerfile-slim @@ -30,14 +30,15 @@ ENV PATH "${HOME}/.local/bin:${HOME}/.cargo/bin:${PATH}" # Create app dir and copy the project's requirements file into it RUN mkdir -p $G4F_DIR +COPY requirements-min.txt $G4F_DIR COPY requirements-slim.txt $G4F_DIR # Install rust toolchain 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: \ +RUN pip install --no-cache-dir -r requirements-min.txt \ + && pip install --no-cache-dir --no-binary setuptools \ Cython==0.29.22 \ setuptools \ # Install PyDantic @@ -57,7 +58,8 @@ RUN pip install --no-cache-dir \ USER root # Clean up build deps -RUN rustup self uninstall -y \ +RUN rm --recursive --force "${HOME}/.rustup" \ + && rustup self uninstall -y \ && apt-get purge --auto-remove --yes \ build-essential \ && apt-get clean \ diff --git a/etc/tool/openapi.py b/etc/tool/openapi.py new file mode 100644 index 00000000000..83359e4e7eb --- /dev/null +++ b/etc/tool/openapi.py @@ -0,0 +1,11 @@ +import json + +from g4f.api import create_app + +app = create_app() + +with open("openapi.json", "w") as f: + data = json.dumps(app.openapi()) + f.write(data) + +print(f"openapi.json - {round(len(data)/1024, 2)} kbytes") \ No newline at end of file diff --git a/g4f/api/__init__.py b/g4f/api/__init__.py index a8403e5cddb..e8e979b389c 100644 --- a/g4f/api/__init__.py +++ b/g4f/api/__init__.py @@ -19,7 +19,8 @@ HTTP_422_UNPROCESSABLE_ENTITY, HTTP_404_NOT_FOUND, HTTP_401_UNAUTHORIZED, - HTTP_403_FORBIDDEN + HTTP_403_FORBIDDEN, + HTTP_500_INTERNAL_SERVER_ERROR, ) from fastapi.encoders import jsonable_encoder from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials @@ -35,7 +36,7 @@ from g4f.client.helper import filter_none from g4f.image import is_accepted_format, images_dir from g4f.typing import Messages -from g4f.errors import ProviderNotFoundError +from g4f.errors import ProviderNotFoundError, ModelNotFoundError, MissingAuthError from g4f.cookies import read_cookie_files, get_cookies_dir from g4f.Provider import ProviderType, ProviderUtils, __providers__ from g4f.gui import get_gui_app @@ -55,6 +56,15 @@ def create_app(g4f_api_key: str = None): ) api = Api(app, g4f_api_key=g4f_api_key) + + if AppConfig.gui: + @app.get("/") + async def home(): + return HTMLResponse(f'g4f v-{g4f.version.utils.current_version}:

' + 'Start to chat: /chat/
' + 'Open Swagger UI at: ' + '/docs') + api.register_routes() api.register_authorization() api.register_validation_exception_handler() @@ -98,11 +108,10 @@ class ProviderResponseModel(BaseModel): id: str object: str = "provider" created: int - owned_by: Optional[str] url: Optional[str] label: Optional[str] -class ProviderResponseModelDetail(ProviderResponseModel): +class ProviderResponseDetailModel(ProviderResponseModel): models: list[str] image_models: list[str] vision_models: list[str] @@ -115,7 +124,28 @@ class ModelResponseModel(BaseModel): owned_by: Optional[str] class ErrorResponseModel(BaseModel): - error: str + error: ErrorResponseMessageModel + model: Optional[str] = None + provider: Optional[str] = None + +class ErrorResponseMessageModel(BaseModel): + message: str + +class FileResponseModel(BaseModel): + filename: str + +class ErrorResponse(Response): + media_type = "application/json" + + @classmethod + def from_exception(cls, exception: Exception, + config: Union[ChatCompletionsConfig, ImageGenerationConfig] = None, + status_code: int = HTTP_500_INTERNAL_SERVER_ERROR): + return cls(format_exception(exception, config), status_code) + + @classmethod + def from_message(cls, message: str, status_code: int = HTTP_500_INTERNAL_SERVER_ERROR): + return cls(format_exception(message), status_code) class AppConfig: ignored_providers: Optional[list[str]] = None @@ -156,15 +186,9 @@ async def authorization(request: Request, call_next): user_g4f_api_key = await self.get_g4f_api_key(request) except HTTPException as e: if e.status_code == 403: - return JSONResponse( - status_code=HTTP_401_UNAUTHORIZED, - content=jsonable_encoder({"detail": "G4F API key required"}), - ) + return ErrorResponse("G4F API key required", HTTP_401_UNAUTHORIZED) if not secrets.compare_digest(self.g4f_api_key, user_g4f_api_key): - return JSONResponse( - status_code=HTTP_403_FORBIDDEN, - content=jsonable_encoder({"detail": "Invalid G4F API key"}), - ) + return ErrorResponse("Invalid G4F API key", HTTP_403_FORBIDDEN) return await call_next(request) def register_validation_exception_handler(self): @@ -197,8 +221,10 @@ async def read_root_v1(): 'Open Swagger UI at: ' '/docs') - @self.app.get("/v1/models") - async def models() -> list[ModelResponseModel]: + @self.app.get("/v1/models", responses={ + HTTP_200_OK: {"model": List[ModelResponseModel]}, + }) + async def models(): model_list = dict( (model, g4f.models.ModelUtils.convert[model]) for model in g4f.Model.__all__() @@ -210,7 +236,10 @@ async def models() -> list[ModelResponseModel]: 'owned_by': model.base_provider } for model_id, model in model_list.items()] - @self.app.get("/v1/models/{model_name}") + @self.app.get("/v1/models/{model_name}", responses={ + HTTP_200_OK: {"model": ModelResponseModel}, + HTTP_404_NOT_FOUND: {"model": ErrorResponseModel}, + }) async def model_info(model_name: str) -> ModelResponseModel: if model_name in g4f.models.ModelUtils.convert: model_info = g4f.models.ModelUtils.convert[model_name] @@ -220,9 +249,14 @@ async def model_info(model_name: str) -> ModelResponseModel: 'created': 0, 'owned_by': model_info.base_provider }) - return JSONResponse({"error": "The model does not exist."}, HTTP_404_NOT_FOUND) + return ErrorResponse("The model does not exist.", HTTP_404_NOT_FOUND) - @self.app.post("/v1/chat/completions", response_model=ChatCompletion) + @self.app.post("/v1/chat/completions", responses={ + HTTP_200_OK: {"model": ChatCompletion}, + HTTP_401_UNAUTHORIZED: {"model": ErrorResponseModel}, + HTTP_404_NOT_FOUND: {"model": ErrorResponseModel}, + HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel}, + }) async def chat_completions( config: ChatCompletionsConfig, credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None, @@ -282,12 +316,25 @@ async def streaming(): return StreamingResponse(streaming(), media_type="text/event-stream") + except (ModelNotFoundError, ProviderNotFoundError) as e: + logger.exception(e) + return ErrorResponse(e, HTTP_404_NOT_FOUND) + except MissingAuthError as e: + logger.exception(e) + return ErrorResponse(e, HTTP_401_UNAUTHORIZED) except Exception as e: logger.exception(e) - return Response(content=format_exception(e, config), status_code=500, media_type="application/json") + return ErrorResponse(e, HTTP_500_INTERNAL_SERVER_ERROR) - @self.app.post("/v1/images/generate", response_model=ImagesResponse) - @self.app.post("/v1/images/generations", response_model=ImagesResponse) + responses = { + HTTP_200_OK: {"model": ImagesResponse}, + HTTP_401_UNAUTHORIZED: {"model": ErrorResponseModel}, + HTTP_404_NOT_FOUND: {"model": ErrorResponseModel}, + HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel}, + } + + @self.app.post("/v1/images/generate", responses=responses) + @self.app.post("/v1/images/generations", responses=responses) async def generate_image( request: Request, config: ImageGenerationConfig, @@ -310,12 +357,20 @@ async def generate_image( if hasattr(image, "url") and image.url.startswith("/"): image.url = f"{request.base_url}{image.url.lstrip('/')}" return response + except (ModelNotFoundError, ProviderNotFoundError) as e: + logger.exception(e) + return ErrorResponse(e, HTTP_404_NOT_FOUND) + except MissingAuthError as e: + logger.exception(e) + return ErrorResponse(e, HTTP_401_UNAUTHORIZED) except Exception as e: logger.exception(e) - return Response(content=format_exception(e, config, True), status_code=500, media_type="application/json") + return ErrorResponse(e, HTTP_500_INTERNAL_SERVER_ERROR) - @self.app.get("/v1/providers") - async def providers() -> list[ProviderResponseModel]: + @self.app.get("/v1/providers", responses={ + HTTP_200_OK: {"model": List[ProviderResponseModel]}, + }) + async def providers(): return [{ 'id': provider.__name__, 'object': 'provider', @@ -324,10 +379,13 @@ async def providers() -> list[ProviderResponseModel]: 'label': getattr(provider, "label", None), } for provider in __providers__ if provider.working] - @self.app.get("/v1/providers/{provider}") - async def providers_info(provider: str) -> ProviderResponseModelDetail: + @self.app.get("/v1/providers/{provider}", responses={ + HTTP_200_OK: {"model": ProviderResponseDetailModel}, + HTTP_404_NOT_FOUND: {"model": ErrorResponseModel}, + }) + async def providers_info(provider: str): if provider not in ProviderUtils.convert: - return JSONResponse({"error": "The provider does not exist."}, 404) + return ErrorResponse.from_message("The provider does not exist.", 404) provider: ProviderType = ProviderUtils.convert[provider] def safe_get_models(provider: ProviderType) -> list[str]: try: @@ -346,7 +404,9 @@ def safe_get_models(provider: ProviderType) -> list[str]: 'params': [*provider.get_parameters()] if hasattr(provider, "get_parameters") else [] } - @self.app.post("/v1/upload_cookies") + @self.app.post("/v1/upload_cookies", responses={ + HTTP_200_OK: {"model": List[FileResponseModel]}, + }) def upload_cookies(files: List[UploadFile]): response_data = [] for file in files: @@ -369,11 +429,11 @@ async def synthesize(request: Request, provider: str): try: provider_handler = convert_to_provider(provider) except ProviderNotFoundError: - return JSONResponse({"error": "Provider not found"}, HTTP_404_NOT_FOUND) + return ErrorResponse("Provider not found", HTTP_404_NOT_FOUND) if not hasattr(provider_handler, "synthesize"): - return JSONResponse({"error": "Provider doesn't support synthesize"}, HTTP_404_NOT_FOUND) + return ErrorResponse("Provider doesn't support synthesize", HTTP_404_NOT_FOUND) if len(request.query_params) == 0: - return JSONResponse({"error": "Missing query params"}, HTTP_422_UNPROCESSABLE_ENTITY) + return ErrorResponse("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) @@ -395,12 +455,21 @@ async def get_image(filename): -def format_exception(e: Exception, config: Union[ChatCompletionsConfig, ImageGenerationConfig], image: bool = False) -> str: +def format_exception(e: Union[Exception, str], config: Union[ChatCompletionsConfig, ImageGenerationConfig] = None, image: bool = False) -> str: last_provider = {} if not image else g4f.get_last_provider(True) - provider = (AppConfig.image_provider if image else AppConfig.provider) if config.provider is None else config.provider - model = AppConfig.model if config.model is None else config.model + provider = (AppConfig.image_provider if image else AppConfig.provider) + model = AppConfig.model + if config is not None: + if config.provider is not None: + provider = config.provider + if config.model is not None: + model = config.model + if isinstance(e, str): + message = e + else: + message = f"{e.__class__.__name__}: {e}" return json.dumps({ - "error": {"message": f"{e.__class__.__name__}: {e}"}, + "error": {"message": message}, "model": last_provider.get("model") if model is None else model, **filter_none( provider=last_provider.get("name") if provider is None else provider From c57321e2873c533a4f5d1cd5b278e3a8ca108656 Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Sun, 24 Nov 2024 23:34:59 +0100 Subject: [PATCH 2/4] Improve error handling in api, Update openapi.json workflow --- .github/workflows/publish-workflow.yaml | 4 ++- docker/Dockerfile-slim | 44 ++++--------------------- g4f/api/__init__.py | 29 ++++++++-------- 3 files changed, 25 insertions(+), 52 deletions(-) diff --git a/.github/workflows/publish-workflow.yaml b/.github/workflows/publish-workflow.yaml index 49ff03a57f8..9ad68bd8aff 100644 --- a/.github/workflows/publish-workflow.yaml +++ b/.github/workflows/publish-workflow.yaml @@ -16,7 +16,9 @@ jobs: python-version: "3.8" cache: 'pip' - name: Install requirements - run: pip install fastapi uvicorn python-multipart + run: | + pip install fastapi uvicorn python-multipart + pip install -r requirements-min.txt - name: Generate openapi.json run: | python -m etc.tool.openapi diff --git a/docker/Dockerfile-slim b/docker/Dockerfile-slim index 0a09395be2a..0423814481f 100644 --- a/docker/Dockerfile-slim +++ b/docker/Dockerfile-slim @@ -1,9 +1,8 @@ -FROM python:bookworm +FROM python:slim-bookworm ARG G4F_VERSION ARG G4F_USER=g4f ARG G4F_USER_ID=1000 -ARG PYDANTIC_VERSION=1.8.1 ENV G4F_VERSION $G4F_VERSION ENV G4F_USER $G4F_USER @@ -12,60 +11,29 @@ ENV G4F_DIR /app RUN apt-get update && apt-get upgrade -y \ && apt-get install -y git \ - && apt-get install --quiet --yes --no-install-recommends \ - build-essential \ # Add user and user group && groupadd -g $G4F_USER_ID $G4F_USER \ && 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 \ - && python -m pip install --upgrade pip + && python -m pip install --upgrade pip \ + && apt-get clean \ + && rm --recursive --force /var/lib/apt/lists/* /tmp/* /var/tmp/* USER $G4F_USER_ID WORKDIR $G4F_DIR ENV HOME /home/$G4F_USER -ENV PATH "${HOME}/.local/bin:${HOME}/.cargo/bin:${PATH}" +ENV PATH "${HOME}/.local/bin:${PATH}" # Create app dir and copy the project's requirements file into it RUN mkdir -p $G4F_DIR COPY requirements-min.txt $G4F_DIR COPY requirements-slim.txt $G4F_DIR -# Install rust toolchain -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 -r requirements-min.txt \ - && pip install --no-cache-dir --no-binary setuptools \ - Cython==0.29.22 \ - setuptools \ - # Install PyDantic - && pip install \ - -vvv \ - --no-cache-dir \ - --no-binary :all: \ - --global-option=build_ext \ - --global-option=-j8 \ - pydantic==${PYDANTIC_VERSION} \ - && cat requirements-slim.txt | xargs -n 1 pip install --no-cache-dir || true \ - # Remove build packages - && pip uninstall --yes \ - Cython \ - setuptools - -USER root - -# Clean up build deps -RUN rm --recursive --force "${HOME}/.rustup" \ - && rustup self uninstall -y \ - && apt-get purge --auto-remove --yes \ - build-essential \ - && apt-get clean \ - && rm --recursive --force /var/lib/apt/lists/* /tmp/* /var/tmp/* - -USER $G4F_USER_ID +RUN cat requirements-slim.txt | xargs -n 1 pip install --no-cache-dir || true # Copy the entire package into the container. ADD --chown=$G4F_USER:$G4F_USER g4f $G4F_DIR/g4f \ No newline at end of file diff --git a/g4f/api/__init__.py b/g4f/api/__init__.py index e8e979b389c..2f34fa6aac8 100644 --- a/g4f/api/__init__.py +++ b/g4f/api/__init__.py @@ -147,6 +147,9 @@ def from_exception(cls, exception: Exception, def from_message(cls, message: str, status_code: int = HTTP_500_INTERNAL_SERVER_ERROR): return cls(format_exception(message), status_code) + def render(self, content) -> bytes: + return str(content).encode(errors="ignore") + class AppConfig: ignored_providers: Optional[list[str]] = None g4f_api_key: Optional[str] = None @@ -186,9 +189,9 @@ async def authorization(request: Request, call_next): user_g4f_api_key = await self.get_g4f_api_key(request) except HTTPException as e: if e.status_code == 403: - return ErrorResponse("G4F API key required", HTTP_401_UNAUTHORIZED) + return ErrorResponse.from_message("G4F API key required", HTTP_401_UNAUTHORIZED) if not secrets.compare_digest(self.g4f_api_key, user_g4f_api_key): - return ErrorResponse("Invalid G4F API key", HTTP_403_FORBIDDEN) + return ErrorResponse.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN) return await call_next(request) def register_validation_exception_handler(self): @@ -249,7 +252,7 @@ async def model_info(model_name: str) -> ModelResponseModel: 'created': 0, 'owned_by': model_info.base_provider }) - return ErrorResponse("The model does not exist.", HTTP_404_NOT_FOUND) + return ErrorResponse.from_message("The model does not exist.", HTTP_404_NOT_FOUND) @self.app.post("/v1/chat/completions", responses={ HTTP_200_OK: {"model": ChatCompletion}, @@ -318,13 +321,13 @@ async def streaming(): except (ModelNotFoundError, ProviderNotFoundError) as e: logger.exception(e) - return ErrorResponse(e, HTTP_404_NOT_FOUND) + return ErrorResponse.from_exception(e, config, HTTP_404_NOT_FOUND) except MissingAuthError as e: logger.exception(e) - return ErrorResponse(e, HTTP_401_UNAUTHORIZED) + return ErrorResponse.from_exception(e, config, HTTP_401_UNAUTHORIZED) except Exception as e: logger.exception(e) - return ErrorResponse(e, HTTP_500_INTERNAL_SERVER_ERROR) + return ErrorResponse.from_exception(e, config, HTTP_500_INTERNAL_SERVER_ERROR) responses = { HTTP_200_OK: {"model": ImagesResponse}, @@ -359,13 +362,13 @@ async def generate_image( return response except (ModelNotFoundError, ProviderNotFoundError) as e: logger.exception(e) - return ErrorResponse(e, HTTP_404_NOT_FOUND) + return ErrorResponse.from_exception(e, config, HTTP_404_NOT_FOUND) except MissingAuthError as e: logger.exception(e) - return ErrorResponse(e, HTTP_401_UNAUTHORIZED) + return ErrorResponse.from_exception(e, config, HTTP_401_UNAUTHORIZED) except Exception as e: logger.exception(e) - return ErrorResponse(e, HTTP_500_INTERNAL_SERVER_ERROR) + return ErrorResponse.from_exception(e, config, HTTP_500_INTERNAL_SERVER_ERROR) @self.app.get("/v1/providers", responses={ HTTP_200_OK: {"model": List[ProviderResponseModel]}, @@ -428,12 +431,12 @@ def upload_cookies(files: List[UploadFile]): async def synthesize(request: Request, provider: str): try: provider_handler = convert_to_provider(provider) - except ProviderNotFoundError: - return ErrorResponse("Provider not found", HTTP_404_NOT_FOUND) + except ProviderNotFoundError as e: + return ErrorResponse.from_exception(e, status_code=HTTP_404_NOT_FOUND) if not hasattr(provider_handler, "synthesize"): - return ErrorResponse("Provider doesn't support synthesize", HTTP_404_NOT_FOUND) + return ErrorResponse.from_message("Provider doesn't support synthesize", HTTP_404_NOT_FOUND) if len(request.query_params) == 0: - return ErrorResponse("Missing query params", HTTP_422_UNPROCESSABLE_ENTITY) + return ErrorResponse.from_message("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) From 1b7f89f69aba6f5639ec245836c771f05e6c07df Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Mon, 25 Nov 2024 00:36:38 +0100 Subject: [PATCH 3/4] Fix typing issue --- g4f/client/stubs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/g4f/client/stubs.py b/g4f/client/stubs.py index 8e54e419f45..7367ac75d42 100644 --- a/g4f/client/stubs.py +++ b/g4f/client/stubs.py @@ -133,13 +133,13 @@ def model_construct(cls, url: str = None, b64_json: str = None, revised_prompt: )) class ImagesResponse(BaseModel): - data: list[Image] + data: List[Image] model: str provider: str created: int @classmethod - def model_construct(cls, data: list[Image], created: int = None, model: str = None, provider: str = None): + def model_construct(cls, data: List[Image], created: int = None, model: str = None, provider: str = None): if created is None: created = int(time()) return super().model_construct( From 8153668ab877ee23c5dc05351f5f61be2d497956 Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Mon, 25 Nov 2024 01:24:03 +0100 Subject: [PATCH 4/4] Fix import Annotated typing --- .github/workflows/publish-workflow.yaml | 4 ++-- g4f/api/__init__.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-workflow.yaml b/.github/workflows/publish-workflow.yaml index 9ad68bd8aff..82d21cb701d 100644 --- a/.github/workflows/publish-workflow.yaml +++ b/.github/workflows/publish-workflow.yaml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python 3.8 + - name: Set up Python 3.13 uses: actions/setup-python@v4 with: - python-version: "3.8" + python-version: "3.13" cache: 'pip' - name: Install requirements run: | diff --git a/g4f/api/__init__.py b/g4f/api/__init__.py index 2f34fa6aac8..fc8a4339664 100644 --- a/g4f/api/__init__.py +++ b/g4f/api/__init__.py @@ -27,7 +27,12 @@ from fastapi.middleware.cors import CORSMiddleware from starlette.responses import FileResponse from pydantic import BaseModel, Field -from typing import Union, Optional, List, Annotated +from typing import Union, Optional, List +try: + from typing import Annotated +except ImportError: + class Annotated: + pass import g4f import g4f.debug @@ -456,8 +461,6 @@ async def get_image(filename): return FileResponse(target, media_type=content_type) - - def format_exception(e: Union[Exception, str], config: Union[ChatCompletionsConfig, ImageGenerationConfig] = None, image: bool = False) -> str: last_provider = {} if not image else g4f.get_last_provider(True) provider = (AppConfig.image_provider if image else AppConfig.provider)