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

XFEstudio/gpt4free

feat: add usage retrieval methods for GitHub Copilot and Antigravity providers, update Flask dependency to async version

3f5eca67
hlohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

11 个文件 +137 -24
Modified .gitignore +2 -1
@@ -10,4 +10,5 @@ models/models.json
10 10 pyvenv.cfg
11 11 lib64
12 12 /.idea
13 container/
13 container/
14 g4f.dev
Modified g4f/Provider/github/GithubCopilot.py +33 -1
@@ -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."""
Modified g4f/Provider/github/copilotTokenProvider.py +4 -3
@@ -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:
Modified g4f/Provider/needs_auth/Antigravity.py +57 -12
@@ -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,
Modified g4f/Provider/needs_auth/GeminiCLI.py +7 -0
@@ -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,
Modified g4f/gui/server/backend_api.py +16 -0
@@ -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"
Modified g4f/gui/server/website.py +7 -0
@@ -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)
Modified g4f/typing.py +4 -2
@@ -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
Modified requirements-slim.txt +1 -1
@@ -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
Modified requirements.txt +1 -1
@@ -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
Modified setup.py +5 -3
@@ -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": [