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

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
H Lohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

10 个文件 +225 -41
Modified g4f/Provider/Copilot.py +2 -3
@@ -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,
Modified g4f/Provider/needs_auth/Gemini.py +2 -3
@@ -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",
Added g4f/Provider/needs_auth/GithubCopilot.py +93 -0
@@ -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")
Modified g4f/Provider/needs_auth/OpenaiChat.py +15 -11
@@ -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":
Modified g4f/Provider/needs_auth/__init__.py +1 -0
@@ -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
Modified g4f/api/__init__.py +85 -18
@@ -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):
Modified g4f/image.py +7 -3
@@ -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"[![{alt}]({fix_url(preview.replace('{image}', images) if preview else images)})]({fix_url(images)})"
233 result = f"[![{fix_title(alt)}]({fix_url(preview.replace('{image}', images) 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"[![#{idx+1} {alt}]({fix_url(preview[idx])})]({fix_url(image)})"
238 f"[![#{idx+1} {fix_title(alt)}]({fix_url(preview[idx])})]({fix_url(image)})"
235 239 for idx, image in enumerate(images)
236 240 )
237 241 start_flag = "<!-- generated images start -->\n"
Modified requirements-slim.txt +2 -1
@@ -13,4 +13,5 @@ flask
13 13 brotli
14 14 beautifulsoup4
15 15 aiohttp_socks
16 cryptography
16 cryptography
17 python-multipart
Modified requirements.txt +2 -1
@@ -18,4 +18,5 @@ aiohttp_socks
18 18 pywebview
19 19 plyer
20 20 cryptography
21 nodriver
21 nodriver
22 python-multipart
Modified setup.py +16 -1
@@ -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",