diff --git a/.gitignore b/.gitignore index b79960e8..cba5d74e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ models/models.json pyvenv.cfg lib64 /.idea -container/ \ No newline at end of file +container/ +g4f.dev \ No newline at end of file diff --git a/g4f/Provider/github/GithubCopilot.py b/g4f/Provider/github/GithubCopilot.py index 21fdf225..55747d2f 100644 --- a/g4f/Provider/github/GithubCopilot.py +++ b/g4f/Provider/github/GithubCopilot.py @@ -4,14 +4,17 @@ import sys import json import time import asyncio +import aiohttp from pathlib import Path from typing import Optional from ...typing import Messages, AsyncResult +from ...errors import MissingAuthError from ..template import OpenaiTemplate from ...providers.asyncio import get_running_loop -from .copilotTokenProvider import CopilotTokenProvider, EDITOR_VERSION, EDITOR_PLUGIN_VERSION +from .copilotTokenProvider import CopilotTokenProvider, EDITOR_VERSION, EDITOR_PLUGIN_VERSION, USER_AGENT, API_VERSION from .sharedTokenManager import TokenManagerError, SharedTokenManager +from .githubOAuth2 import GithubOAuth2Client from .oauthFlow import launch_browser_for_oauth @@ -233,6 +236,35 @@ class GithubCopilot(OpenaiTemplate): pass return None + @classmethod + async def get_usage(cls) -> dict: + """ + Fetch and summarize current GitHub Copilot usage/quota information. + Returns a dictionary with usage details or raises an exception on failure. + """ + client = GithubOAuth2Client() + github_creds = await client.sharedManager.getValidCredentials(client) + if not github_creds or not github_creds.get("access_token"): + raise MissingAuthError("No GitHub OAuth token available. Please login first.") + + github_token = github_creds["access_token"] + url = f"https://api.github.com/copilot_internal/user" + headers = { + "Accept": "application/json", + "authorization": f"token {github_token}", + "editor-version": EDITOR_VERSION, + "editor-plugin-version": EDITOR_PLUGIN_VERSION, + "user-agent": USER_AGENT, + "x-github-api-version": API_VERSION, + "x-vscode-user-agent-library-version": "electron-fetch", + } + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as resp: + if resp.status != 200: + text = await resp.text() + raise RuntimeError(f"Failed to fetch Copilot usage: {resp.status} {text}") + usage = await resp.json() + return usage async def main(): """CLI entry point for GitHub Copilot OAuth authentication.""" diff --git a/g4f/Provider/github/copilotTokenProvider.py b/g4f/Provider/github/copilotTokenProvider.py index 2f5f5334..e465f617 100644 --- a/g4f/Provider/github/copilotTokenProvider.py +++ b/g4f/Provider/github/copilotTokenProvider.py @@ -14,7 +14,8 @@ from .sharedTokenManager import SharedTokenManager, TokenManagerError # Editor/Plugin version headers required by Copilot API EDITOR_VERSION = "vscode/1.95.0" EDITOR_PLUGIN_VERSION = "copilot/1.250.0" - +USER_AGENT = "GithubCopilot/1.250.0" +API_VERSION = "2024-12-15" class CopilotTokenProvider: """Provides Copilot API tokens from GitHub OAuth credentials.""" @@ -55,11 +56,11 @@ class CopilotTokenProvider: headers={ "Authorization": f"token {github_token}", "Accept": "application/json", - "User-Agent": "GithubCopilot/1.250.0", + "User-Agent": USER_AGENT, "Editor-Version": EDITOR_VERSION, "Editor-Plugin-Version": EDITOR_PLUGIN_VERSION, "Openai-Organization": "github-copilot", - "X-GitHub-Api-Version": "2024-12-15", + "X-GitHub-Api-Version": API_VERSION, } ) as resp: if resp.status == 401: diff --git a/g4f/Provider/needs_auth/Antigravity.py b/g4f/Provider/needs_auth/Antigravity.py index 3e0869df..d39b238d 100644 --- a/g4f/Provider/needs_auth/Antigravity.py +++ b/g4f/Provider/needs_auth/Antigravity.py @@ -34,6 +34,7 @@ from ...errors import MissingAuthError from ...image.copy_images import save_response_media from ...image import to_bytes, is_data_an_media from ...providers.response import Usage, ImageResponse, ToolCalls, Reasoning +from ...providers.asyncio import get_running_loop from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin from ..helper import get_connector, get_system_prompt, format_media_prompt from ... import debug @@ -1255,16 +1256,8 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin): # Try to fetch models dynamically if we have credentials if not cls.models and cls.has_credentials(): try: - import asyncio - cls.models = asyncio.get_event_loop().run_until_complete( - cls._fetch_models() - ) - except RuntimeError: - # No event loop running, try creating one - try: - cls.models = asyncio.run(cls._fetch_models()) - except Exception as e: - debug.log(f"Failed to fetch dynamic models: {e}") + get_running_loop(check_nested=True) + cls.models = asyncio.run(cls._fetch_models()) except Exception as e: debug.log(f"Failed to fetch dynamic models: {e}") @@ -1275,7 +1268,7 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin): if cls.auth_manager.get_access_token() is not None: cls.live += 1 - return [m for m in cls.models if not m.startswith("chat_") and not m.startswith("tab_")] if cls.models else cls.fallback_models + return cls.models if cls.models else cls.fallback_models @classmethod async def _fetch_models(cls) -> List[str]: @@ -1292,7 +1285,7 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin): ) # Extract model names from the response - models = list(response.get("models", {}).keys()) + models = [key for key, value in response.get("models", {}).items() if not value.get("isInternal", False) and not key.startswith("tab_")] if not isinstance(models, list): raise ValueError("Invalid response format: 'models' should be a list") @@ -1301,6 +1294,58 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin): debug.log(f"Failed to fetch models: {e}") return [] + @classmethod + async def get_usage(cls) -> dict: + """ + Fetch and summarize quota usage for Antigravity account. + Returns a dict with OpenAI Usage keys if possible, or quota info. + """ + if cls.auth_manager is None: + cls.auth_manager = AntigravityAuthManager(env=os.environ) + await cls.auth_manager.initialize_auth() + + access_token = cls.auth_manager.get_access_token() + project_id = cls.auth_manager.get_project_id() + if not access_token or not project_id: + raise MissingAuthError("Cannot fetch usage without valid authentication") + + data = await cls.auth_manager.call_endpoint( + method="fetchAvailableModels", + body={"project": cls.auth_manager.get_project_id()} + ) + + def classify_group(model_name, display_name=None): + combined = f"{model_name} {display_name or ''}".lower() + if "claude" in combined: + return "claude" + if "gemini-3" in combined or "gemini 3" in combined: + if "flash" in combined: + return "gemini-flash" + return "gemini-pro" + if "gemini-2.5" in combined or "gemini 2.5" in combined: + if "flash" in combined: + return "gemini-flash" + return "gemini-pro" + return None + + groups = {} + models = data.get("models", {}) + for model_name, entry in models.items(): + group = classify_group(model_name, entry.get("displayName") or entry.get("modelName")) + if not group: + continue + quota_info = entry.get("quotaInfo", {}) + remaining = quota_info.get("remainingFraction") + reset_time = quota_info.get("resetTime") + if group not in groups: + groups[group] = {"remainingFraction": remaining, "resetTime": reset_time, "modelCount": 1} + else: + g = groups[group] + g["remainingFraction"] = min(g["remainingFraction"], remaining) if g["remainingFraction"] is not None and remaining is not None else g["remainingFraction"] or remaining + g["resetTime"] = reset_time if not g["resetTime"] or (reset_time and reset_time < g["resetTime"]) else g["resetTime"] + g["modelCount"] += 1 + return {**data, "groups": groups} + @classmethod async def create_async_generator( cls, diff --git a/g4f/Provider/needs_auth/GeminiCLI.py b/g4f/Provider/needs_auth/GeminiCLI.py index 8fedb678..5eaf4aa2 100644 --- a/g4f/Provider/needs_auth/GeminiCLI.py +++ b/g4f/Provider/needs_auth/GeminiCLI.py @@ -854,6 +854,13 @@ class GeminiCLI(AsyncGeneratorProvider, ProviderModelMixin): cls.models = [bucket["modelId"] for bucket in buckets.get("buckets", [])] return cls.models if cls.models else cls.fallback_models + @classmethod + async def get_usage(cls) -> dict: + if cls.auth_manager is None: + cls.auth_manager = AuthManager(env=os.environ) + provider = GeminiCLIProvider(env=os.environ, auth_manager=cls.auth_manager) + return await provider.retrieve_user_quota() + @classmethod async def create_async_generator( cls, diff --git a/g4f/gui/server/backend_api.py b/g4f/gui/server/backend_api.py index b3333146..727454b5 100644 --- a/g4f/gui/server/backend_api.py +++ b/g4f/gui/server/backend_api.py @@ -253,6 +253,22 @@ class Backend_Api(Api): else: return (jsonify({"error": {"message": "No usage data found for this date"}}), 404) + @app.route('/backend-api/v2/quota/', methods=['GET']) + async def get_quota(provider: str): + try: + provider_handler = convert_to_provider(provider) + except ProviderNotFoundError: + return "Provider not found", 404 + if not hasattr(provider_handler, "get_usage"): + return "Provider doesn't support get_usage", 500 + try: + response_data = await provider_handler.getUsage() + return jsonify(response_data) + except MissingAuthError as e: + return jsonify({"error": {"message": f"{type(e).__name__}: {e}"}}), 401 + except Exception as e: + return jsonify({"error": {"message": f"{type(e).__name__}: {e}"}}), 500 + @app.route('/backend-api/v2/log', methods=['POST']) def add_log(): cache_dir = Path(get_cookies_dir()) / ".logging" diff --git a/g4f/gui/server/website.py b/g4f/gui/server/website.py index 723740f3..b377d6b3 100644 --- a/g4f/gui/server/website.py +++ b/g4f/gui/server/website.py @@ -98,6 +98,10 @@ class Website: 'function': self._background, 'methods': ['GET', 'POST'] }, + '/home.html': { + 'function': self._home, + 'methods': ['GET', 'POST'] + }, '/chat/': { 'function': self._chat, 'methods': ['GET', 'POST'] @@ -137,6 +141,9 @@ class Website: def _background(self, filename = "background"): return render(filename) + def _home(self, filename = "home"): + return render(filename) + def _chat(self, filename = ""): filename = f"chat/{filename}" if filename else "chat/index" return render(filename) diff --git a/g4f/typing.py b/g4f/typing.py index 2ce4db58..77548fc8 100644 --- a/g4f/typing.py +++ b/g4f/typing.py @@ -17,8 +17,10 @@ from typing import ( Optional, TYPE_CHECKING, ) -from typing_extensions import TypedDict - +try: + from typing_extensions import TypedDict +except ImportError: + from typing import TypedDict # Only import PIL for type-checkers; no runtime dependency required. if TYPE_CHECKING: from PIL.Image import Image as PILImage diff --git a/requirements-slim.txt b/requirements-slim.txt index e952c6d3..3b070d38 100644 --- a/requirements-slim.txt +++ b/requirements-slim.txt @@ -8,7 +8,7 @@ werkzeug pillow fastapi uvicorn -flask +flask[async] brotli beautifulsoup4 aiohttp_socks diff --git a/requirements.txt b/requirements.txt index 0697966e..50d534fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ pillow platformdirs fastapi uvicorn -flask +flask[async] brotli beautifulsoup4 setuptools diff --git a/setup.py b/setup.py index fb8a6fe1..eb759f8a 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,8 @@ EXTRA_REQUIRE = { "aiohttp_socks", # proxy "pillow", # image "cairosvg", # svg image - "werkzeug", "flask", # gui + "werkzeug", + "flask[async]", # gui "fastapi", # api "uvicorn", # api "nodriver", @@ -48,7 +49,8 @@ EXTRA_REQUIRE = { "beautifulsoup4", # web_search and bing.create_images "aiohttp_socks", # proxy "pillow", # image - "werkzeug", "flask", # gui + "werkzeug", + "flask[async]", # gui "fastapi", # api "uvicorn", # api "nodriver", @@ -78,7 +80,7 @@ EXTRA_REQUIRE = { "a2wsgi", ], "gui": [ - "werkzeug", "flask", + "werkzeug", "flask[async]", "beautifulsoup4", "pillow", ], "search": [