返回提交历史
Modified
g4f/Provider/Copilot.py
+2
-3
Modified
g4f/Provider/needs_auth/Gemini.py
+2
-3
Added
g4f/Provider/needs_auth/GithubCopilot.py
+93
-0
Modified
g4f/Provider/needs_auth/OpenaiChat.py
+15
-11
Modified
g4f/Provider/needs_auth/__init__.py
+1
-0
Modified
g4f/api/__init__.py
+85
-18
Modified
g4f/image.py
+7
-3
Modified
requirements-slim.txt
+2
-1
Modified
requirements.txt
+2
-1
Modified
setup.py
+16
-1
XFEstudio/gpt4free
Update api / add a synthesize and upload_cookies endpoint (#2406)
* Add web_search function to OpenaiChat provider * GithubCopilot provider added, it need a api_key * Remove nodriver login in Gemini synthesize * Update api / add a synthesize and upload_cookies endpoint
431db7b8
代码差异
10 个文件
+225
-41
@@ -73,10 +73,9 @@ class Copilot(AbstractProvider):
73
73
else:
74
74
access_token = conversation.access_token
75
75
debug.log(f"Copilot: Access token: {access_token[:7]}...{access_token[-5:]}")
76
debug.log(f"Copilot: Cookies: {';'.join([*cookies])}")
77
76
websocket_url = f"{websocket_url}&accessToken={quote(access_token)}"
78
headers = {"authorization": f"Bearer {access_token}", "cookie": format_cookies(cookies)}
79
77
headers = {"authorization": f"Bearer {access_token}"}
78
80
79
with Session(
81
80
timeout=timeout,
82
81
proxy=proxy,
@@ -206,6 +206,8 @@ class Gemini(AsyncGeneratorProvider):
206
206
207
207
@classmethod
208
208
async def synthesize(cls, params: dict, proxy: str = None) -> AsyncIterator[bytes]:
209
if "text" not in params:
210
raise ValueError("Missing parameter text")
209
211
async with ClientSession(
210
212
cookies=cls._cookies,
211
213
headers=REQUEST_HEADERS,
@@ -213,9 +215,6 @@ class Gemini(AsyncGeneratorProvider):
213
215
) as session:
214
216
if not cls._snlm0e:
215
217
await cls.fetch_snlm0e(session, cls._cookies) if cls._cookies else None
216
if not cls._snlm0e:
217
async for chunk in cls.nodriver_login(proxy):
218
debug.log(chunk)
219
218
inner_data = json.dumps([None, params["text"], "de-DE", None, 2])
220
219
async with session.post(
221
220
"https://gemini.google.com/_/BardChatUi/data/batchexecute",
@@ -0,0 +1,93 @@
1
from __future__ import annotations
2
3
import json
4
5
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, BaseConversation
6
from ...typing import AsyncResult, Messages, Cookies
7
from ...requests.raise_for_status import raise_for_status
8
from ...requests import StreamSession
9
from ...providers.helper import format_prompt
10
from ...cookies import get_cookies
11
12
class Conversation(BaseConversation):
13
conversation_id: str
14
15
def __init__(self, conversation_id: str):
16
self.conversation_id = conversation_id
17
18
class GithubCopilot(AsyncGeneratorProvider, ProviderModelMixin):
19
url = "https://copilot.microsoft.com"
20
working = True
21
needs_auth = True
22
supports_stream = True
23
default_model = "gpt-4o"
24
models = [default_model, "o1-mini", "o1-preview", "claude-3.5-sonnet"]
25
26
@classmethod
27
async def create_async_generator(
28
cls,
29
model: str,
30
messages: Messages,
31
stream: bool = False,
32
api_key: str = "X2eRgXPamxGK_TXS6seGGYy541mQuVJdH1CYljrvSPuc38je5J4KK4Aw0y5X2oVRFMjA4B1fo9sdsr4VJcl-VBae7H0Mr4U9GIkFnGx3hSs=",
33
proxy: str = None,
34
cookies: Cookies = None,
35
conversation_id: str = None,
36
conversation: Conversation = None,
37
return_conversation: bool = False,
38
**kwargs
39
) -> AsyncResult:
40
if not model:
41
model = cls.default_model
42
if cookies is None:
43
cookies = get_cookies(".github.com")
44
async with StreamSession(
45
proxy=proxy,
46
impersonate="chrome",
47
cookies=cookies,
48
headers={
49
"GitHub-Verified-Fetch": "true",
50
}
51
) as session:
52
headers = {}
53
if api_key is None:
54
async with session.post("https://github.com/github-copilot/chat/token") as response:
55
await raise_for_status(response, "Get token")
56
api_key = (await response.json()).get("token")
57
headers = {
58
"Authorization": f"GitHub-Bearer {api_key}",
59
}
60
if conversation is not None:
61
conversation_id = conversation.conversation_id
62
if conversation_id is None:
63
print(headers)
64
async with session.post("https://api.individual.githubcopilot.com/github/chat/threads", headers=headers) as response:
65
await raise_for_status(response)
66
conversation_id = (await response.json()).get("thread_id")
67
if return_conversation:
68
yield Conversation(conversation_id)
69
content = messages[-1]["content"]
70
else:
71
content = format_prompt(messages)
72
json_data = {
73
"content": content,
74
"intent": "conversation",
75
"references":[],
76
"context": [],
77
"currentURL": f"https://github.com/copilot/c/{conversation_id}",
78
"streaming": True,
79
"confirmations": [],
80
"customInstructions": [],
81
"model": model,
82
"mode": "immersive"
83
}
84
async with session.post(
85
f"https://api.individual.githubcopilot.com/github/chat/threads/{conversation_id}/messages",
86
json=json_data,
87
headers=headers
88
) as response:
89
async for line in response.iter_lines():
90
if line.startswith(b"data: "):
91
data = json.loads(line[6:])
92
if data.get("type") == "content":
93
yield data.get("body")
@@ -111,7 +111,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
111
111
# Post the image data to the service and get the image data
112
112
async with session.post(f"{cls.url}/backend-api/files", json=data, headers=headers) as response:
113
113
cls._update_request_args(session)
114
await raise_for_status(response)
114
await raise_for_status(response, "Create file failed")
115
115
image_data = {
116
116
**data,
117
117
**await response.json(),
@@ -129,7 +129,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
129
129
"x-ms-blob-type": "BlockBlob"
130
130
}
131
131
) as response:
132
await raise_for_status(response)
132
await raise_for_status(response, "Send file failed")
133
133
# Post the file ID to the service and get the download URL
134
134
async with session.post(
135
135
f"{cls.url}/backend-api/files/{image_data['file_id']}/uploaded",
@@ -137,12 +137,12 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
137
137
headers=headers
138
138
) as response:
139
139
cls._update_request_args(session)
140
await raise_for_status(response)
140
await raise_for_status(response, "Get download url failed")
141
141
image_data["download_url"] = (await response.json())["download_url"]
142
142
return ImageRequest(image_data)
143
143
144
144
@classmethod
145
def create_messages(cls, messages: Messages, image_request: ImageRequest = None):
145
def create_messages(cls, messages: Messages, image_request: ImageRequest = None, system_hints: list = None):
146
146
"""
147
147
Create a list of messages for the user input
148
148
@@ -160,7 +160,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
160
160
"id": str(uuid.uuid4()),
161
161
"create_time": int(time.time()),
162
162
"id": str(uuid.uuid4()),
163
"metadata": {"serialization_metadata": {"custom_symbol_offsets": []}}
163
"metadata": {"serialization_metadata": {"custom_symbol_offsets": []}, "system_hints": system_hints},
164
164
} for message in messages]
165
165
166
166
# Check if there is an image response
@@ -189,7 +189,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
189
189
return messages
190
190
191
191
@classmethod
192
async def get_generated_image(cls, session: StreamSession, headers: dict, element: dict) -> ImageResponse:
192
async def get_generated_image(cls, session: StreamSession, headers: dict, element: dict, prompt: str = None) -> ImageResponse:
193
193
"""
194
194
Retrieves the image response based on the message content.
195
195
@@ -211,6 +211,8 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
211
211
try:
212
212
prompt = element["metadata"]["dalle"]["prompt"]
213
213
file_id = element["asset_pointer"].split("file-service://", 1)[1]
214
except TypeError:
215
return
214
216
except Exception as e:
215
217
raise RuntimeError(f"No Image: {e.__class__.__name__}: {e}")
216
218
try:
@@ -240,6 +242,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
240
242
image_name: str = None,
241
243
return_conversation: bool = False,
242
244
max_retries: int = 3,
245
web_search: bool = False,
243
246
**kwargs
244
247
) -> AsyncResult:
245
248
"""
@@ -331,14 +334,15 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
331
334
"conversation_mode": {"kind":"primary_assistant"},
332
335
"websocket_request_id": str(uuid.uuid4()),
333
336
"supported_encodings": ["v1"],
334
"supports_buffering": True
337
"supports_buffering": True,
338
"system_hints": ["search"] if web_search else None
335
339
}
336
340
if conversation.conversation_id is not None:
337
341
data["conversation_id"] = conversation.conversation_id
338
342
debug.log(f"OpenaiChat: Use conversation: {conversation.conversation_id}")
339
343
if action != "continue":
340
344
messages = messages if conversation_id is None else [messages[-1]]
341
data["messages"] = cls.create_messages(messages, image_request)
345
data["messages"] = cls.create_messages(messages, image_request, ["search"] if web_search else None)
342
346
headers = {
343
347
**cls._headers,
344
348
"accept": "text/event-stream",
@@ -419,9 +423,9 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
419
423
generated_images = []
420
424
for element in c.get("parts"):
421
425
if isinstance(element, dict) and element.get("content_type") == "image_asset_pointer":
422
generated_images.append(
423
cls.get_generated_image(session, cls._headers, element)
424
)
426
image = cls.get_generated_image(session, cls._headers, element)
427
if image is not None:
428
generated_images.append(image)
425
429
for image_response in await asyncio.gather(*generated_images):
426
430
yield image_response
427
431
if m.get("author", {}).get("role") == "assistant":
@@ -7,6 +7,7 @@ from .DeepInfra import DeepInfra
7
7
from .DeepInfraImage import DeepInfraImage
8
8
from .Gemini import Gemini
9
9
from .GeminiPro import GeminiPro
10
from .GithubCopilot import GithubCopilot
10
11
from .Groq import Groq
11
12
from .HuggingFace import HuggingFace
12
13
from .HuggingFace2 import HuggingFace2
@@ -5,8 +5,10 @@ import json
5
5
import uvicorn
6
6
import secrets
7
7
import os
8
import shutil
8
9
9
from fastapi import FastAPI, Response, Request
10
import os.path
11
from fastapi import FastAPI, Response, Request, UploadFile
10
12
from fastapi.responses import StreamingResponse, RedirectResponse, HTMLResponse, JSONResponse
11
13
from fastapi.exceptions import RequestValidationError
12
14
from fastapi.security import APIKeyHeader
@@ -16,16 +18,17 @@ from fastapi.encoders import jsonable_encoder
16
18
from fastapi.middleware.cors import CORSMiddleware
17
19
from starlette.responses import FileResponse
18
20
from pydantic import BaseModel
19
from typing import Union, Optional
21
from typing import Union, Optional, List
20
22
21
23
import g4f
22
24
import g4f.debug
23
from g4f.client import AsyncClient, ChatCompletion
25
from g4f.client import AsyncClient, ChatCompletion, convert_to_provider
24
26
from g4f.providers.response import BaseConversation
25
27
from g4f.client.helper import filter_none
26
28
from g4f.image import is_accepted_format, images_dir
27
29
from g4f.typing import Messages
28
from g4f.cookies import read_cookie_files
30
from g4f.errors import ProviderNotFoundError
31
from g4f.cookies import read_cookie_files, get_cookies_dir
29
32
from g4f.Provider import ProviderType, ProviderUtils, __providers__
30
33
31
34
logger = logging.getLogger(__name__)
@@ -78,6 +81,18 @@ class ImageGenerationConfig(BaseModel):
78
81
api_key: Optional[str] = None
79
82
proxy: Optional[str] = None
80
83
84
class ProviderResponseModel(BaseModel):
85
id: str
86
object: str = "provider"
87
created: int
88
owned_by: Optional[str]
89
90
class ModelResponseModel(BaseModel):
91
id: str
92
object: str = "model"
93
created: int
94
owned_by: Optional[str]
95
81
96
class AppConfig:
82
97
ignored_providers: Optional[list[str]] = None
83
98
g4f_api_key: Optional[str] = None
@@ -109,7 +124,7 @@ class Api:
109
124
def register_authorization(self):
110
125
@self.app.middleware("http")
111
126
async def authorization(request: Request, call_next):
112
if self.g4f_api_key and request.url.path in ["/v1/chat/completions", "/v1/completions", "/v1/images/generate"]:
127
if self.g4f_api_key and request.url.path not in ("/", "/v1"):
113
128
try:
114
129
user_g4f_api_key = await self.get_g4f_api_key(request)
115
130
except HTTPException as e:
@@ -123,9 +138,7 @@ class Api:
123
138
status_code=HTTP_403_FORBIDDEN,
124
139
content=jsonable_encoder({"detail": "Invalid G4F API key"}),
125
140
)
126
127
response = await call_next(request)
128
return response
141
return await call_next(request)
129
142
130
143
def register_validation_exception_handler(self):
131
144
@self.app.exception_handler(RequestValidationError)
@@ -158,22 +171,21 @@ class Api:
158
171
'<a href="/docs">/docs</a>')
159
172
160
173
@self.app.get("/v1/models")
161
async def models():
174
async def models() -> list[ModelResponseModel]:
162
175
model_list = dict(
163
176
(model, g4f.models.ModelUtils.convert[model])
164
177
for model in g4f.Model.__all__()
165
178
)
166
model_list = [{
179
return [{
167
180
'id': model_id,
168
181
'object': 'model',
169
182
'created': 0,
170
183
'owned_by': model.base_provider
171
184
} for model_id, model in model_list.items()]
172
return JSONResponse(model_list)
173
185
174
186
@self.app.get("/v1/models/{model_name}")
175
187
async def model_info(model_name: str):
176
try:
188
if model_name in g4f.models.ModelUtils.convert:
177
189
model_info = g4f.models.ModelUtils.convert[model_name]
178
190
return JSONResponse({
179
191
'id': model_name,
@@ -181,8 +193,7 @@ class Api:
181
193
'created': 0,
182
194
'owned_by': model_info.base_provider
183
195
})
184
except:
185
return JSONResponse({"error": "The model does not exist."})
196
return JSONResponse({"error": "The model does not exist."}, 404)
186
197
187
198
@self.app.post("/v1/chat/completions")
188
199
async def chat_completions(config: ChatCompletionsConfig, request: Request = None, provider: str = None):
@@ -277,12 +288,68 @@ class Api:
277
288
logger.exception(e)
278
289
return Response(content=format_exception(e, config, True), status_code=500, media_type="application/json")
279
290
280
@self.app.post("/v1/completions")
281
async def completions():
282
return Response(content=json.dumps({'info': 'Not working yet.'}, indent=4), media_type="application/json")
291
@self.app.get("/v1/providers")
292
async def providers() -> list[ProviderResponseModel]:
293
return [{
294
'id': provider.__name__,
295
'object': 'provider',
296
'created': 0,
297
'url': provider.url,
298
'label': getattr(provider, "label", None),
299
} for provider in __providers__ if provider.working]
300
301
@self.app.get("/v1/providers/{provider}")
302
async def providers_info(provider: str) -> ProviderResponseModel:
303
if provider not in ProviderUtils.convert:
304
return JSONResponse({"error": "The provider does not exist."}, 404)
305
provider: ProviderType = ProviderUtils.convert[provider]
306
def safe_get_models(provider: ProviderType) -> list[str]:
307
try:
308
return provider.get_models() if hasattr(provider, "get_models") else []
309
except:
310
return []
311
return {
312
'id': provider.__name__,
313
'object': 'provider',
314
'created': 0,
315
'url': provider.url,
316
'label': getattr(provider, "label", None),
317
'models': safe_get_models(provider),
318
'image_models': getattr(provider, "image_models", []) or [],
319
'vision_models': [model for model in [getattr(provider, "default_vision_model", None)] if model],
320
'params': [*provider.get_parameters()] if hasattr(provider, "get_parameters") else []
321
}
322
323
@self.app.post("/v1/upload_cookies")
324
def upload_cookies(files: List[UploadFile]):
325
response_data = []
326
for file in files:
327
try:
328
if file and file.filename.endswith(".json") or file.filename.endswith(".har"):
329
filename = os.path.basename(file.filename)
330
with open(os.path.join(get_cookies_dir(), filename), 'wb') as f:
331
shutil.copyfileobj(file.file, f)
332
response_data.append({"filename": filename})
333
finally:
334
file.file.close()
335
return response_data
336
337
@self.app.get("/v1/synthesize/{provider}")
338
async def synthesize(request: Request, provider: str):
339
try:
340
provider_handler = convert_to_provider(provider)
341
except ProviderNotFoundError:
342
return Response("Provider not found", 404)
343
if not hasattr(provider_handler, "synthesize"):
344
return Response("Provider doesn't support synthesize", 500)
345
if len(request.query_params) == 0:
346
return Response("Missing query params", 500)
347
response_data = provider_handler.synthesize({**request.query_params})
348
content_type = getattr(provider_handler, "synthesize_content_type", "application/octet-stream")
349
return StreamingResponse(response_data, media_type=content_type)
283
350
284
351
@self.app.get("/images/{filename}")
285
async def get_image(filename):
352
async def get_image(filename) -> FileResponse:
286
353
target = os.path.join(images_dir, filename)
287
354
288
355
if not os.path.isfile(target):
@@ -33,10 +33,14 @@ EXTENSIONS_MAP: dict[str, str] = {
33
33
# Define the directory for generated images
34
34
images_dir = "./generated_images"
35
35
36
def fix_url(url:str) -> str:
36
def fix_url(url: str) -> str:
37
37
""" replace ' ' by '+' (to be markdown compliant)"""
38
38
return url.replace(" ","+")
39
39
40
def fix_title(title: str) -> str:
41
if title:
42
return title.replace("\n", "").replace('"', '')
43
40
44
def to_image(image: ImageType, is_svg: bool = False) -> Image:
41
45
"""
42
46
Converts the input image to a PIL Image object.
@@ -226,12 +230,12 @@ def format_images_markdown(images: Union[str, list], alt: str, preview: Union[st
226
230
str: The formatted markdown string.
227
231
"""
228
232
if isinstance(images, str):
229
result = f"[ if preview else images)})]({fix_url(images)})"
233
result = f"[ if preview else images)})]({fix_url(images)})"
230
234
else:
231
235
if not isinstance(preview, list):
232
236
preview = [preview.replace('{image}', image) if preview else image for image in images]
233
237
result = "\n".join(
234
f"[})]({fix_url(image)})"
238
f"[})]({fix_url(image)})"
235
239
for idx, image in enumerate(images)
236
240
)
237
241
start_flag = "<!-- generated images start -->\n"
@@ -13,4 +13,5 @@ flask
13
13
brotli
14
14
beautifulsoup4
15
15
aiohttp_socks
16
cryptography
16
cryptography
17
python-multipart
@@ -18,4 +18,5 @@ aiohttp_socks
18
18
pywebview
19
19
plyer
20
20
cryptography
21
nodriver
21
nodriver
22
python-multipart
@@ -31,6 +31,20 @@ EXTRA_REQUIRE = {
31
31
"fastapi", # api
32
32
"uvicorn", # api
33
33
"nodriver",
34
"python-multipart",
35
],
36
'slim': [
37
"curl_cffi>=0.6.2",
38
"certifi",
39
"duckduckgo-search>=5.0" ,# internet.search
40
"beautifulsoup4", # internet.search and bing.create_images
41
"aiohttp_socks", # proxy
42
"pillow", # image
43
"cairosvg", # svg image
44
"werkzeug", "flask", # gui
45
"fastapi", # api
46
"uvicorn", # api
47
"python-multipart",
34
48
],
35
49
'slim': [
36
50
"curl_cffi>=0.6.2",
@@ -69,7 +83,8 @@ EXTRA_REQUIRE = {
69
83
"werkzeug", "flask",
70
84
"beautifulsoup4", "pillow",
71
85
"duckduckgo-search>=5.0",
72
"browser_cookie3"
86
"browser_cookie3",
87
"python-multipart",
73
88
],
74
89
"search": [
75
90
"beautifulsoup4", "pillow",