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

XFEstudio/gpt4free

Enhance API routing by allowing path parameters for provider endpoints and improve response handling with headers in iter_response functions

193a481c
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

3 个文件 +41 -103
Modified g4f/api/__init__.py +22 -102
@@ -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)
Modified g4f/client/__init__.py +16 -0
@@ -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
Modified g4f/client/factory.py +3 -1
@@ -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: