返回提交历史
Modified
.gitignore
+2
-1
Modified
g4f/Provider/github/GithubCopilot.py
+33
-1
Modified
g4f/Provider/github/copilotTokenProvider.py
+4
-3
Modified
g4f/Provider/needs_auth/Antigravity.py
+57
-12
Modified
g4f/Provider/needs_auth/GeminiCLI.py
+7
-0
Modified
g4f/gui/server/backend_api.py
+16
-0
Modified
g4f/gui/server/website.py
+7
-0
Modified
g4f/typing.py
+4
-2
Modified
requirements-slim.txt
+1
-1
Modified
requirements.txt
+1
-1
Modified
setup.py
+5
-3
XFEstudio/gpt4free
feat: add usage retrieval methods for GitHub Copilot and Antigravity providers, update Flask dependency to async version
3f5eca67
代码差异
11 个文件
+137
-24
@@ -10,4 +10,5 @@ models/models.json
10
10
pyvenv.cfg
11
11
lib64
12
12
/.idea
13
container/
13
container/
14
g4f.dev
@@ -4,14 +4,17 @@ import sys
4
4
import json
5
5
import time
6
6
import asyncio
7
import aiohttp
7
8
from pathlib import Path
8
9
from typing import Optional
9
10
10
11
from ...typing import Messages, AsyncResult
12
from ...errors import MissingAuthError
11
13
from ..template import OpenaiTemplate
12
14
from ...providers.asyncio import get_running_loop
13
from .copilotTokenProvider import CopilotTokenProvider, EDITOR_VERSION, EDITOR_PLUGIN_VERSION
15
from .copilotTokenProvider import CopilotTokenProvider, EDITOR_VERSION, EDITOR_PLUGIN_VERSION, USER_AGENT, API_VERSION
14
16
from .sharedTokenManager import TokenManagerError, SharedTokenManager
17
from .githubOAuth2 import GithubOAuth2Client
15
18
from .oauthFlow import launch_browser_for_oauth
16
19
17
20
@@ -233,6 +236,35 @@ class GithubCopilot(OpenaiTemplate):
233
236
pass
234
237
return None
235
238
239
@classmethod
240
async def get_usage(cls) -> dict:
241
"""
242
Fetch and summarize current GitHub Copilot usage/quota information.
243
Returns a dictionary with usage details or raises an exception on failure.
244
"""
245
client = GithubOAuth2Client()
246
github_creds = await client.sharedManager.getValidCredentials(client)
247
if not github_creds or not github_creds.get("access_token"):
248
raise MissingAuthError("No GitHub OAuth token available. Please login first.")
249
250
github_token = github_creds["access_token"]
251
url = f"https://api.github.com/copilot_internal/user"
252
headers = {
253
"Accept": "application/json",
254
"authorization": f"token {github_token}",
255
"editor-version": EDITOR_VERSION,
256
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
257
"user-agent": USER_AGENT,
258
"x-github-api-version": API_VERSION,
259
"x-vscode-user-agent-library-version": "electron-fetch",
260
}
261
async with aiohttp.ClientSession() as session:
262
async with session.get(url, headers=headers) as resp:
263
if resp.status != 200:
264
text = await resp.text()
265
raise RuntimeError(f"Failed to fetch Copilot usage: {resp.status} {text}")
266
usage = await resp.json()
267
return usage
236
268
237
269
async def main():
238
270
"""CLI entry point for GitHub Copilot OAuth authentication."""
@@ -14,7 +14,8 @@ from .sharedTokenManager import SharedTokenManager, TokenManagerError
14
14
# Editor/Plugin version headers required by Copilot API
15
15
EDITOR_VERSION = "vscode/1.95.0"
16
16
EDITOR_PLUGIN_VERSION = "copilot/1.250.0"
17
17
USER_AGENT = "GithubCopilot/1.250.0"
18
API_VERSION = "2024-12-15"
18
19
19
20
class CopilotTokenProvider:
20
21
"""Provides Copilot API tokens from GitHub OAuth credentials."""
@@ -55,11 +56,11 @@ class CopilotTokenProvider:
55
56
headers={
56
57
"Authorization": f"token {github_token}",
57
58
"Accept": "application/json",
58
"User-Agent": "GithubCopilot/1.250.0",
59
"User-Agent": USER_AGENT,
59
60
"Editor-Version": EDITOR_VERSION,
60
61
"Editor-Plugin-Version": EDITOR_PLUGIN_VERSION,
61
62
"Openai-Organization": "github-copilot",
62
"X-GitHub-Api-Version": "2024-12-15",
63
"X-GitHub-Api-Version": API_VERSION,
63
64
}
64
65
) as resp:
65
66
if resp.status == 401:
@@ -34,6 +34,7 @@ from ...errors import MissingAuthError
34
34
from ...image.copy_images import save_response_media
35
35
from ...image import to_bytes, is_data_an_media
36
36
from ...providers.response import Usage, ImageResponse, ToolCalls, Reasoning
37
from ...providers.asyncio import get_running_loop
37
38
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin
38
39
from ..helper import get_connector, get_system_prompt, format_media_prompt
39
40
from ... import debug
@@ -1255,16 +1256,8 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin):
1255
1256
# Try to fetch models dynamically if we have credentials
1256
1257
if not cls.models and cls.has_credentials():
1257
1258
try:
1258
import asyncio
1259
cls.models = asyncio.get_event_loop().run_until_complete(
1260
cls._fetch_models()
1261
)
1262
except RuntimeError:
1263
# No event loop running, try creating one
1264
try:
1265
cls.models = asyncio.run(cls._fetch_models())
1266
except Exception as e:
1267
debug.log(f"Failed to fetch dynamic models: {e}")
1259
get_running_loop(check_nested=True)
1260
cls.models = asyncio.run(cls._fetch_models())
1268
1261
except Exception as e:
1269
1262
debug.log(f"Failed to fetch dynamic models: {e}")
1270
1263
@@ -1275,7 +1268,7 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin):
1275
1268
if cls.auth_manager.get_access_token() is not None:
1276
1269
cls.live += 1
1277
1270
1278
return [m for m in cls.models if not m.startswith("chat_") and not m.startswith("tab_")] if cls.models else cls.fallback_models
1271
return cls.models if cls.models else cls.fallback_models
1279
1272
1280
1273
@classmethod
1281
1274
async def _fetch_models(cls) -> List[str]:
@@ -1292,7 +1285,7 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin):
1292
1285
)
1293
1286
1294
1287
# Extract model names from the response
1295
models = list(response.get("models", {}).keys())
1288
models = [key for key, value in response.get("models", {}).items() if not value.get("isInternal", False) and not key.startswith("tab_")]
1296
1289
if not isinstance(models, list):
1297
1290
raise ValueError("Invalid response format: 'models' should be a list")
1298
1291
@@ -1301,6 +1294,58 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin):
1301
1294
debug.log(f"Failed to fetch models: {e}")
1302
1295
return []
1303
1296
1297
@classmethod
1298
async def get_usage(cls) -> dict:
1299
"""
1300
Fetch and summarize quota usage for Antigravity account.
1301
Returns a dict with OpenAI Usage keys if possible, or quota info.
1302
"""
1303
if cls.auth_manager is None:
1304
cls.auth_manager = AntigravityAuthManager(env=os.environ)
1305
await cls.auth_manager.initialize_auth()
1306
1307
access_token = cls.auth_manager.get_access_token()
1308
project_id = cls.auth_manager.get_project_id()
1309
if not access_token or not project_id:
1310
raise MissingAuthError("Cannot fetch usage without valid authentication")
1311
1312
data = await cls.auth_manager.call_endpoint(
1313
method="fetchAvailableModels",
1314
body={"project": cls.auth_manager.get_project_id()}
1315
)
1316
1317
def classify_group(model_name, display_name=None):
1318
combined = f"{model_name} {display_name or ''}".lower()
1319
if "claude" in combined:
1320
return "claude"
1321
if "gemini-3" in combined or "gemini 3" in combined:
1322
if "flash" in combined:
1323
return "gemini-flash"
1324
return "gemini-pro"
1325
if "gemini-2.5" in combined or "gemini 2.5" in combined:
1326
if "flash" in combined:
1327
return "gemini-flash"
1328
return "gemini-pro"
1329
return None
1330
1331
groups = {}
1332
models = data.get("models", {})
1333
for model_name, entry in models.items():
1334
group = classify_group(model_name, entry.get("displayName") or entry.get("modelName"))
1335
if not group:
1336
continue
1337
quota_info = entry.get("quotaInfo", {})
1338
remaining = quota_info.get("remainingFraction")
1339
reset_time = quota_info.get("resetTime")
1340
if group not in groups:
1341
groups[group] = {"remainingFraction": remaining, "resetTime": reset_time, "modelCount": 1}
1342
else:
1343
g = groups[group]
1344
g["remainingFraction"] = min(g["remainingFraction"], remaining) if g["remainingFraction"] is not None and remaining is not None else g["remainingFraction"] or remaining
1345
g["resetTime"] = reset_time if not g["resetTime"] or (reset_time and reset_time < g["resetTime"]) else g["resetTime"]
1346
g["modelCount"] += 1
1347
return {**data, "groups": groups}
1348
1304
1349
@classmethod
1305
1350
async def create_async_generator(
1306
1351
cls,
@@ -854,6 +854,13 @@ class GeminiCLI(AsyncGeneratorProvider, ProviderModelMixin):
854
854
cls.models = [bucket["modelId"] for bucket in buckets.get("buckets", [])]
855
855
return cls.models if cls.models else cls.fallback_models
856
856
857
@classmethod
858
async def get_usage(cls) -> dict:
859
if cls.auth_manager is None:
860
cls.auth_manager = AuthManager(env=os.environ)
861
provider = GeminiCLIProvider(env=os.environ, auth_manager=cls.auth_manager)
862
return await provider.retrieve_user_quota()
863
857
864
@classmethod
858
865
async def create_async_generator(
859
866
cls,
@@ -253,6 +253,22 @@ class Backend_Api(Api):
253
253
else:
254
254
return (jsonify({"error": {"message": "No usage data found for this date"}}), 404)
255
255
256
@app.route('/backend-api/v2/quota/<provider>', methods=['GET'])
257
async def get_quota(provider: str):
258
try:
259
provider_handler = convert_to_provider(provider)
260
except ProviderNotFoundError:
261
return "Provider not found", 404
262
if not hasattr(provider_handler, "get_usage"):
263
return "Provider doesn't support get_usage", 500
264
try:
265
response_data = await provider_handler.getUsage()
266
return jsonify(response_data)
267
except MissingAuthError as e:
268
return jsonify({"error": {"message": f"{type(e).__name__}: {e}"}}), 401
269
except Exception as e:
270
return jsonify({"error": {"message": f"{type(e).__name__}: {e}"}}), 500
271
256
272
@app.route('/backend-api/v2/log', methods=['POST'])
257
273
def add_log():
258
274
cache_dir = Path(get_cookies_dir()) / ".logging"
@@ -98,6 +98,10 @@ class Website:
98
98
'function': self._background,
99
99
'methods': ['GET', 'POST']
100
100
},
101
'/home.html': {
102
'function': self._home,
103
'methods': ['GET', 'POST']
104
},
101
105
'/chat/<filename>': {
102
106
'function': self._chat,
103
107
'methods': ['GET', 'POST']
@@ -137,6 +141,9 @@ class Website:
137
141
def _background(self, filename = "background"):
138
142
return render(filename)
139
143
144
def _home(self, filename = "home"):
145
return render(filename)
146
140
147
def _chat(self, filename = ""):
141
148
filename = f"chat/{filename}" if filename else "chat/index"
142
149
return render(filename)
@@ -17,8 +17,10 @@ from typing import (
17
17
Optional,
18
18
TYPE_CHECKING,
19
19
)
20
from typing_extensions import TypedDict
21
20
try:
21
from typing_extensions import TypedDict
22
except ImportError:
23
from typing import TypedDict
22
24
# Only import PIL for type-checkers; no runtime dependency required.
23
25
if TYPE_CHECKING:
24
26
from PIL.Image import Image as PILImage
@@ -8,7 +8,7 @@ werkzeug
8
8
pillow
9
9
fastapi
10
10
uvicorn
11
flask
11
flask[async]
12
12
brotli
13
13
beautifulsoup4
14
14
aiohttp_socks
@@ -10,7 +10,7 @@ pillow
10
10
platformdirs
11
11
fastapi
12
12
uvicorn
13
flask
13
flask[async]
14
14
brotli
15
15
beautifulsoup4
16
16
setuptools
@@ -28,7 +28,8 @@ EXTRA_REQUIRE = {
28
28
"aiohttp_socks", # proxy
29
29
"pillow", # image
30
30
"cairosvg", # svg image
31
"werkzeug", "flask", # gui
31
"werkzeug",
32
"flask[async]", # gui
32
33
"fastapi", # api
33
34
"uvicorn", # api
34
35
"nodriver",
@@ -48,7 +49,8 @@ EXTRA_REQUIRE = {
48
49
"beautifulsoup4", # web_search and bing.create_images
49
50
"aiohttp_socks", # proxy
50
51
"pillow", # image
51
"werkzeug", "flask", # gui
52
"werkzeug",
53
"flask[async]", # gui
52
54
"fastapi", # api
53
55
"uvicorn", # api
54
56
"nodriver",
@@ -78,7 +80,7 @@ EXTRA_REQUIRE = {
78
80
"a2wsgi",
79
81
],
80
82
"gui": [
81
"werkzeug", "flask",
83
"werkzeug", "flask[async]",
82
84
"beautifulsoup4", "pillow",
83
85
],
84
86
"search": [