返回提交历史
Modified
g4f/Provider/github/GithubCopilot.py
+15
-11
Modified
g4f/Provider/needs_auth/Antigravity.py
+75
-28
Modified
g4f/Provider/needs_auth/GeminiCLI.py
+39
-3
Modified
g4f/cli/client.py
+9
-2
Modified
g4f/client/__init__.py
+1
-1
XFEstudio/gpt4free
Updates
3810e9a7
代码差异
5 个文件
+139
-45
@@ -14,7 +14,6 @@ from .copilotTokenProvider import CopilotTokenProvider, EDITOR_VERSION, EDITOR_P
14
14
from .sharedTokenManager import TokenManagerError, SharedTokenManager
15
15
from .oauthFlow import launch_browser_for_oauth
16
16
17
18
17
class GithubCopilot(OpenaiTemplate):
19
18
"""
20
19
GitHub Copilot provider with OAuth authentication.
@@ -113,8 +112,8 @@ class GithubCopilot(OpenaiTemplate):
113
112
cls,
114
113
model: str,
115
114
messages: Messages,
116
api_key: str = None,
117
base_url: str = None,
115
api_key: Optional[str] = None,
116
base_url: Optional[str] = None,
118
117
**kwargs
119
118
) -> AsyncResult:
120
119
"""
@@ -129,13 +128,18 @@ class GithubCopilot(OpenaiTemplate):
129
128
token_provider = cls._get_token_provider()
130
129
creds = await token_provider.get_valid_token()
131
130
api_key = creds.get("token")
131
if not api_key:
132
raise RuntimeError(
133
"GitHub Copilot OAuth not configured. "
134
"Please run 'g4f auth github-copilot' to authenticate."
135
)
132
136
if not base_url:
133
137
base_url = creds.get("endpoint", cls.base_url)
134
138
except TokenManagerError as e:
135
139
if "login" in str(e).lower() or "credentials" in str(e).lower():
136
140
raise RuntimeError(
137
141
"GitHub Copilot OAuth not configured. "
138
"Please run 'g4f-github-copilot login' to authenticate."
142
"Please run 'g4f auth github-copilot' to authenticate."
139
143
) from e
140
144
raise
141
145
@@ -150,7 +154,7 @@ class GithubCopilot(OpenaiTemplate):
150
154
yield chunk
151
155
152
156
@classmethod
153
def get_models(cls, api_key = None, base_url = None, timeout = None):
157
def get_models(cls, api_key: Optional[str] = None, base_url: Optional[str] = None, timeout: Optional[int] = None):
154
158
# If no API key provided, use OAuth token
155
159
if api_key is None:
156
160
try:
@@ -164,24 +168,24 @@ class GithubCopilot(OpenaiTemplate):
164
168
if "login" in str(e).lower() or "credentials" in str(e).lower():
165
169
raise RuntimeError(
166
170
"GitHub Copilot OAuth not configured. "
167
"Please run 'g4f-github-copilot login' to authenticate."
171
"Please run 'g4f auth github-copilot' to authenticate."
168
172
) from e
169
173
raise
170
174
return super().get_models(api_key, base_url, timeout)
171
175
172
176
@classmethod
173
def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict:
174
headers = super().get_headers(stream, api_key, headers)
177
def get_headers(cls, stream: bool, api_key: str | None = None, headers: dict[str, str] | None = None) -> dict[str, str]:
178
headers_result = super().get_headers(stream, api_key or "", headers or {})
175
179
# Add required Copilot headers
176
copilot_headers = {
180
copilot_headers: dict[str, str] = {
177
181
"Editor-Version": EDITOR_VERSION,
178
182
"Editor-Plugin-Version": EDITOR_PLUGIN_VERSION,
179
183
"Openai-Organization": "github-copilot",
180
184
"Copilot-Integration-Id": "vscode-chat",
181
185
"X-GitHub-Api-Version": "2024-12-15",
182
186
}
183
if headers:
184
copilot_headers.update(headers)
187
if headers_result:
188
copilot_headers.update(headers_result)
185
189
return copilot_headers
186
190
187
191
@classmethod
@@ -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
@@ -650,7 +651,57 @@ class AntigravityAuthManager(AuthFileMixin):
650
651
except Exception as e:
651
652
debug.log(f"Project discovery failed at {base_url}: {e}")
652
653
continue
653
654
# If discovery failed, attempt to onboard a managed project for the user.
655
# Read optional configuration from environment
656
attempts = int(os.environ.get("ANTIGRAVITY_ONBOARD_ATTEMPTS", "10"))
657
delay_seconds = float(os.environ.get("ANTIGRAVITY_ONBOARD_DELAY_S", "5"))
658
tier_id = os.environ.get("ANTIGRAVITY_TIER_ID", "free-tier")
659
# Use any preconfigured project id as metadata if available
660
configured_project = os.environ.get("ANTIGRAVITY_PROJECT_ID", "")
661
662
if tier_id:
663
onboard_request_body = {"tierId": tier_id, "metadata": {}}
664
if configured_project:
665
# include requested project id in metadata
666
onboard_request_body["metadata"]["cloudaicompanionProject"] = configured_project
667
668
# Try onboarding across endpoints with retries
669
for base_url in BASE_URLS:
670
for attempt in range(attempts):
671
try:
672
url = f"{base_url}:onboardUser"
673
onboard_headers = {
674
"Authorization": f"Bearer {access_token}",
675
"Content-Type": "application/json",
676
**ANTIGRAVITY_HEADERS,
677
}
678
async with session.post(url, headers=onboard_headers, json=onboard_request_body, timeout=timeout) as resp:
679
if not resp.ok:
680
print(f"Onboarding attempt {attempt+1} at {base_url} failed with status {resp.status}")
681
print(await resp.text())
682
# Stop attempts on this endpoint and try next base_url
683
break
684
685
payload = await resp.json()
686
# payload.response?.cloudaicompanionProject?.id
687
response_obj = payload.get("response") or {}
688
managed = response_obj.get("cloudaicompanionProject")
689
if isinstance(managed, dict):
690
managed_id = managed.get("id")
691
else:
692
managed_id = None
693
694
done = bool(payload.get("done", False))
695
if done and managed_id:
696
return managed_id
697
if done and configured_project:
698
return configured_project
699
except Exception as e:
700
debug.log(f"Failed to onboard managed project at {base_url}: {e}")
701
break
702
703
await asyncio.sleep(delay_seconds)
704
654
705
return ""
655
706
656
707
@classmethod
@@ -857,24 +908,18 @@ class AntigravityProvider:
857
908
858
909
# Fall back to API discovery
859
910
try:
860
load_response = await self.auth_manager.call_endpoint(
861
"loadCodeAssist",
862
{
863
"cloudaicompanionProject": "default-project",
864
"metadata": {"duetProject": "default-project"},
865
},
866
use_auth_headers=True,
867
)
868
869
# Handle both string and object formats for cloudaicompanionProject
870
project = load_response.get("cloudaicompanionProject")
871
if isinstance(project, dict):
872
project = project.get("id")
911
access_token = self.auth_manager.get_access_token()
912
if not access_token:
913
raise RuntimeError("No valid access token available for project discovery")
873
914
915
async with aiohttp.ClientSession() as session:
916
project = await self.auth_manager._fetch_project_id(
917
session=session,
918
access_token=access_token
919
)
874
920
if project:
875
921
self._project_id = project
876
922
return project
877
878
923
raise RuntimeError(
879
924
"Project ID discovery failed - set ANTIGRAVITY_PROJECT_ID in environment."
880
925
)
@@ -1142,6 +1187,17 @@ class AntigravityProvider:
1142
1187
1143
1188
async with ClientSession(headers=headers, timeout=timeout, connector=connector) as session:
1144
1189
async with session.post(url, json=req_body) as resp:
1190
if not resp.ok:
1191
if resp.status == 503:
1192
try:
1193
max_retry_delay = int(max([d.get("retryDelay", 0) for d in (await resp.json(content_type=None)).get("error", {}).get("details", [])]))
1194
except ValueError:
1195
max_retry_delay = 30 # Default retry delay if not specified
1196
debug.log(f"Received 503 error, retrying after {max_retry_delay}")
1197
await asyncio.sleep(max_retry_delay)
1198
resp = await session.post(url, json=req_body)
1199
if not resp.ok:
1200
debug.error(f"Retry after 503 failed with status {resp.status}")
1145
1201
if not resp.ok:
1146
1202
if resp.status == 401:
1147
1203
raise MissingAuthError("Unauthorized (401) from Antigravity API")
@@ -1220,14 +1276,13 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin):
1220
1276
login_url = "https://cloud.google.com/code-assist"
1221
1277
url = "https://antigravity.google"
1222
1278
1223
default_model = "gemini-3-pro-preview"
1279
default_model = "gemini-3-flash"
1224
1280
fallback_models = [
1225
1281
# Gemini 2.5 models
1226
1282
"gemini-2.5-pro",
1227
1283
"gemini-2.5-flash",
1228
1284
"gemini-2.5-flash-lite",
1229
1285
# Gemini 3 models
1230
"gemini-3-pro-preview",
1231
1286
"gemini-3-flash",
1232
1287
# Claude models (via Antigravity proxy)
1233
1288
"claude-sonnet-4.5",
@@ -1247,7 +1302,6 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin):
1247
1302
active_by_default = True
1248
1303
1249
1304
auth_manager: AntigravityAuthManager = None
1250
_dynamic_models: List[str] = None
1251
1305
1252
1306
@classmethod
1253
1307
def get_models(cls, **kwargs) -> List[str]:
@@ -1255,16 +1309,9 @@ class Antigravity(AsyncGeneratorProvider, ProviderModelMixin):
1255
1309
# Try to fetch models dynamically if we have credentials
1256
1310
if not cls.models and cls.has_credentials():
1257
1311
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}")
1312
# Try to reuse current loop, fallback to asyncio.run when required
1313
get_running_loop(check_nested=True)
1314
cls.models = asyncio.run(cls._fetch_models())
1268
1315
except Exception as e:
1269
1316
debug.log(f"Failed to fetch dynamic models: {e}")
1270
1317
@@ -9,6 +9,7 @@ import hashlib
9
9
import asyncio
10
10
import webbrowser
11
11
import threading
12
import platform
12
13
from pathlib import Path
13
14
from typing import Any, AsyncGenerator, Dict, List, Optional, Union, Tuple
14
15
from urllib.parse import urlencode, parse_qs, urlparse
@@ -22,6 +23,7 @@ from ...errors import MissingAuthError
22
23
from ...image.copy_images import save_response_media
23
24
from ...image import to_bytes, is_data_an_media
24
25
from ...providers.response import Usage, ImageResponse, ToolCalls, Reasoning
26
from ...providers.asyncio import get_running_loop
25
27
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin
26
28
from ..helper import get_connector, get_system_prompt, format_media_prompt
27
29
from ... import debug
@@ -56,6 +58,24 @@ def encode_oauth_state(verifier: str) -> str:
56
58
return base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
57
59
58
60
61
def get_gemini_cli_user_agent(model: str = "gemini-2.5-pro") -> str:
62
"""Generate Gemini CLI user-agent string with platform and architecture.
63
64
This user-agent is required to access CLI quota buckets instead of Antigravity buckets.
65
"""
66
system_platform = platform.system().lower()
67
if system_platform == "darwin":
68
system_platform = "macos"
69
70
arch = platform.machine().lower()
71
if arch == "x86_64":
72
arch = "x64"
73
elif arch == "arm64" or arch == "aarch64":
74
arch = "arm64"
75
76
return f"GeminiCLI/1.0.0/{model} ({system_platform}; {arch})"
77
78
59
79
def decode_oauth_state(state: str) -> Dict[str, str]:
60
80
"""Decode OAuth state parameter back to verifier."""
61
81
padded = state + '=' * (4 - len(state) % 4) if len(state) % 4 else state
@@ -369,11 +389,23 @@ class AuthManager(AuthFileMixin):
369
389
return self._access_token
370
390
return None
371
391
372
async def call_endpoint(self, method: str, body: Dict[str, Any], is_retry=False) -> Any:
392
async def call_endpoint(
393
self,
394
method: str,
395
body: Dict[str, Any],
396
is_retry=False,
397
user_agent: Optional[str] = None,
398
) -> Any:
373
399
"""
374
400
Call Google Code Assist API endpoint with JSON body.
375
401
376
402
Automatically retries once on 401 Unauthorized by refreshing auth.
403
404
Args:
405
method: API method name
406
body: Request body
407
is_retry: Whether this is a retry attempt
408
user_agent: Custom user-agent string (defaults to standard one)
377
409
"""
378
410
if not self.get_access_token():
379
411
await self.initialize_auth()
@@ -383,6 +415,8 @@ class AuthManager(AuthFileMixin):
383
415
"Content-Type": "application/json",
384
416
"Authorization": f"Bearer {self.get_access_token()}",
385
417
}
418
if user_agent:
419
headers["User-Agent"] = user_agent
386
420
387
421
async with aiohttp.ClientSession() as session:
388
422
async with session.post(url, headers=headers, json=body) as resp:
@@ -390,7 +424,9 @@ class AuthManager(AuthFileMixin):
390
424
# Token likely expired, clear and retry once
391
425
await self.clear_token_cache()
392
426
await self.initialize_auth()
393
return await self.call_endpoint(method, body, is_retry=True)
427
return await self.call_endpoint(
428
method, body, is_retry=True, user_agent=user_agent
429
)
394
430
elif not resp.ok:
395
431
text = await resp.text()
396
432
raise RuntimeError(f"API call failed with status {resp.status}: {text}")
@@ -840,7 +876,7 @@ class GeminiCLI(AsyncGeneratorProvider, ProviderModelMixin):
840
876
needs_auth = True
841
877
active_by_default = True
842
878
843
auth_manager: AuthManager = None
879
auth_manager: Optional[AuthManager] = None
844
880
845
881
@classmethod
846
882
def get_models(cls, **kwargs):
@@ -10,7 +10,7 @@ import requests
10
10
11
11
from pathlib import Path
12
12
from typing import Optional, List, Dict
13
from g4f.client import AsyncClient
13
from g4f.client import AsyncClient, ClientFactory
14
14
from g4f.providers.response import JsonConversation, MediaResponse, is_content
15
15
from g4f.cookies import set_cookies_dir, read_cookie_files
16
16
from g4f.Provider import ProviderUtils
@@ -253,7 +253,14 @@ async def run_args(input_val, args):
253
253
set_cookies_dir(str(args.cookies_dir))
254
254
read_cookie_files()
255
255
256
client = AsyncClient(provider=conv.provider)
256
client = ClientFactory.create_async_client(provider=conv.provider)
257
258
if input_val == "models":
259
models = client.models.get_all()
260
print("\nAvailable models:")
261
for m in models:
262
print(f"- {m}")
263
return
257
264
258
265
if isinstance(args.edit, Path):
259
266
file_to_edit = args.edit
@@ -929,7 +929,7 @@ class ClientFactory:
929
929
)
930
930
"""
931
931
return AsyncClient(
932
provider=cls.create_provider(provider, base_url, api_key, **kwargs),
932
provider=cls.create_provider(None, provider, base_url, api_key, **kwargs),
933
933
media_provider=media_provider,
934
934
api_key=api_key,
935
935
base_url=base_url,