mirror of
https://github.com/xtekky/gpt4free.git
synced 2026-04-22 15:47:11 +08:00
feat: add usage retrieval methods for GitHub Copilot and Antigravity providers, update Flask dependency to async version
This commit is contained in:
+2
-1
@@ -10,4 +10,5 @@ models/models.json
|
||||
pyvenv.cfg
|
||||
lib64
|
||||
/.idea
|
||||
container/
|
||||
container/
|
||||
g4f.dev
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/<provider>', 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"
|
||||
|
||||
@@ -98,6 +98,10 @@ class Website:
|
||||
'function': self._background,
|
||||
'methods': ['GET', 'POST']
|
||||
},
|
||||
'/home.html': {
|
||||
'function': self._home,
|
||||
'methods': ['GET', 'POST']
|
||||
},
|
||||
'/chat/<filename>': {
|
||||
'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)
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ werkzeug
|
||||
pillow
|
||||
fastapi
|
||||
uvicorn
|
||||
flask
|
||||
flask[async]
|
||||
brotli
|
||||
beautifulsoup4
|
||||
aiohttp_socks
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ pillow
|
||||
platformdirs
|
||||
fastapi
|
||||
uvicorn
|
||||
flask
|
||||
flask[async]
|
||||
brotli
|
||||
beautifulsoup4
|
||||
setuptools
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user