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

XFEstudio/gpt4free

Improve slim docker build, Add openapi.json to release

2114520e
Heiner Lohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

4 个文件 +137 -38
Modified .github/workflows/publish-workflow.yaml +17 -0
@@ -6,6 +6,23 @@ on:
6 6 - '**'
7 7
8 8 jobs:
9 openapi:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v4
13 - name: Set up Python 3.8
14 uses: actions/setup-python@v4
15 with:
16 python-version: "3.8"
17 cache: 'pip'
18 - name: Install requirements
19 run: pip install fastapi uvicorn python-multipart
20 - name: Generate openapi.json
21 run: |
22 python -m etc.tool.openapi
23 - uses: actions/upload-artifact@v4
24 with:
25 path: openapi.json
9 26 publish:
10 27 runs-on: ubuntu-latest
11 28 steps:
Modified docker/Dockerfile-slim +5 -3
@@ -30,14 +30,15 @@ ENV PATH "${HOME}/.local/bin:${HOME}/.cargo/bin:${PATH}"
30 30
31 31 # Create app dir and copy the project's requirements file into it
32 32 RUN mkdir -p $G4F_DIR
33 COPY requirements-min.txt $G4F_DIR
33 34 COPY requirements-slim.txt $G4F_DIR
34 35
35 36 # Install rust toolchain
36 37 RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
37 38
38 39 # Upgrade pip for the latest features and install the project's Python dependencies.
39 RUN pip install --no-cache-dir \
40 --no-binary :all: \
40 RUN pip install --no-cache-dir -r requirements-min.txt \
41 && pip install --no-cache-dir --no-binary setuptools \
41 42 Cython==0.29.22 \
42 43 setuptools \
43 44 # Install PyDantic
@@ -57,7 +58,8 @@ RUN pip install --no-cache-dir \
57 58 USER root
58 59
59 60 # Clean up build deps
60 RUN rustup self uninstall -y \
61 RUN rm --recursive --force "${HOME}/.rustup" \
62 && rustup self uninstall -y \
61 63 && apt-get purge --auto-remove --yes \
62 64 build-essential \
63 65 && apt-get clean \
Added etc/tool/openapi.py +11 -0
@@ -0,0 +1,11 @@
1 import json
2
3 from g4f.api import create_app
4
5 app = create_app()
6
7 with open("openapi.json", "w") as f:
8 data = json.dumps(app.openapi())
9 f.write(data)
10
11 print(f"openapi.json - {round(len(data)/1024, 2)} kbytes")
Modified g4f/api/__init__.py +104 -35
@@ -19,7 +19,8 @@ from starlette.status import (
19 19 HTTP_422_UNPROCESSABLE_ENTITY,
20 20 HTTP_404_NOT_FOUND,
21 21 HTTP_401_UNAUTHORIZED,
22 HTTP_403_FORBIDDEN
22 HTTP_403_FORBIDDEN,
23 HTTP_500_INTERNAL_SERVER_ERROR,
23 24 )
24 25 from fastapi.encoders import jsonable_encoder
25 26 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
@@ -35,7 +36,7 @@ from g4f.providers.response import BaseConversation
35 36 from g4f.client.helper import filter_none
36 37 from g4f.image import is_accepted_format, images_dir
37 38 from g4f.typing import Messages
38 from g4f.errors import ProviderNotFoundError
39 from g4f.errors import ProviderNotFoundError, ModelNotFoundError, MissingAuthError
39 40 from g4f.cookies import read_cookie_files, get_cookies_dir
40 41 from g4f.Provider import ProviderType, ProviderUtils, __providers__
41 42 from g4f.gui import get_gui_app
@@ -55,6 +56,15 @@ def create_app(g4f_api_key: str = None):
55 56 )
56 57
57 58 api = Api(app, g4f_api_key=g4f_api_key)
59
60 if AppConfig.gui:
61 @app.get("/")
62 async def home():
63 return HTMLResponse(f'g4f v-{g4f.version.utils.current_version}:<br><br>'
64 'Start to chat: <a href="/chat/">/chat/</a><br>'
65 'Open Swagger UI at: '
66 '<a href="/docs">/docs</a>')
67
58 68 api.register_routes()
59 69 api.register_authorization()
60 70 api.register_validation_exception_handler()
@@ -98,11 +108,10 @@ class ProviderResponseModel(BaseModel):
98 108 id: str
99 109 object: str = "provider"
100 110 created: int
101 owned_by: Optional[str]
102 111 url: Optional[str]
103 112 label: Optional[str]
104 113
105 class ProviderResponseModelDetail(ProviderResponseModel):
114 class ProviderResponseDetailModel(ProviderResponseModel):
106 115 models: list[str]
107 116 image_models: list[str]
108 117 vision_models: list[str]
@@ -115,7 +124,28 @@ class ModelResponseModel(BaseModel):
115 124 owned_by: Optional[str]
116 125
117 126 class ErrorResponseModel(BaseModel):
118 error: str
127 error: ErrorResponseMessageModel
128 model: Optional[str] = None
129 provider: Optional[str] = None
130
131 class ErrorResponseMessageModel(BaseModel):
132 message: str
133
134 class FileResponseModel(BaseModel):
135 filename: str
136
137 class ErrorResponse(Response):
138 media_type = "application/json"
139
140 @classmethod
141 def from_exception(cls, exception: Exception,
142 config: Union[ChatCompletionsConfig, ImageGenerationConfig] = None,
143 status_code: int = HTTP_500_INTERNAL_SERVER_ERROR):
144 return cls(format_exception(exception, config), status_code)
145
146 @classmethod
147 def from_message(cls, message: str, status_code: int = HTTP_500_INTERNAL_SERVER_ERROR):
148 return cls(format_exception(message), status_code)
119 149
120 150 class AppConfig:
121 151 ignored_providers: Optional[list[str]] = None
@@ -156,15 +186,9 @@ class Api:
156 186 user_g4f_api_key = await self.get_g4f_api_key(request)
157 187 except HTTPException as e:
158 188 if e.status_code == 403:
159 return JSONResponse(
160 status_code=HTTP_401_UNAUTHORIZED,
161 content=jsonable_encoder({"detail": "G4F API key required"}),
162 )
189 return ErrorResponse("G4F API key required", HTTP_401_UNAUTHORIZED)
163 190 if not secrets.compare_digest(self.g4f_api_key, user_g4f_api_key):
164 return JSONResponse(
165 status_code=HTTP_403_FORBIDDEN,
166 content=jsonable_encoder({"detail": "Invalid G4F API key"}),
167 )
191 return ErrorResponse("Invalid G4F API key", HTTP_403_FORBIDDEN)
168 192 return await call_next(request)
169 193
170 194 def register_validation_exception_handler(self):
@@ -197,8 +221,10 @@ class Api:
197 221 'Open Swagger UI at: '
198 222 '<a href="/docs">/docs</a>')
199 223
200 @self.app.get("/v1/models")
201 async def models() -> list[ModelResponseModel]:
224 @self.app.get("/v1/models", responses={
225 HTTP_200_OK: {"model": List[ModelResponseModel]},
226 })
227 async def models():
202 228 model_list = dict(
203 229 (model, g4f.models.ModelUtils.convert[model])
204 230 for model in g4f.Model.__all__()
@@ -210,7 +236,10 @@ class Api:
210 236 'owned_by': model.base_provider
211 237 } for model_id, model in model_list.items()]
212 238
213 @self.app.get("/v1/models/{model_name}")
239 @self.app.get("/v1/models/{model_name}", responses={
240 HTTP_200_OK: {"model": ModelResponseModel},
241 HTTP_404_NOT_FOUND: {"model": ErrorResponseModel},
242 })
214 243 async def model_info(model_name: str) -> ModelResponseModel:
215 244 if model_name in g4f.models.ModelUtils.convert:
216 245 model_info = g4f.models.ModelUtils.convert[model_name]
@@ -220,9 +249,14 @@ class Api:
220 249 'created': 0,
221 250 'owned_by': model_info.base_provider
222 251 })
223 return JSONResponse({"error": "The model does not exist."}, HTTP_404_NOT_FOUND)
252 return ErrorResponse("The model does not exist.", HTTP_404_NOT_FOUND)
224 253
225 @self.app.post("/v1/chat/completions", response_model=ChatCompletion)
254 @self.app.post("/v1/chat/completions", responses={
255 HTTP_200_OK: {"model": ChatCompletion},
256 HTTP_401_UNAUTHORIZED: {"model": ErrorResponseModel},
257 HTTP_404_NOT_FOUND: {"model": ErrorResponseModel},
258 HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
259 })
226 260 async def chat_completions(
227 261 config: ChatCompletionsConfig,
228 262 credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None,
@@ -282,12 +316,25 @@ class Api:
282 316
283 317 return StreamingResponse(streaming(), media_type="text/event-stream")
284 318
319 except (ModelNotFoundError, ProviderNotFoundError) as e:
320 logger.exception(e)
321 return ErrorResponse(e, HTTP_404_NOT_FOUND)
322 except MissingAuthError as e:
323 logger.exception(e)
324 return ErrorResponse(e, HTTP_401_UNAUTHORIZED)
285 325 except Exception as e:
286 326 logger.exception(e)
287 return Response(content=format_exception(e, config), status_code=500, media_type="application/json")
327 return ErrorResponse(e, HTTP_500_INTERNAL_SERVER_ERROR)
288 328
289 @self.app.post("/v1/images/generate", response_model=ImagesResponse)
290 @self.app.post("/v1/images/generations", response_model=ImagesResponse)
329 responses = {
330 HTTP_200_OK: {"model": ImagesResponse},
331 HTTP_401_UNAUTHORIZED: {"model": ErrorResponseModel},
332 HTTP_404_NOT_FOUND: {"model": ErrorResponseModel},
333 HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
334 }
335
336 @self.app.post("/v1/images/generate", responses=responses)
337 @self.app.post("/v1/images/generations", responses=responses)
291 338 async def generate_image(
292 339 request: Request,
293 340 config: ImageGenerationConfig,
@@ -310,12 +357,20 @@ class Api:
310 357 if hasattr(image, "url") and image.url.startswith("/"):
311 358 image.url = f"{request.base_url}{image.url.lstrip('/')}"
312 359 return response
360 except (ModelNotFoundError, ProviderNotFoundError) as e:
361 logger.exception(e)
362 return ErrorResponse(e, HTTP_404_NOT_FOUND)
363 except MissingAuthError as e:
364 logger.exception(e)
365 return ErrorResponse(e, HTTP_401_UNAUTHORIZED)
313 366 except Exception as e:
314 367 logger.exception(e)
315 return Response(content=format_exception(e, config, True), status_code=500, media_type="application/json")
368 return ErrorResponse(e, HTTP_500_INTERNAL_SERVER_ERROR)
316 369
317 @self.app.get("/v1/providers")
318 async def providers() -> list[ProviderResponseModel]:
370 @self.app.get("/v1/providers", responses={
371 HTTP_200_OK: {"model": List[ProviderResponseModel]},
372 })
373 async def providers():
319 374 return [{
320 375 'id': provider.__name__,
321 376 'object': 'provider',
@@ -324,10 +379,13 @@ class Api:
324 379 'label': getattr(provider, "label", None),
325 380 } for provider in __providers__ if provider.working]
326 381
327 @self.app.get("/v1/providers/{provider}")
328 async def providers_info(provider: str) -> ProviderResponseModelDetail:
382 @self.app.get("/v1/providers/{provider}", responses={
383 HTTP_200_OK: {"model": ProviderResponseDetailModel},
384 HTTP_404_NOT_FOUND: {"model": ErrorResponseModel},
385 })
386 async def providers_info(provider: str):
329 387 if provider not in ProviderUtils.convert:
330 return JSONResponse({"error": "The provider does not exist."}, 404)
388 return ErrorResponse.from_message("The provider does not exist.", 404)
331 389 provider: ProviderType = ProviderUtils.convert[provider]
332 390 def safe_get_models(provider: ProviderType) -> list[str]:
333 391 try:
@@ -346,7 +404,9 @@ class Api:
346 404 'params': [*provider.get_parameters()] if hasattr(provider, "get_parameters") else []
347 405 }
348 406
349 @self.app.post("/v1/upload_cookies")
407 @self.app.post("/v1/upload_cookies", responses={
408 HTTP_200_OK: {"model": List[FileResponseModel]},
409 })
350 410 def upload_cookies(files: List[UploadFile]):
351 411 response_data = []
352 412 for file in files:
@@ -369,11 +429,11 @@ class Api:
369 429 try:
370 430 provider_handler = convert_to_provider(provider)
371 431 except ProviderNotFoundError:
372 return JSONResponse({"error": "Provider not found"}, HTTP_404_NOT_FOUND)
432 return ErrorResponse("Provider not found", HTTP_404_NOT_FOUND)
373 433 if not hasattr(provider_handler, "synthesize"):
374 return JSONResponse({"error": "Provider doesn't support synthesize"}, HTTP_404_NOT_FOUND)
434 return ErrorResponse("Provider doesn't support synthesize", HTTP_404_NOT_FOUND)
375 435 if len(request.query_params) == 0:
376 return JSONResponse({"error": "Missing query params"}, HTTP_422_UNPROCESSABLE_ENTITY)
436 return ErrorResponse("Missing query params", HTTP_422_UNPROCESSABLE_ENTITY)
377 437 response_data = provider_handler.synthesize({**request.query_params})
378 438 content_type = getattr(provider_handler, "synthesize_content_type", "application/octet-stream")
379 439 return StreamingResponse(response_data, media_type=content_type)
@@ -395,12 +455,21 @@ class Api:
395 455
396 456
397 457
398 def format_exception(e: Exception, config: Union[ChatCompletionsConfig, ImageGenerationConfig], image: bool = False) -> str:
458 def format_exception(e: Union[Exception, str], config: Union[ChatCompletionsConfig, ImageGenerationConfig] = None, image: bool = False) -> str:
399 459 last_provider = {} if not image else g4f.get_last_provider(True)
400 provider = (AppConfig.image_provider if image else AppConfig.provider) if config.provider is None else config.provider
401 model = AppConfig.model if config.model is None else config.model
460 provider = (AppConfig.image_provider if image else AppConfig.provider)
461 model = AppConfig.model
462 if config is not None:
463 if config.provider is not None:
464 provider = config.provider
465 if config.model is not None:
466 model = config.model
467 if isinstance(e, str):
468 message = e
469 else:
470 message = f"{e.__class__.__name__}: {e}"
402 471 return json.dumps({
403 "error": {"message": f"{e.__class__.__name__}: {e}"},
472 "error": {"message": message},
404 473 "model": last_provider.get("model") if model is None else model,
405 474 **filter_none(
406 475 provider=last_provider.get("name") if provider is None else provider