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

XFEstudio/gpt4free

feat: Implement OAuth flow for GitHub Copilot and enhance provider login handling

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

代码差异

7 个文件 +128 -10
Modified g4f/Provider/github/GithubCopilot.py +44 -1
@@ -14,7 +14,7 @@ from ..template import OpenaiTemplate
14 14 from ...providers.asyncio import get_running_loop
15 15 from .copilotTokenProvider import CopilotTokenProvider, EDITOR_VERSION, EDITOR_PLUGIN_VERSION, USER_AGENT, API_VERSION
16 16 from .sharedTokenManager import TokenManagerError, SharedTokenManager
17 from .githubOAuth2 import GithubOAuth2Client
17 from .githubOAuth2 import GithubOAuth2Client, GITHUB_COPILOT_SCOPE
18 18 from .oauthFlow import launch_browser_for_oauth
19 19
20 20 class GithubCopilot(OpenaiTemplate):
@@ -218,6 +218,49 @@ class GithubCopilot(OpenaiTemplate):
218 218
219 219 return shared_manager
220 220
221 @classmethod
222 async def oauth_start(cls):
223 """Initiate Copilot device code flow and return code/url to client."""
224 client = GithubOAuth2Client()
225 device_auth = await client.requestDeviceAuthorization({"scope": GITHUB_COPILOT_SCOPE})
226
227 verification_uri = device_auth.get("verification_uri", "https://github.com/login/device")
228 user_code = device_auth.get("user_code")
229 device_code = device_auth.get("device_code")
230
231 return {
232 "status": "pending",
233 "verification_uri": verification_uri,
234 "user_code": user_code,
235 "device_code": device_code,
236 "expires_in": device_auth.get("expires_in"),
237 "interval": device_auth.get("interval", 5),
238 }
239
240 @classmethod
241 async def oauth_poll(cls, device_code: str):
242 """Poll GitHub token endpoint; save credentials once access_token is available."""
243 if not device_code:
244 raise ValueError("device_code is required for polling")
245
246 client = GithubOAuth2Client()
247 token_response = await client.pollDeviceToken({"device_code": device_code})
248
249 if token_response.get("status") == "pending":
250 return {"status": "pending", "message": "Authorization pending"}
251
252 if token_response.get("access_token"):
253 credentials = {
254 "access_token": token_response["access_token"],
255 "token_type": token_response.get("token_type", "bearer"),
256 "scope": token_response.get("scope", ""),
257 "expiry_date": int(time.time() * 1000) + (365 * 24 * 60 * 60 * 1000),
258 }
259 await client.sharedManager.saveCredentialsToFile(credentials)
260 return {"status": "success", "message": "GitHub Copilot OAuth successful"}
261
262 return {"status": "error", "message": "Unexpected token response", "detail": token_response}
263
221 264 @classmethod
222 265 def has_credentials(cls) -> bool:
223 266 """Check if valid credentials exist."""
Modified g4f/Provider/local/Ollama.py +1 -5
@@ -35,11 +35,7 @@ class Ollama(OpenaiTemplate):
35 35 if api_key:
36 36 cookies = {"__Secure-session": api_key}
37 37 else:
38 api_key = AuthManager.load_api_key(cls)
39 if api_key:
40 cookies = {"__Secure-session": api_key}
41 else:
42 cookies = get_cookies("ollama.com", raise_requirements_error=False)
38 cookies = get_cookies("ollama.com", cache_result=False)
43 39 if not cookies:
44 40 return None
45 41 try:
Modified g4f/cookies.py +2 -1
@@ -81,7 +81,8 @@ COOKIE_DOMAINS = (
81 81 "github.com",
82 82 "yupp.ai",
83 83 "chat.deepseek.com",
84 ".perplexity.ai"
84 ".perplexity.ai",
85 "ollama.com"
85 86 )
86 87
87 88 if has_browser_cookie3 and os.environ.get("DBUS_SESSION_BUS_ADDRESS", "/dev/null") == "/dev/null":
Modified g4f/gui/server/api.py +2 -1
@@ -111,7 +111,8 @@ class Api:
111 111 "active_by_default": False if provider.active_by_default is None else provider.active_by_default,
112 112 "auth": provider.needs_auth,
113 113 "login_url": getattr(provider, "login_url", None),
114 "live": provider.live
114 "live": provider.live,
115 "login": hasattr(provider, "login")
115 116 } for provider in Provider.__providers__ if provider.working and safe_get_models(provider)]
116 117
117 118 def get_all_models(self) -> dict[str, list]:
Modified g4f/gui/server/backend_api.py +57 -0
@@ -53,6 +53,7 @@ from ...client.service import get_model_and_provider
53 53 from ...providers.any_model_map import model_map
54 54 from ... import Provider
55 55 from ... import models
56 from ...Provider import ProviderUtils
56 57 from .api import Api
57 58
58 59 logger = logging.getLogger(__name__)
@@ -178,6 +179,62 @@ class Backend_Api(Api):
178 179 response = self.get_providers(**kwargs)
179 180 return jsonify(response)
180 181
182 @app.route('/backend-api/v2/oauth/<provider>', methods=['GET', 'POST'])
183 def oauth_login(provider: str):
184 timeout = 300.0
185 if request.method == 'GET':
186 timeout = float(request.args.get('timeout') or timeout)
187 else:
188 try:
189 data = request.get_json(silent=True) or {}
190 timeout = float(data.get('timeout') or timeout)
191 except Exception:
192 pass
193
194 # Resolve provider class
195 try:
196 provider_class = ProviderUtils.get_by_label(provider)
197 except ValueError as e:
198 return jsonify({"error": {"message": str(e)}}), 404
199
200 if request.method == 'GET':
201 data = request.args.to_dict() or {}
202 else:
203 data = request.get_json(silent=True) or {}
204
205 action = data.get("action", "start")
206
207 # Github Copilot device flow: start/poll actions
208 if hasattr(provider_class, "oauth_start") and action == "start":
209 try:
210 result = asyncio.run(provider_class.oauth_start())
211 return jsonify(result), 200
212 except Exception as e:
213 logger.exception(e)
214 return jsonify({"error": {"message": str(e)}}), 500
215
216 if hasattr(provider_class, "oauth_poll") and action == "poll":
217 device_code = data.get("device_code")
218 if not device_code:
219 return jsonify({"error": {"message": "device_code is required for poll action"}}), 400
220 try:
221 result = asyncio.run(provider_class.oauth_poll(device_code))
222 return jsonify(result), 200
223 except Exception as e:
224 logger.exception(e)
225 return jsonify({"error": {"message": str(e)}}), 500
226
227 # Fallback: provider.login (blocking) for interactive login flows
228 if hasattr(provider_class, "login"):
229 try:
230 asyncio.run(provider_class.login())
231 return jsonify({"status": "success"}), 200
232 except Exception as e:
233 logger.exception(e)
234 return jsonify({"error": {"message": str(e)}}), 500
235
236 return jsonify({"error": {"message": f"Provider {provider} does not support OAuth login"}}), 404
237
181 238 def handle_conversation():
182 239 """
183 240 Handles conversation requests and streams responses back.
Modified g4f/providers/any_provider.py +10 -0
@@ -7,6 +7,7 @@ from ..typing import AsyncResult, Messages, MediaListType, Union
7 7 from ..errors import ModelNotFoundError
8 8 from ..image import is_data_an_audio
9 9 from ..providers.retry_provider import RotatedProvider
10 from ..providers.config_provider import RouterConfig, ConfigModelProvider
10 11 from ..Provider.needs_auth import OpenaiChat, CopilotAccount
11 12 from ..Provider.hf_space import HuggingSpace
12 13 from ..Provider import (
@@ -87,6 +88,7 @@ PROVIDERS_LIST_3 = [
87 88
88 89 LABELS = {
89 90 "default": "Default",
91 "custom": "Custom Routes",
90 92 "openai": "OpenAI: ChatGPT",
91 93 "llama": "Meta: LLaMA",
92 94 "deepseek": "DeepSeek",
@@ -342,6 +344,8 @@ class AnyModelProviderMixin(ProviderModelMixin):
342 344 # Always add default first
343 345 groups["default"].append("default")
344 346
347 groups["custom"] = list(RouterConfig.routes.keys())
348
345 349 for model in unsorted_models:
346 350 if model == "default":
347 351 continue # Already added
@@ -466,6 +470,12 @@ class AnyProvider(AsyncGeneratorProvider, AnyModelProviderMixin):
466 470 providers = models.default_vision.best_provider.providers
467 471 else:
468 472 providers = models.default.best_provider.providers
473 elif model in RouterConfig.routes:
474 async for chunk in ConfigModelProvider(RouterConfig.routes.get(model)).create_async_generator(
475 model, messages, stream=stream, media=media, api_key=api_key, **kwargs
476 ):
477 yield chunk
478 return
469 479 elif model in Provider.__map__:
470 480 provider = Provider.__map__[model]
471 481 if provider.working and provider.get_parent() not in ignored:
Modified g4f/providers/config_provider.py +12 -2
@@ -70,6 +70,8 @@ from ..typing import Messages, AsyncResult
70 70 from .base_provider import AsyncGeneratorProvider
71 71 from .response import ProviderInfo
72 72 from .. import debug
73 from ..config import AppConfig
74 from ..tools.auth import AuthManager
73 75
74 76 # ---------------------------------------------------------------------------
75 77 # Quota cache
@@ -505,6 +507,7 @@ class ConfigModelProvider(AsyncGeneratorProvider):
505 507 self,
506 508 model: str,
507 509 messages: Messages,
510 api_key: Optional[Dict[str, str]] = None,
508 511 **kwargs,
509 512 ) -> AsyncResult:
510 513 """Yield response chunks, routing through configured providers."""
@@ -541,15 +544,22 @@ class ConfigModelProvider(AsyncGeneratorProvider):
541 544 model=target_model,
542 545 )
543 546
547 extra_body = kwargs.copy()
548 current_api_key = api_key.get(provider.get_parent()) if isinstance(api_key, dict) else api_key
549 if not current_api_key or AppConfig.disable_custom_api_key:
550 current_api_key = AuthManager.load_api_key(provider)
551 if current_api_key:
552 extra_body["api_key"] = current_api_key
553
544 554 try:
545 555 if hasattr(provider, "create_async_generator"):
546 556 async for chunk in provider.create_async_generator(
547 target_model, messages, **kwargs
557 target_model, messages, **extra_body
548 558 ):
549 559 yield chunk
550 560 elif hasattr(provider, "create_completion"):
551 561 for chunk in provider.create_completion(
552 target_model, messages, stream=True, **kwargs
562 target_model, messages, **extra_body
553 563 ):
554 564 yield chunk
555 565 else: