diff --git a/.github/workflows/publish-workflow.yaml b/.github/workflows/publish-workflow.yaml
index be479f66739..82d21cb701d 100644
--- a/.github/workflows/publish-workflow.yaml
+++ b/.github/workflows/publish-workflow.yaml
@@ -6,6 +6,25 @@ on:
- '**'
jobs:
+ openapi:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python 3.13
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.13"
+ cache: 'pip'
+ - name: Install requirements
+ run: |
+ pip install fastapi uvicorn python-multipart
+ pip install -r requirements-min.txt
+ - 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..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,58 +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 \
- --no-binary :all: \
- 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 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/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..fc8a4339664 100644
--- a/g4f/api/__init__.py
+++ b/g4f/api/__init__.py
@@ -19,14 +19,20 @@
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
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
@@ -35,7 +41,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 +61,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 +113,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 +129,31 @@ 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)
+
+ def render(self, content) -> bytes:
+ return str(content).encode(errors="ignore")
class AppConfig:
ignored_providers: Optional[list[str]] = None
@@ -156,15 +194,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.from_message("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.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN)
return await call_next(request)
def register_validation_exception_handler(self):
@@ -197,8 +229,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 +244,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 +257,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.from_message("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 +324,25 @@ async def streaming():
return StreamingResponse(streaming(), media_type="text/event-stream")
+ except (ModelNotFoundError, ProviderNotFoundError) as e:
+ logger.exception(e)
+ return ErrorResponse.from_exception(e, config, HTTP_404_NOT_FOUND)
+ except MissingAuthError as e:
+ logger.exception(e)
+ return ErrorResponse.from_exception(e, config, 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.from_exception(e, config, HTTP_500_INTERNAL_SERVER_ERROR)
+
+ 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", response_model=ImagesResponse)
- @self.app.post("/v1/images/generations", response_model=ImagesResponse)
+ @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 +365,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.from_exception(e, config, HTTP_404_NOT_FOUND)
+ except MissingAuthError as e:
+ logger.exception(e)
+ return ErrorResponse.from_exception(e, config, 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.from_exception(e, config, 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 +387,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 +412,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:
@@ -368,12 +436,12 @@ def upload_cookies(files: List[UploadFile]):
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)
+ except ProviderNotFoundError as e:
+ return ErrorResponse.from_exception(e, status_code=HTTP_404_NOT_FOUND)
if not hasattr(provider_handler, "synthesize"):
- return JSONResponse({"error": "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 JSONResponse({"error": "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)
@@ -393,14 +461,21 @@ async def get_image(filename):
return FileResponse(target, media_type=content_type)
-
-
-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
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(