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

XFEstudio/gpt4free

Refactor providers to use OpenaiTemplate and add backup URLs; enhance response handling in API

798a951b
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

10 个文件 +52 -230
Modified g4f/Provider/Copilot.py +5 -2
@@ -191,7 +191,7 @@ class Copilot(AsyncAuthedProvider, ProviderModelMixin):
191 191 response.raise_for_status()
192 192 debug.log(f"Copilot: Update cookies: [{', '.join(key for key in response.cookies)}]")
193 193 auth_result.cookies.update({key: value for key, value in response.cookies.items()})
194 if not cls.needs_auth and cls.anon_cookie_name not in auth_result.cookies:
194 if not getattr(auth_result, "access_token", None) and not cls.needs_auth and cls.anon_cookie_name not in auth_result.cookies:
195 195 raise MissingAuthError(f"Missing cookie: {cls.anon_cookie_name}")
196 196 conversation = Conversation(response.json().get("currentConversationId"))
197 197 debug.log(f"Copilot: Created conversation: {conversation.conversation_id}")
@@ -360,14 +360,17 @@ async def get_access_token_and_cookies(url: str, proxy: str = None, needs_auth:
360 360 debug.log(f"Got access token: {access_token[:10]}..., useridentitytype: {useridentitytype}")
361 361 break
362 362 if not needs_auth:
363 debug.log("No access token found, but authentication not required.")
363 364 break
364 365 if not needs_auth:
365 366 textarea = await page.select("textarea")
366 367 if textarea is not None:
368 debug.log("Filling textarea to generate anon cookie.")
367 369 await textarea.send_keys("Hello")
368 370 await asyncio.sleep(1)
369 371 button = await page.select("[data-testid=\"submit-button\"]")
370 372 if button:
373 debug.log("Clicking submit button to generate anon cookie.")
371 374 await button.click()
372 375 turnstile = await page.select('#cf-turnstile')
373 376 if turnstile:
@@ -375,7 +378,7 @@ async def get_access_token_and_cookies(url: str, proxy: str = None, needs_auth:
375 378 await asyncio.sleep(3)
376 379 await click_trunstile(page)
377 380 cookies = {}
378 while Copilot.anon_cookie_name not in cookies:
381 while not access_token and Copilot.anon_cookie_name not in cookies:
379 382 await asyncio.sleep(2)
380 383 cookies = {c.name: c.value for c in await page.send(nodriver.cdp.network.get_cookies([url]))}
381 384 if not needs_auth and Copilot.anon_cookie_name in cookies:
Modified g4f/Provider/local/Ollama.py +21 -17
@@ -4,17 +4,17 @@ import json
4 4 import requests
5 5 import os
6 6
7 from ..needs_auth.OpenaiAPI import OpenaiAPI
7 from ..template import OpenaiTemplate
8 8 from ...requests import StreamSession, raise_for_status
9 9 from ...providers.response import Usage, Reasoning
10 10 from ...tools.run_tools import AuthManager
11 11 from ...typing import AsyncResult, Messages
12 12
13 class Ollama(OpenaiAPI):
13 class Ollama(OpenaiTemplate):
14 14 label = "Ollama 🦙"
15 15 url = "https://ollama.com"
16 16 login_url = "https://ollama.com/settings/keys"
17 api_endpoint = "https://ollama.com/api/chat"
17 backup_url = "https://g4f.dev/api/ollama"
18 18 needs_auth = False
19 19 working = True
20 20 active_by_default = True
@@ -30,11 +30,10 @@ class Ollama(OpenaiAPI):
30 30 cls.models = []
31 31 if not api_key:
32 32 api_key = AuthManager.load_api_key(cls)
33 if api_key:
34 models = requests.get("https://ollama.com/api/tags", {"headers": {"Authorization": f"Bearer {api_key}"}}).json()["models"]
35 if models:
36 cls.live += 1
37 cls.models = [model["name"] for model in models]
33 models = requests.get("https://ollama.com/api/tags").json()["models"]
34 if models:
35 cls.live += 1
36 cls.models = [model["name"] for model in models]
38 37 if base_url is None:
39 38 host = os.getenv("OLLAMA_HOST", "127.0.0.1")
40 39 port = os.getenv("OLLAMA_PORT", "11434")
@@ -48,7 +47,7 @@ class Ollama(OpenaiAPI):
48 47 if cls.live == 0 and models:
49 48 cls.live += 1
50 49 cls.local_models = [model["name"] for model in models]
51 cls.models = cls.models + cls.local_models
50 cls.models = cls.models.copy() + cls.local_models
52 51 cls.default_model = next(iter(cls.models), None)
53 52 return cls.models
54 53
@@ -66,14 +65,9 @@ class Ollama(OpenaiAPI):
66 65 host = os.getenv("OLLAMA_HOST", "localhost")
67 66 port = os.getenv("OLLAMA_PORT", "11434")
68 67 base_url: str = f"http://{host}:{port}/v1"
69 if model in cls.local_models or not api_key:
70 async for chunk in super().create_async_generator(
71 model, messages, base_url=base_url, proxy=proxy, **kwargs
72 ):
73 yield chunk
74 else:
68 if model in cls.local_models:
75 69 async with StreamSession(headers={"Authorization": f"Bearer {api_key}"}, proxy=proxy) as session:
76 async with session.post(cls.api_endpoint, json={
70 async with session.post(f"{base_url}/api/chat", json={
77 71 "model": model,
78 72 "messages": messages,
79 73 }) as response:
@@ -92,4 +86,14 @@ class Ollama(OpenaiAPI):
92 86 prompt_tokens=last_data.get("prompt_eval_count", 0),
93 87 completion_tokens=last_data.get("eval_count", 0),
94 88 total_tokens=last_data.get("prompt_eval_count", 0) + last_data.get("eval_count", 0),
95 )
89 )
90 else:
91 async for chunk in super().create_async_generator(
92 model,
93 messages,
94 api_key=api_key,
95 base_url=cls.backup_url,
96 proxy=proxy,
97 **kwargs
98 ):
99 yield chunk
Modified g4f/Provider/needs_auth/Azure.py +2 -3
@@ -14,10 +14,9 @@ from ..helper import format_media_prompt
14 14 class Azure(OpenaiTemplate):
15 15 label = "Azure ☁️"
16 16 url = "https://ai.azure.com"
17 base_url = "https://host.g4f.dev/api/Azure"
17 base_url = "https://g4f.dev/api/azure"
18 backup_url = "https://g4f.dev/api/azure"
18 19 working = True
19 needs_auth = True
20 models_needs_auth = True
21 20 active_by_default = False
22 21 login_url = "https://discord.gg/qXA4Wf4Fsm"
23 22 routes: dict[str, str] = {}
Modified g4f/Provider/needs_auth/GeminiPro.py +5 -181
@@ -1,33 +1,15 @@
1 1 from __future__ import annotations
2 2
3 import base64
4 import json
5 import requests
6 from typing import Optional
7 from aiohttp import ClientSession, BaseConnector
3 from ..template import OpenaiTemplate
8 4
9 from ...typing import AsyncResult, Messages, MediaListType
10 from ...image import to_bytes, is_data_an_media
11 from ...errors import MissingAuthError, ModelNotFoundError
12 from ...requests import raise_for_status, iter_lines
13 from ...providers.response import Usage, FinishReason
14 from ...image.copy_images import save_response_media
15 from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
16 from ..helper import get_connector, to_string, format_media_prompt, get_system_prompt
17 from ... import debug
18
19 class GeminiPro(AsyncGeneratorProvider, ProviderModelMixin):
5 class GeminiPro(OpenaiTemplate):
20 6 label = "Google Gemini API"
21 7 url = "https://ai.google.dev"
22 8 login_url = "https://aistudio.google.com/u/0/apikey"
23 base_url = "https://generativelanguage.googleapis.com/v1beta"
9 base_url = "https://generativelanguage.googleapis.com/v1beta/openai"
10 backup_url = "https://g4f.dev/custom/srv_mjnryskw9fe0567fa267"
24 11 active_by_default = True
25
26 12 working = True
27 supports_message_history = True
28 supports_system_message = True
29 needs_auth = True
30
31 13 default_model = "gemini-2.5-flash"
32 14 default_vision_model = default_model
33 15 fallback_models = [
@@ -39,162 +21,4 @@ class GeminiPro(AsyncGeneratorProvider, ProviderModelMixin):
39 21 "gemma-3-4b-it",
40 22 "gemma-3n-e2b-it",
41 23 "gemma-3n-e4b-it",
42 ]
43
44 @classmethod
45 def get_models(cls, api_key: str = None, base_url: str = base_url, **kwargs) -> list[str]:
46 if not api_key:
47 return cls.fallback_models
48 if not cls.models:
49 try:
50 url = f"{cls.base_url if not base_url else base_url}/models"
51 response = requests.get(url, params={"key": api_key})
52 raise_for_status(response)
53 data = response.json()
54 cls.models = [
55 model.get("name").split("/").pop()
56 for model in data.get("models")
57 if "generateContent" in model.get("supportedGenerationMethods")
58 ]
59 cls.models.sort()
60 cls.live += 1
61 except Exception as e:
62 debug.error(e)
63 if api_key is not None:
64 raise MissingAuthError("Invalid API key")
65 return cls.fallback_models
66 return cls.models
67
68 @classmethod
69 async def create_async_generator(
70 cls,
71 model: str,
72 messages: Messages,
73 stream: bool = False,
74 proxy: str = None,
75 api_key: str = None,
76 base_url: str = base_url,
77 use_auth_header: bool = False,
78 media: MediaListType = None,
79 tools: Optional[list] = None,
80 connector: BaseConnector = None,
81 **kwargs
82 ) -> AsyncResult:
83 if not api_key:
84 raise MissingAuthError('Add a "api_key"')
85
86 try:
87 model = cls.get_model(model, api_key=api_key, base_url=base_url)
88 except ModelNotFoundError:
89 pass
90
91 headers = params = None
92 if use_auth_header:
93 headers = {"Authorization": f"Bearer {api_key}"}
94 else:
95 params = {"key": api_key}
96
97 method = "streamGenerateContent" if stream else "generateContent"
98 url = f"{base_url.rstrip('/')}/models/{model}:{method}"
99 async with ClientSession(headers=headers, connector=get_connector(connector, proxy)) as session:
100 contents = [
101 {
102 "role": "model" if message["role"] == "assistant" else "user",
103 "parts": [{"text": to_string(message["content"])}]
104 }
105 for message in messages
106 if message["role"] not in ["system", "developer"]
107 ]
108 if media is not None:
109 if not contents:
110 contents.append({"role": "user", "parts": []})
111 for media_data, filename in media:
112 media_data = to_bytes(media_data)
113 contents[-1]["parts"].append({
114 "inline_data": {
115 "mime_type": is_data_an_media(media_data, filename),
116 "data": base64.b64encode(media_data).decode()
117 }
118 })
119 responseModalities = {"responseModalities": ["AUDIO"]} if "tts" in model else {}
120 data = {
121 "contents": contents,
122 "generationConfig": {
123 "stopSequences": kwargs.get("stop"),
124 "temperature": kwargs.get("temperature"),
125 "maxOutputTokens": kwargs.get("max_tokens"),
126 "topP": kwargs.get("top_p"),
127 "topK": kwargs.get("top_k"),
128 **responseModalities,
129 },
130 "tools": [{
131 "function_declarations": [{
132 "name": tool["function"]["name"],
133 "description": tool["function"]["description"],
134 "parameters": {
135 "type": "object",
136 "properties": {key: {
137 "type": value["type"],
138 "description": value["title"]
139 } for key, value in tool["function"]["parameters"]["properties"].items()}
140 },
141 } for tool in tools]
142 }] if tools else None
143 }
144 system_prompt = get_system_prompt(messages)
145 if system_prompt:
146 data["system_instruction"] = {"parts": {"text": system_prompt}}
147 async with session.post(url, params=params, json=data) as response:
148 if not response.ok:
149 data = await response.json()
150 data = data[0] if isinstance(data, list) else data
151 raise RuntimeError(f"Response {response.status}: {data['error']['message']}")
152 if stream:
153 lines = []
154 buffer = b""
155 async for chunk in iter_lines(response.content.iter_any()):
156 buffer += chunk
157 if chunk == b"[{":
158 lines = [b"{"]
159 elif chunk == b"," or chunk == b"]":
160 try:
161 data = json.loads(b"".join(lines))
162 content = data["candidates"][0]["content"]
163 if "parts" in content and content["parts"]:
164 if "text" in content["parts"][0]:
165 yield content["parts"][0]["text"]
166 elif "inlineData" in content["parts"][0]:
167 async for media in save_response_media(
168 content["parts"][0]["inlineData"], format_media_prompt(messages)
169 ):
170 yield media
171 if "finishReason" in data["candidates"][0]:
172 yield FinishReason(data["candidates"][0]["finishReason"].lower())
173 usage = data.get("usageMetadata")
174 if usage:
175 yield Usage(
176 prompt_tokens=usage.get("promptTokenCount"),
177 completion_tokens=usage.get("candidatesTokenCount"),
178 total_tokens=usage.get("totalTokenCount")
179 )
180 except Exception as e:
181 raise RuntimeError(f"Read chunk failed") from e
182 lines = []
183 else:
184 lines.append(chunk)
185 else:
186 data = await response.json()
187 candidate = data["candidates"][0]
188 if "content" in candidate:
189 content = candidate["content"]
190 if "parts" in content and content["parts"]:
191 for part in content["parts"]:
192 if "text" in part:
193 yield part["text"]
194 elif "inlineData" in part:
195 async for media in save_response_media(
196 part["inlineData"], format_media_prompt(messages)
197 ):
198 yield media
199 if "finishReason" in candidate:
200 yield FinishReason(candidate["finishReason"].lower())
24 ]
Modified g4f/Provider/needs_auth/Groq.py +1 -22
@@ -7,31 +7,10 @@ class Groq(OpenaiTemplate):
7 7 url = "https://console.groq.com/playground"
8 8 login_url = "https://console.groq.com/keys"
9 9 base_url = "https://api.groq.com/openai/v1"
10 backup_url = "https://g4f.dev/api/groq"
10 11 working = True
11 needs_auth = True
12 models_needs_auth = True
13 12 active_by_default = True
14 13 default_model = DEFAULT_MODEL
15 fallback_models = [
16 "distil-whisper-large-v3-en",
17 "gemma2-9b-it",
18 "gemma-7b-it",
19 "llama3-groq-70b-8192-tool-use-preview",
20 "llama3-groq-8b-8192-tool-use-preview",
21 "llama-3.1-70b-versatile",
22 "llama-3.1-8b-instant",
23 "llama-3.2-1b-preview",
24 "llama-3.2-3b-preview",
25 "llama-3.2-11b-vision-preview",
26 "llama-3.2-90b-vision-preview",
27 "llama-guard-3-8b",
28 "llava-v1.5-7b-4096-preview",
29 "llama3-70b-8192",
30 "llama3-8b-8192",
31 "mixtral-8x7b-32768",
32 "whisper-large-v3",
33 "whisper-large-v3-turbo",
34 ]
35 14 model_aliases = {
36 15 "mixtral-8x7b": "mixtral-8x7b-32768",
37 16 "llama2-70b": "llama2-70b-4096",
Modified g4f/Provider/needs_auth/Nvidia.py +1 -2
@@ -6,11 +6,10 @@ from ...config import DEFAULT_MODEL
6 6 class Nvidia(OpenaiTemplate):
7 7 label = "Nvidia"
8 8 base_url = "https://integrate.api.nvidia.com/v1"
9 backup_url = "https://g4f.dev/api/nvidia"
9 10 login_url = "https://google.com"
10 11 url = "https://build.nvidia.com"
11 12 working = True
12 13 active_by_default = True
13 needs_auth = True
14 models_needs_auth = True
15 14 default_model = DEFAULT_MODEL
16 15 add_user = False
Modified g4f/Provider/needs_auth/OpenRouter.py +1 -0
@@ -13,6 +13,7 @@ class OpenRouter(OpenaiTemplate):
13 13
14 14 class OpenRouterFree(OpenRouter):
15 15 label = "OpenRouter (free)"
16 backup_url = "https://g4f.dev/api/openrouter"
16 17 max_tokens = 4096
17 18 active_by_default = True
18 19
Modified g4f/Provider/template/OpenaiTemplate.py +11 -3
@@ -16,6 +16,7 @@ from ... import debug
16 16
17 17 class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin):
18 18 base_url = ""
19 backup_url = None
19 20 api_key = None
20 21 api_endpoint = None
21 22 supports_message_history = True
@@ -30,16 +31,22 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
30 31 use_image_size = False
31 32 max_tokens: int = None
32 33
34 @classmethod
35 def is_provider_api_key(cls, api_key: str) -> bool:
36 if cls.backup_url is None:
37 return True
38 return api_key and not api_key.startswith("g4f-") and not api_key.startswith("gfs-")
39
33 40 @classmethod
34 41 def get_models(cls, api_key: str = None, base_url: str = None, timeout: int = None) -> list[str]:
35 42 if not cls.models:
36 43 try:
37 if base_url is None:
38 base_url = cls.base_url
39 44 if api_key is None and cls.api_key is not None:
40 45 api_key = cls.api_key
41 46 if not api_key:
42 47 api_key = AuthManager.load_api_key(cls)
48 if base_url is None:
49 base_url = cls.base_url if cls.is_provider_api_key(api_key) else cls.backup_url
43 50 if cls.models_needs_auth and not api_key:
44 51 raise MissingAuthError('Add a "api_key"')
45 52 response = requests.get(f"{base_url}/models", headers=cls.get_headers(False, api_key), verify=cls.ssl, timeout=timeout)
@@ -102,7 +109,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
102 109 ) as session:
103 110 model = cls.get_model(model, api_key=api_key, base_url=base_url)
104 111 if base_url is None:
105 base_url = cls.base_url
112 base_url = cls.base_url if cls.is_provider_api_key(api_key) else cls.backup_url
106 113
107 114 # Proxy for image generation feature
108 115 if model and model in cls.image_models:
@@ -165,6 +172,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
165 172 }
166 173
167 174 async def read_response(response: StreamResponse, stream: bool, prompt: str, provider_info: dict, download_media: bool) -> AsyncResult:
175 yield HeadersResponse.from_dict({key: value for key, value in response.headers.items() if key.lower().startswith("x-")})
168 176 content_type = response.headers.get("content-type", "text/event-stream" if stream else "application/json")
169 177 if content_type.startswith("application/json"):
170 178 data = await response.json()
Modified g4f/gui/server/api.py +2 -0
@@ -287,6 +287,8 @@ class Api:
287 287 yield self._format_json("response", chunk.get_dict())
288 288 elif isinstance(chunk, PlainTextResponse):
289 289 yield self._format_json("response", chunk.text)
290 elif isinstance(chunk, HeadersResponse):
291 yield self._format_json("headers", chunk.get_dict())
290 292 else:
291 293 yield self._format_json("content", str(chunk))
292 294 except MissingAuthError as e:
Modified g4f/providers/response.py +3 -0
@@ -178,6 +178,9 @@ class HiddenResponse(ResponseType):
178 178 def __str__(self) -> str:
179 179 """Hidden responses return an empty string."""
180 180 return ""
181
182 class HeadersResponse(HiddenResponse, ObjectMixin):
183 pass
181 184
182 185 class JsonRequest(HiddenResponse, ObjectMixin):
183 186 pass