返回提交历史
Modified
.github/workflows/publish-workflow.yaml
+17
-0
Modified
docker/Dockerfile-slim
+5
-3
Added
etc/tool/openapi.py
+11
-0
Modified
g4f/api/__init__.py
+104
-35
XFEstudio/gpt4free
Improve slim docker build, Add openapi.json to release
2114520e
代码差异
4 个文件
+137
-38
@@ -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:
@@ -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 \
@@ -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")
@@ -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