返回提交历史
Modified
g4f/Provider/github/GithubCopilot.py
+44
-1
Modified
g4f/Provider/local/Ollama.py
+1
-5
Modified
g4f/cookies.py
+2
-1
Modified
g4f/gui/server/api.py
+2
-1
Modified
g4f/gui/server/backend_api.py
+57
-0
Modified
g4f/providers/any_provider.py
+10
-0
Modified
g4f/providers/config_provider.py
+12
-2
XFEstudio/gpt4free
feat: Implement OAuth flow for GitHub Copilot and enhance provider login handling
db8fbe8f
代码差异
7 个文件
+128
-10
@@ -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."""
@@ -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:
@@ -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":
@@ -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]:
@@ -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.
@@ -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:
@@ -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: