返回提交历史
Modified
g4f/api/__init__.py
+22
-102
Modified
g4f/client/__init__.py
+16
-0
Modified
g4f/client/factory.py
+3
-1
XFEstudio/gpt4free
Enhance API routing by allowing path parameters for provider endpoints and improve response handling with headers in iter_response functions
193a481c
代码差异
3 个文件
+41
-103
@@ -606,7 +606,7 @@ class Api:
606
606
]
607
607
}
608
608
609
@self.app.get("/api/{provider}/models", responses={
609
@self.app.get("/api/{provider:path}/models", responses={
610
610
HTTP_200_OK: {"model": List[ModelResponseModel]},
611
611
})
612
612
async def models(provider: str, credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None):
@@ -638,7 +638,7 @@ class Api:
638
638
}
639
639
640
640
# quota endpoint mimics backend-api/v2/quota but exposed on public API
641
@self.app.get("/api/{provider}/quota")
641
@self.app.get("/api/{provider:path}/quota")
642
642
async def provider_quota(provider: str, credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None):
643
643
try:
644
644
provider = AbstractClientFactory.create_provider(None, provider)
@@ -685,7 +685,7 @@ class Api:
685
685
HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
686
686
}
687
687
@self.app.post("/v1/chat/completions", responses=responses)
688
@self.app.post("/api/{provider}/chat/completions", responses=responses)
688
@self.app.post("/api/{provider:path}/chat/completions", responses=responses)
689
689
@self.app.post("/api/{provider}/{conversation_id}/chat/completions", responses=responses)
690
690
async def chat_completions(
691
691
config: ChatCompletionsConfig,
@@ -755,9 +755,16 @@ class Api:
755
755
)
756
756
757
757
if not config.stream:
758
return await response
759
758
result = await response
759
return Response(
760
content=result.model_dump_json() if hasattr(result, "model_dump_json") else result.json(),
761
media_type="application/json",
762
headers=getattr(result, "_headers").get_dict() if hasattr(result, "_headers") else None
763
)
764
765
first_chunk = await response.__anext__()
760
766
async def streaming():
767
yield f"data: {first_chunk.model_dump_json() if hasattr(first_chunk, 'model_dump_json') else first_chunk.json()}\n\n"
761
768
try:
762
769
async for chunk in response:
763
770
if isinstance(chunk, BaseConversation):
@@ -777,8 +784,11 @@ class Api:
777
784
yield f'data: {format_exception(e, config)}\n\n'
778
785
yield "data: [DONE]\n\n"
779
786
780
return StreamingResponse(streaming(), media_type="text/event-stream")
781
787
return StreamingResponse(
788
streaming(),
789
media_type="text/event-stream",
790
headers=getattr(first_chunk, "_headers").get_dict() if hasattr(first_chunk, "_headers") else None
791
)
782
792
except (ModelNotFoundError, ProviderNotFoundError) as e:
783
793
logger.exception(e)
784
794
return ErrorResponse.from_exception(e, config, HTTP_404_NOT_FOUND)
@@ -801,7 +811,7 @@ class Api:
801
811
@self.app.post("/v1/media/generate", responses=responses)
802
812
@self.app.post("/v1/images/generate", responses=responses)
803
813
@self.app.post("/v1/images/generations", responses=responses)
804
@self.app.post("/api/{provider}/images/generations", responses=responses)
814
@self.app.post("/api/{provider:path}/images/generations", responses=responses)
805
815
async def generate_image(
806
816
request: Request,
807
817
config: ImageGenerationConfig,
@@ -907,96 +917,6 @@ class Api:
907
917
)
908
918
return info
909
919
910
responses_pa = {
911
HTTP_200_OK: {"model": ChatCompletion},
912
HTTP_401_UNAUTHORIZED: {"model": ErrorResponseModel},
913
HTTP_404_NOT_FOUND: {"model": ErrorResponseModel},
914
HTTP_422_UNPROCESSABLE_ENTITY: {"model": ErrorResponseModel},
915
HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
916
}
917
918
@self.app.post("/pa/chat/completions", responses=responses_pa)
919
@self.app.post("/pa/{provider_id}/chat/completions", responses=responses_pa)
920
async def pa_chat_completions(
921
config: ChatCompletionsConfig,
922
credentials: Annotated[HTTPAuthorizationCredentials, Depends(Api.security)] = None,
923
provider_id: str = None,
924
):
925
"""OpenAI-compatible chat completions endpoint backed by PA providers.
926
927
The PA provider is identified by its opaque ID either from the URL
928
path (``/pa/{provider_id}/chat/completions``) or from the ``provider``
929
field in the JSON body. When both are absent the first available PA
930
provider is used.
931
"""
932
from g4f.mcp.pa_provider import get_pa_registry
933
934
registry = get_pa_registry()
935
pid = provider_id or config.provider
936
if pid is None:
937
listing = registry.list_providers()
938
if not listing:
939
return ErrorResponse.from_message(
940
"No PA providers found in workspace", HTTP_404_NOT_FOUND
941
)
942
pid = listing[0]["id"]
943
944
provider_cls = registry.get_provider_class(pid)
945
if provider_cls is None:
946
return ErrorResponse.from_message(
947
f"PA provider '{pid}' not found", HTTP_404_NOT_FOUND
948
)
949
950
try:
951
config.provider = None # pass the class directly below
952
if credentials is not None and credentials.credentials != "secret":
953
config.api_key = credentials.credentials
954
955
response = self.client.chat.completions.create(
956
**filter_none(
957
**(
958
config.model_dump(exclude_none=True)
959
if hasattr(config, "model_dump")
960
else config.dict(exclude_none=True)
961
),
962
**{
963
"conversation_id": None,
964
"provider": provider_cls,
965
},
966
),
967
)
968
969
if not config.stream:
970
return await response
971
972
async def streaming():
973
try:
974
async for chunk in response:
975
if not isinstance(chunk, BaseConversation):
976
yield (
977
f"data: "
978
f"{chunk.model_dump_json() if hasattr(chunk, 'model_dump_json') else chunk.json()}"
979
f"\n\n"
980
)
981
except GeneratorExit:
982
pass
983
except Exception as e:
984
logger.exception(e)
985
yield f"data: {format_exception(e, config)}\n\n"
986
yield "data: [DONE]\n\n"
987
988
return StreamingResponse(streaming(), media_type="text/event-stream")
989
990
except (ModelNotFoundError, ProviderNotFoundError) as e:
991
logger.exception(e)
992
return ErrorResponse.from_exception(e, config, HTTP_404_NOT_FOUND)
993
except (MissingAuthError, NoValidHarFileError) as e:
994
logger.exception(e)
995
return ErrorResponse.from_exception(e, config, HTTP_401_UNAUTHORIZED)
996
except Exception as e:
997
logger.exception(e)
998
return ErrorResponse.from_exception(e, config, HTTP_500_INTERNAL_SERVER_ERROR)
999
1000
920
# ------------------------------------------------------------------ #
1001
921
# PA workspace static file serving (HTML/CSS/JS/images for browser) #
1002
922
# ------------------------------------------------------------------ #
@@ -1151,7 +1071,7 @@ class Api:
1151
1071
HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
1152
1072
}
1153
1073
@self.app.post("/v1/audio/transcriptions", responses=responses)
1154
@self.app.post("/api/{path_provider}/audio/transcriptions", responses=responses)
1074
@self.app.post("/api/{path_provider:path}/audio/transcriptions", responses=responses)
1155
1075
@self.app.post("/api/markitdown", responses=responses)
1156
1076
async def convert(
1157
1077
file: UploadFile,
@@ -1195,7 +1115,7 @@ class Api:
1195
1115
HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
1196
1116
}
1197
1117
@self.app.post("/v1/audio/speech", responses=responses)
1198
@self.app.post("/api/{provider}/audio/speech", responses=responses)
1118
@self.app.post("/api/{provider:path}/audio/speech", responses=responses)
1199
1119
async def generate_speech(
1200
1120
config: AudioSpeechConfig,
1201
1121
provider: Optional[str] = None,
@@ -1385,12 +1305,12 @@ class Api:
1385
1305
start = max(0, total - limit - offset)
1386
1306
end = total - offset if offset < total else total
1387
1307
page = list(reversed(entries[start:end]))
1388
return {"total": total, "entries": page}
1308
return JSONResponse({"total": total, "entries": page}, headers={"Cache-Control": "no-store"})
1389
1309
1390
1310
@self.app.delete("/api/logs")
1391
1311
async def clear_logs():
1392
1312
_request_log.clear()
1393
return {"status": "cleared"}
1313
return JSONResponse({"status": "cleared"})
1394
1314
1395
1315
def format_exception(e: Union[Exception, str], config: Union[ChatCompletionsConfig, ImageGenerationConfig] = None, image: bool = False) -> str:
1396
1316
provider = (AppConfig.media_provider if image else AppConfig.provider)
@@ -81,6 +81,7 @@ def iter_response(
81
81
conversation: JsonConversation = None
82
82
completion_id = ''.join(random.choices(string.ascii_letters + string.digits, k=28))
83
83
idx = 0
84
headers = None
84
85
85
86
if hasattr(response, '__aiter__'):
86
87
response = to_sync_generator(response)
@@ -104,6 +105,9 @@ def iter_response(
104
105
continue
105
106
elif isinstance(chunk, Reasoning):
106
107
reasoning.append(chunk)
108
elif isinstance(chunk, HeadersResponse):
109
headers = chunk
110
continue
107
111
elif isinstance(chunk, (HiddenResponse, Exception, JsonRequest, JsonResponse)):
108
112
continue
109
113
elif not chunk:
@@ -125,6 +129,8 @@ def iter_response(
125
129
if provider_info is not None:
126
130
chunk.provider = provider_info.name
127
131
chunk.model = provider_info.model
132
if headers is not None:
133
chunk._headers = headers
128
134
yield chunk
129
135
130
136
if finish_reason is not None:
@@ -156,6 +162,8 @@ def iter_response(
156
162
if provider_info is not None:
157
163
chat_completion.provider = provider_info.name
158
164
chat_completion.model = provider_info.model
165
if headers is not None:
166
chat_completion._headers = headers
159
167
yield chat_completion
160
168
161
169
async def async_iter_response(
@@ -174,6 +182,7 @@ async def async_iter_response(
174
182
tool_calls = None
175
183
usage = None
176
184
conversation: JsonConversation = None
185
headers = None
177
186
178
187
try:
179
188
async for chunk in response:
@@ -195,6 +204,9 @@ async def async_iter_response(
195
204
continue
196
205
elif isinstance(chunk, Reasoning) and not stream:
197
206
reasoning.append(chunk)
207
elif isinstance(chunk, HeadersResponse):
208
headers = chunk
209
continue
198
210
elif isinstance(chunk, (HiddenResponse, Exception, JsonRequest, JsonResponse)):
199
211
continue
200
212
elif not chunk:
@@ -216,6 +228,8 @@ async def async_iter_response(
216
228
if provider_info is not None:
217
229
chunk.provider = provider_info.name
218
230
chunk.model = provider_info.model
231
if headers is not None:
232
chunk._headers = headers
219
233
yield chunk
220
234
221
235
if finish_reason is not None:
@@ -244,6 +258,8 @@ async def async_iter_response(
244
258
conversation=conversation,
245
259
reasoning=reasoning if reasoning else None
246
260
)
261
if headers is not None:
262
chat_completion._headers = headers
247
263
if provider_info is not None:
248
264
chat_completion.provider = provider_info.name
249
265
chat_completion.model = provider_info.model
@@ -89,11 +89,13 @@ class AbstractClientFactory:
89
89
"""
90
90
if not isinstance(provider, str):
91
91
return provider
92
elif provider.startswith("http://") or provider.startswith("https://"):
93
provider = create_custom_provider(provider, api_key, name=name, **kwargs)
92
94
elif provider.startswith("custom:"):
93
95
base_url = f"https://g4f.space/custom/{provider[7:]}"
94
96
if not api_key and not cls.is_provider_api_key(AppConfig.g4f_api_key):
95
97
api_key = AppConfig.g4f_api_key
96
provider = create_custom_provider(base_url, api_key, name=name, **kwargs)
98
provider = create_custom_provider(base_url, api_key, name=name, **kwargs)
97
99
elif provider in ProviderUtils.convert:
98
100
provider = ProviderUtils.convert[provider]
99
101
else: