返回提交历史
Modified
g4f/api/__init__.py
+1
-90
Modified
g4f/gui/server/backend_api.py
+78
-0
Modified
g4f/gui/server/crypto.py
+2
-1
XFEstudio/gpt4free
Refactor PA backend conversation endpoint for improved streaming and error handling; adjust session key generation to use 1024 bits for user input compatibility
05a9ac45
代码差异
3 个文件
+81
-91
@@ -224,7 +224,7 @@ class Api:
224
224
return (
225
225
path.startswith("/v1")
226
226
or path.startswith("/api/")
227
or path.startswith("/pa/")
227
or (path.startswith("/pa/") and not demo)
228
228
or (demo and path == "/backend-api/v2/upload_cookies")
229
229
)
230
230
@@ -766,95 +766,6 @@ class Api:
766
766
logger.exception(e)
767
767
return ErrorResponse.from_exception(e, config, HTTP_500_INTERNAL_SERVER_ERROR)
768
768
769
@self.app.post("/pa/backend-api/v2/conversation", responses={
770
HTTP_200_OK: {},
771
HTTP_404_NOT_FOUND: {"model": ErrorResponseModel},
772
HTTP_422_UNPROCESSABLE_ENTITY: {"model": ErrorResponseModel},
773
HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
774
})
775
async def pa_backend_conversation(request: Request):
776
"""GUI-compatible streaming conversation endpoint for PA providers.
777
778
Accepts the same JSON body as ``/backend-api/v2/conversation`` and
779
streams Server-Sent Events in the same format used by the gpt4free
780
web interface (``{"type": "content", "content": "..."}`` etc.).
781
782
The ``provider`` field should contain the opaque PA provider ID
783
returned by ``GET /pa/providers``. When omitted the first available
784
PA provider is used.
785
"""
786
from g4f.mcp.pa_provider import get_pa_registry
787
788
try:
789
body = await request.json()
790
except Exception:
791
return ErrorResponse.from_message(
792
"Invalid JSON body", HTTP_422_UNPROCESSABLE_ENTITY
793
)
794
795
registry = get_pa_registry()
796
pid = body.get("provider")
797
if pid:
798
provider_cls = registry.get_provider_class(pid)
799
if provider_cls is None:
800
return ErrorResponse.from_message(
801
f"PA provider '{pid}' not found", HTTP_404_NOT_FOUND
802
)
803
else:
804
listing = registry.list_providers()
805
if not listing:
806
return ErrorResponse.from_message(
807
"No PA providers found in workspace", HTTP_404_NOT_FOUND
808
)
809
provider_cls = registry.get_provider_class(listing[0]["id"])
810
811
provider_label = getattr(provider_cls, "label", provider_cls.__name__)
812
messages = body.get("messages") or []
813
model = body.get("model") or getattr(provider_cls, "default_model", "") or ""
814
815
async def gen_backend_stream():
816
yield (
817
"data: "
818
+ json.dumps({"type": "provider", "provider": provider_label, "model": model})
819
+ "\n\n"
820
)
821
try:
822
response = self.client.chat.completions.create(
823
messages=messages,
824
model=model,
825
provider=provider_cls,
826
stream=True,
827
)
828
async for chunk in response:
829
if isinstance(chunk, BaseConversation):
830
continue
831
text = ""
832
if hasattr(chunk, "choices") and chunk.choices:
833
delta = chunk.choices[0].delta
834
text = getattr(delta, "content", "") or ""
835
if text:
836
yield (
837
"data: "
838
+ json.dumps({"type": "content", "content": text})
839
+ "\n\n"
840
)
841
except GeneratorExit:
842
pass
843
except Exception as e:
844
logger.exception(e)
845
yield (
846
"data: "
847
+ json.dumps({"type": "error", "error": f"{type(e).__name__}: {e}"})
848
+ "\n\n"
849
)
850
yield (
851
"data: "
852
+ json.dumps({"type": "finish", "finish": "stop"})
853
+ "\n\n"
854
)
855
856
return StreamingResponse(gen_backend_stream(), media_type="text/event-stream")
857
858
769
# ------------------------------------------------------------------ #
859
770
# PA workspace static file serving (HTML/CSS/JS/images for browser) #
860
771
# ------------------------------------------------------------------ #
@@ -37,6 +37,7 @@ try:
37
37
except ImportError:
38
38
has_crypto = False
39
39
40
from ...client import Client
40
41
from ...client.service import convert_to_provider
41
42
from ...providers.asyncio import to_sync_generator
42
43
from ...providers.response import FinishReason, AudioResponse, MediaResponse, Reasoning, HiddenResponse, JsonResponse
@@ -85,6 +86,7 @@ class Backend_Api(Api):
85
86
"""
86
87
self.app: Flask = app
87
88
self.chat_cache = {}
89
self.client = Client()
88
90
89
91
if has_crypto:
90
92
private_key_obj = get_session_key()
@@ -130,6 +132,82 @@ class Backend_Api(Api):
130
132
"user": request.headers.get("x-user", "error")
131
133
})
132
134
135
@app.route('/pa/backend-api/v2/conversation', methods=['POST'])
136
async def pa_backend_conversation():
137
"""GUI-compatible streaming conversation endpoint for PA providers.
138
139
Accepts the same JSON body as ``/backend-api/v2/conversation`` and
140
streams Server-Sent Events in the same format used by the gpt4free
141
web interface (``{"type": "content", "content": "..."}`` etc.).
142
143
The ``provider`` field should contain the opaque PA provider ID
144
returned by ``GET /pa/providers``. When omitted the first available
145
PA provider is used.
146
"""
147
from g4f.mcp.pa_provider import get_pa_registry
148
149
if app.demo and has_crypto:
150
secret = request.headers.get("x-secret", request.headers.get("x_secret"))
151
if not secret or not validate_secret(secret):
152
return jsonify({"error": {"message": "Invalid or missing secret"}}), 403
153
154
try:
155
body = {**request.json}
156
except Exception:
157
return jsonify({"error": {"message": "Invalid JSON body"}}), 422
158
159
registry = get_pa_registry()
160
pid = body.get("provider")
161
if pid:
162
provider_cls = registry.get_provider_class(pid)
163
if provider_cls is None:
164
return jsonify({"error": {"message": f"PA provider '{pid}' not found"}}), 404
165
else:
166
listing = registry.list_providers()
167
if not listing:
168
return jsonify({"error": {"message": "No PA providers found in workspace"}}), 404
169
provider_cls = registry.get_provider_class(listing[0]["id"])
170
171
provider_label = getattr(provider_cls, "label", provider_cls.__name__)
172
messages = body.get("messages") or []
173
model = body.get("model") or getattr(provider_cls, "default_model", "") or ""
174
175
def gen_backend_stream():
176
yield (
177
"data: "
178
+ json.dumps({"type": "provider", "provider": {"name": pid, "label": provider_label, "model": model}})
179
+ "\n\n"
180
)
181
try:
182
response = self.client.chat.completions.create(
183
messages=messages,
184
model=model,
185
provider=provider_cls,
186
stream=True,
187
)
188
for chunk in response:
189
if chunk.choices and chunk.choices[0].delta:
190
yield f"data: {json.dumps({'type': 'content', 'content': chunk.choices[0].delta.content})}\n\n"
191
except GeneratorExit:
192
pass
193
except Exception as e:
194
logger.exception(e)
195
yield (
196
"data: "
197
+ json.dumps({"type": "error", "error": f"{type(e).__name__}: {e}"})
198
+ "\n\n"
199
)
200
yield (
201
"data: "
202
+ json.dumps({"type": "finish", "finish": "stop"})
203
+ "\n\n"
204
)
205
206
return self.app.response_class(
207
safe_iter_generator(gen_backend_stream()),
208
mimetype='text/event-stream'
209
)
210
133
211
@app.route('/backend-api/v2/models', methods=['GET'])
134
212
@lru_cache(maxsize=1)
135
213
def jsonify_models():
@@ -39,7 +39,8 @@ def create_or_read_keys() -> tuple[RSAPrivateKey, RSAPublicKey]:
39
39
return private_key, public_key
40
40
41
41
# Generate keys
42
private_key_obj = rsa.generate_private_key(public_exponent=65537, key_size=4096)
42
# Note: Using 1024 bits for the session key so the user can put it his secret (captcha)
43
private_key_obj = rsa.generate_private_key(public_exponent=65537, key_size=1024)
43
44
public_key_obj = private_key_obj.public_key()
44
45
45
46
# Serialize private key