返回提交历史
Modified
g4f/Provider/local/Ollama.py
+1
-0
Modified
g4f/Provider/template/OpenaiTemplate.py
+1
-3
Modified
g4f/api/__init__.py
+31
-10
Modified
g4f/requests/cdp.py
+27
-302
XFEstudio/gpt4free
Add AuthManager import to Ollama and scroll to top before screenshot capture
5c1c143d
代码差异
4 个文件
+60
-315
@@ -12,6 +12,7 @@ from ...cookies import get_cookies
12
12
from ...typing import AsyncResult, Messages
13
13
from ...config import AppConfig
14
14
from ...errors import MissingAuthError
15
from ...tools.auth import AuthManager
15
16
16
17
17
18
class Ollama(OpenaiTemplate):
@@ -51,9 +51,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
51
51
return await super().get_quota(api_key=api_key, **kwargs)
52
52
if not api_key and cls.needs_auth:
53
53
raise MissingAuthError("API key is required.")
54
if not cls.default_model and not cls.backup_url:
55
raise NotImplementedError("No default model specified.")
56
return await cls.test_api_key(api_key)
54
raise NotImplementedError("Quota URL is not defined for this provider.")
57
55
58
56
@classmethod
59
57
async def test_api_key(cls, api_key: str):
@@ -132,6 +132,36 @@ except ImportError:
132
132
133
133
logger = logging.getLogger(__name__)
134
134
135
V1_LANDING_PAGE = """<!doctype html>
136
<html lang="en">
137
<head>
138
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
139
<meta name="theme-color" content="#101411"><title>g4f /v1 API</title>
140
<style>
141
:root{--bg:#101411;--panel:#171d18;--ink:#f5f4ed;--muted:#a7afa4;--line:#344138;--lime:#c4f06c;--coral:#ff816d;--mono:ui-monospace,SFMono-Regular,Consolas,monospace}
142
*{box-sizing:border-box}body{margin:0;color:var(--ink);background:var(--bg);font:16px/1.55 Georgia,serif}body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.2;background-image:linear-gradient(#344138 1px,transparent 1px),linear-gradient(90deg,#344138 1px,transparent 1px);background-size:42px 42px;mask-image:linear-gradient(#000,transparent 72%)}a{color:var(--lime)}.wrap{width:min(1120px,calc(100% - 40px));margin:auto;position:relative}
143
header{display:flex;justify-content:space-between;align-items:center;padding:28px 0;border-bottom:1px solid var(--line)}.brand{color:var(--ink);text-decoration:none;font:700 18px/1 var(--mono);letter-spacing:-.04em}.brand b{color:var(--lime)}nav{display:flex;gap:22px;font:13px var(--mono)}nav a{color:var(--muted);text-decoration:none}nav a:hover{color:var(--lime)}
144
.hero{padding:96px 0 88px;display:grid;grid-template-columns:1.2fr .8fr;gap:70px;align-items:end}.eyebrow{color:var(--coral);font:12px var(--mono);letter-spacing:.12em;text-transform:uppercase}h1{max-width:720px;margin:20px 0;font-size:clamp(48px,8vw,94px);line-height:.94;letter-spacing:-.065em;font-weight:400}.lede{max-width:590px;color:var(--muted);font-size:20px}.hero-aside{border-left:2px solid var(--lime);padding-left:22px;color:var(--muted)}.hero-aside strong{display:block;color:var(--ink);font:700 14px var(--mono);margin-bottom:12px}.actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:30px}.button{display:inline-block;padding:11px 16px;border:1px solid var(--lime);color:var(--bg);background:var(--lime);font:700 13px var(--mono);text-decoration:none}.button.alt{color:var(--lime);background:transparent}
145
section{padding:28px 0 92px}.section-head{display:flex;justify-content:space-between;align-items:baseline;gap:20px;margin-bottom:24px;border-bottom:1px solid var(--line);padding-bottom:14px}h2{margin:0;font-size:28px;font-weight:400;letter-spacing:-.04em}.section-head span{color:var(--muted);font:12px var(--mono)}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.card{display:block;padding:23px;border:1px solid var(--line);background:rgba(23,29,24,.88);min-height:174px;text-decoration:none}.card:hover{border-color:var(--lime)}.card h3{margin:0 0 8px;color:var(--ink);font-size:21px;font-weight:400}.card p{margin:0 0 18px;color:var(--muted)}code,pre{font-family:var(--mono)}.endpoint{color:var(--lime);font-size:12px}
146
.code-box{position:relative;border:1px solid var(--line);background:#0b0e0c;padding:24px;overflow:auto}pre{margin:0;color:#d9e5d2;font-size:13px;line-height:1.7}.token{color:var(--coral)}.copy{position:absolute;top:12px;right:12px;border:1px solid var(--line);color:var(--lime);background:transparent;padding:7px 10px;cursor:pointer;font:11px var(--mono)}footer{padding:22px 0 42px;color:var(--muted);border-top:1px solid var(--line);font:12px var(--mono)}
147
@media(max-width:720px){.wrap{width:min(100% - 28px,560px)}header{align-items:flex-start;gap:18px}nav{gap:10px;flex-wrap:wrap;justify-content:flex-end}.hero{padding:68px 0 58px;display:block}h1{font-size:clamp(50px,16vw,78px)}.hero-aside{margin-top:42px}.grid{grid-template-columns:1fr}section{padding-bottom:60px}}
148
</style></head>
149
<body><div class="wrap">
150
<header><a class="brand" href="/"><b>g4f</b> / api</a><nav><a href="/v1/models">models</a><a href="/docs">swagger</a><a href="/redoc">redoc</a></nav></header>
151
<main><div class="hero"><div><div class="eyebrow">Unified inference gateway · v1</div><h1>One endpoint.<br>Many minds.</h1><p class="lede">Build with chat, reasoning, vision, image, audio, and Anthropic-compatible interfaces through a single g4f API.</p><div class="actions"><a class="button" href="/docs">Open API reference</a><a class="button alt" href="/v1/models">Browse models</a></div></div><aside class="hero-aside"><strong>BASE URL</strong><code>/v1</code><p>Drop it into an OpenAI client, keep your existing request shape, and switch models without rewriting your application.</p></aside></div>
152
<section><div class="section-head"><h2>Interfaces</h2><span>three ways in</span></div><div class="grid">
153
<a class="card" href="/docs"><h3>Chat completions</h3><p>OpenAI-compatible messages with streaming, tools, vision, and model selection.</p><span class="endpoint">POST /v1/chat/completions</span></a>
154
<a class="card" href="/docs"><h3>Responses</h3><p>A modern response interface for multi-turn reasoning and richer output types.</p><span class="endpoint">POST /v1/responses</span></a>
155
<a class="card" href="/docs"><h3>Messages</h3><p>Anthropic-style requests for teams moving between compatible providers.</p><span class="endpoint">POST /v1/messages</span></a>
156
<a class="card" href="/docs"><h3>Media generation</h3><p>Generate images and other supported media from the same API surface.</p><span class="endpoint">POST /v1/media/generate</span></a>
157
<a class="card" href="/v1/models"><h3>Model catalog</h3><p>Inspect the models and provider options available on this server right now.</p><span class="endpoint">GET /v1/models</span></a>
158
<a class="card" href="/docs"><h3>Live schema</h3><p>Try requests in your browser and inspect the generated OpenAPI contract.</p><span class="endpoint">GET /docs</span></a>
159
</div></section><section><div class="section-head"><h2>Start here</h2><span>curl · JSON · streamable</span></div><div class="code-box"><button class="copy" onclick="copyExample(this)">copy</button><pre id="example">curl -X POST /v1/chat/completions \\
160
-H 'Content-Type: application/json' \\
161
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello"}]}'</pre></div></section></main>
162
<footer>g4f API · <a href="/openapi.json">openapi.json</a> · Compatible clients welcome.</footer></div>
163
<script>function copyExample(button){navigator.clipboard?.writeText(document.getElementById("example").textContent).then(()=>{button.textContent="copied";setTimeout(()=>button.textContent="copy",1400)})}</script></body></html>"""
164
135
165
# ---------------------------------------------------------------------------
136
166
# Request / response log store
137
167
# ---------------------------------------------------------------------------
@@ -669,16 +699,7 @@ class Api:
669
699
670
700
@self.app.get("/v1")
671
701
async def read_root_v1():
672
return HTMLResponse(
673
"g4f API: Go to "
674
'<a href="/v1/models">models</a>, '
675
'<a href="/v1/chat/completions">chat/completions</a>, '
676
'<a href="/v1/responses">responses</a> (OpenAI), '
677
'<a href="/v1/messages">messages</a> (Anthropic), or '
678
'<a href="/v1/media/generate">media/generate</a> <br><br>'
679
"Open Swagger UI at: "
680
'<a href="/docs">/docs</a>'
681
)
702
return HTMLResponse(V1_LANDING_PAGE)
682
703
683
704
@self.app.get(
684
705
"/v1/models",
@@ -1011,6 +1011,7 @@ if (deepseekSendButton) {
1011
1011
raise RuntimeError(f"Failed to load page {url} for screenshot, document.doctype={await self.evaluate_js('String(document.doctype)')}")
1012
1012
1013
1013
await self.bypass_turnstile()
1014
await self.evaluate_js("window.scrollTo(0, 0);")
1014
1015
1015
1016
result = None
1016
1017
for i in range(n):
@@ -1022,6 +1023,31 @@ if (deepseekSendButton) {
1022
1023
except Exception as e:
1023
1024
debug.log(f"Screenshot #{i+1} failed: {e}")
1024
1025
return result
1026
1027
# response = await self.evaluate_js("""
1028
# new Promise((resolve, reject) => {
1029
# const html2canvasEL = document.createElement('script');
1030
# html2canvasEL.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js';
1031
# html2canvasEL.onload = async () => {
1032
# c=await html2canvas(document.body, {/*width: 1200, height: 630*/});
1033
# c.toBlob(async (b)=>{
1034
# const url = "https://media.pollinations.ai/upload";
1035
# const formData = new FormData();
1036
# formData.append('file', b);
1037
# const response = await fetch(url, {
1038
# method: 'POST',
1039
# body: formData,
1040
# headers: {"Authorization": "Bearer pk_7X0QLj0xijSd0xj7"}
1041
# });
1042
# resolve(await response.json())
1043
# }, 'image/webp');
1044
# };
1045
# html2canvasEL.onerror = (e) => { reject(e); };
1046
# document.head.appendChild(html2canvasEL);
1047
# """)
1048
# async with aiohttp.ClientSession() as session:
1049
# async with session.get(response['url']) as resp:
1050
# image_bytes = await resp.read()
1025
1051
1026
1052
async def _capture_screenshot_impl(self, url: str, n: int) -> str:
1027
1053
url_without_suffix = url[:-7] if url.endswith("_2.webp") or url.endswith("_3.webp") else url
@@ -1106,305 +1132,4 @@ if (deepseekSendButton) {
1106
1132
self.target_id = None
1107
1133
1108
1134
# Release our tab; browser stays alive for reuse by other tabs
1109
release_shared_browser_ref()
1110
1111
1112
class SyncCDPSession:
1113
"""
1114
Synchronous (blocking) CDP client for use in Turnstile-solving providers
1115
(e.g. DeepInfra) where async timeout-based approach is unreliable.
1116
1117
Unlike CDPSession which uses asyncio+aiohttp and futures with timeouts,
1118
this class uses a simple blocking while-loop recv() model — it will wait
1119
as long as the browser needs without the risk of a premature TimeoutError.
1120
1121
Requires: pip install websocket-client
1122
1123
Example Usage (run from an async context via executor):
1124
def run_sync():
1125
session = SyncCDPSession(port=12345, headless=False)
1126
session.start_chrome()
1127
try:
1128
session.navigate("https://example.com")
1129
title = session.evaluate_js("document.title")
1130
return title
1131
finally:
1132
session.close()
1133
1134
result = await asyncio.get_event_loop().run_in_executor(None, run_sync)
1135
"""
1136
1137
def __init__(
1138
self,
1139
port: Optional[int] = None,
1140
host: Optional[str] = None,
1141
user_data_dir: Optional[str] = None,
1142
headless: bool = None,
1143
):
1144
if port is None:
1145
port = BrowserConfig.port
1146
if host is None:
1147
host = BrowserConfig.host
1148
if host is None:
1149
host = "127.0.0.1"
1150
if headless is None:
1151
headless = BrowserConfig.headless
1152
self.port = port
1153
self.host = host
1154
self.headless = headless
1155
self.user_data_dir = (
1156
user_data_dir # Ignored if using shared pool, but kept for compatibility
1157
)
1158
self.process = None
1159
self.ws = None
1160
self.target_id = None
1161
self.id_counter = 0
1162
1163
# Network event loggers
1164
self.network_requests: List[dict] = []
1165
self.network_responses: List[dict] = []
1166
1167
def start_chrome(self):
1168
"""Launch/get shared Chrome and connect via CDP targeting a new tab."""
1169
if self.port is None:
1170
self.port = get_shared_browser(self.host, self.port, self.headless)
1171
1172
# Acquire a reference so the shared browser stays alive for this tab
1173
acquire_shared_browser_ref()
1174
1175
# Create a new tab target
1176
ws_url = None
1177
for _ in range(10):
1178
try:
1179
req = urllib.request.Request(
1180
f"http://{self.host}:{self.port}/json/new", method="PUT"
1181
)
1182
with urllib.request.urlopen(req, timeout=2) as response:
1183
target = json.loads(response.read().decode("utf-8"))
1184
ws_url = target.get("webSocketDebuggerUrl")
1185
self.target_id = target.get("id")
1186
if ws_url:
1187
break
1188
except Exception:
1189
time.sleep(0.5)
1190
1191
if not ws_url:
1192
release_shared_browser_ref()
1193
raise RuntimeError(f"Failed to create new tab target on port {self.port}")
1194
1195
self._connect(ws_url)
1196
1197
def _connect(self, ws_url: str):
1198
"""Connect via WebSocket to the target."""
1199
try:
1200
from websocket import create_connection
1201
except ImportError:
1202
raise ImportError(
1203
'Install "websocket-client" package: pip install websocket-client'
1204
)
1205
1206
self.ws = create_connection(ws_url)
1207
self.ws.settimeout(60) # Prevent infinite hang if Chrome crashes mid-call
1208
1209
# Enable essential CDP domains
1210
self.call("Page.enable")
1211
self.call("DOM.enable")
1212
self.call("Runtime.enable")
1213
self.call("Network.enable")
1214
self.call("Emulation.setFocusEmulationEnabled", enabled=True)
1215
1216
def call(self, method: str, **params) -> dict:
1217
"""Send a CDP command and block until the matching response arrives, logging events."""
1218
self.id_counter += 1
1219
payload = {"id": self.id_counter, "method": method, "params": params}
1220
self.ws.send(json.dumps(payload))
1221
1222
# Blocking loop with a 60s socket timeout — won't hang forever if browser exits
1223
while True:
1224
response = json.loads(self.ws.recv())
1225
if "id" in response:
1226
if response.get("id") == self.id_counter:
1227
if "error" in response:
1228
raise RuntimeError(
1229
f"CDP error in {method}: {response['error']}"
1230
)
1231
return response.get("result", {})
1232
else:
1233
# Event
1234
event_method = response.get("method")
1235
event_params = response.get("params", {})
1236
if event_method == "Network.requestWillBeSent":
1237
self.network_requests.append(event_params)
1238
elif event_method == "Network.responseReceived":
1239
self.network_responses.append(event_params)
1240
1241
def evaluate_js(self, expression: str) -> Any:
1242
"""Execute JS on the page and return the primitive result value."""
1243
res = self.call("Runtime.evaluate", expression=expression, returnByValue=True)
1244
return res.get("result", {}).get("value")
1245
1246
def get_cookies(self) -> dict:
1247
"""Retrieve all cookies from the browser as a name-value dict."""
1248
cookies = self.get_cookies_list()
1249
return {c["name"]: c["value"] for c in cookies}
1250
1251
def get_cookies_list(self, urls: Optional[List[str]] = None) -> List[dict]:
1252
"""Retrieve full cookie objects from the browser session."""
1253
params = {}
1254
if urls:
1255
params["urls"] = urls
1256
res = self.call("Network.getCookies", **params)
1257
return res.get("cookies", [])
1258
1259
def set_cookies(self, cookies: List[dict]):
1260
"""Set cookies in the browser session."""
1261
for cookie in cookies:
1262
params = {
1263
"name": cookie.get("name"),
1264
"value": cookie.get("value"),
1265
"domain": cookie.get("domain"),
1266
"path": cookie.get("path"),
1267
"secure": cookie.get("secure"),
1268
"httpOnly": cookie.get("httpOnly"),
1269
"sameSite": cookie.get("sameSite"),
1270
"expires": cookie.get("expires"),
1271
}
1272
params = {k: v for k, v in params.items() if v is not None}
1273
if "domain" not in params and "url" not in params:
1274
params["url"] = "https://deepinfra.com"
1275
self.call("Network.setCookie", **params)
1276
1277
def navigate(self, url: str):
1278
"""Navigate to a URL and wait for initial load."""
1279
self.call("Page.navigate", url=url)
1280
time.sleep(2.0)
1281
1282
def wait_for_network_idle(self, idle_time: float = 0.5, timeout: float = 15.0) -> bool:
1283
"""Wait until network activity settles (no in-flight requests for *idle_time* seconds).
1284
1285
Polls document.readyState and the Performance Resource Timing API to detect
1286
when resource loading has stabilised. Returns True when idle, False on timeout.
1287
"""
1288
deadline = time.monotonic() + timeout
1289
last_count = -1
1290
stable_since = time.monotonic()
1291
1292
while time.monotonic() < deadline:
1293
try:
1294
ready = self.evaluate_js("document.readyState")
1295
if ready == "complete":
1296
# Count resources that are still loading (responseStart > 0 but no responseEnd)
1297
count = self.evaluate_js(
1298
"""(() => {
1299
const entries = performance.getEntriesByType('resource');
1300
let pending = 0;
1301
for (const e of entries) {
1302
if (e.responseStart > 0 && e.responseEnd === 0) {
1303
pending++;
1304
}
1305
}
1306
return pending;
1307
})()"""
1308
)
1309
count = count or 0
1310
if count == last_count:
1311
if (time.monotonic() - stable_since) >= idle_time:
1312
return True
1313
else:
1314
last_count = count
1315
stable_since = time.monotonic()
1316
else:
1317
# Page not fully loaded yet — reset stability timer
1318
last_count = -1
1319
stable_since = time.monotonic()
1320
except Exception:
1321
pass
1322
time.sleep(0.2)
1323
1324
return False
1325
1326
def click(self, x: int = 200, y: int = 400):
1327
"""
1328
Simulate a real mouse click at (x, y) on the page.
1329
1330
Gives the page window focus, which signals Cloudflare that a real user
1331
is present. This significantly speeds up Turnstile token generation.
1332
Call this once after navigate(), before polling for the token.
1333
"""
1334
self.call(
1335
"Input.dispatchMouseEvent",
1336
type="mousePressed",
1337
x=x,
1338
y=y,
1339
button="left",
1340
clickCount=1,
1341
)
1342
self.call(
1343
"Input.dispatchMouseEvent",
1344
type="mouseReleased",
1345
x=x,
1346
y=y,
1347
button="left",
1348
clickCount=1,
1349
)
1350
1351
def close(self):
1352
"""Close WebSocket session and close this tab only.
1353
1354
The shared browser process is kept alive as long as other CDP sessions
1355
(tabs) are active. When the last session releases its reference the
1356
browser is terminated automatically.
1357
"""
1358
if self.ws:
1359
try:
1360
self.ws.close()
1361
except Exception:
1362
pass
1363
self.ws = None
1364
1365
if self.target_id and self.port:
1366
try:
1367
urllib.request.urlopen(
1368
f"http://{self.host}:{self.port}/json/close/{self.target_id}",
1369
timeout=2,
1370
)
1371
except Exception:
1372
pass
1373
self.target_id = None
1374
1375
# Release our tab; browser stays alive for reuse by other tabs
1376
release_shared_browser_ref()
1377
1378
def capture_screenshot(self, url: str) -> bytes:
1379
"""Navigate to a URL and capture a screenshot, caching the result."""
1380
datekey = datetime.date.today().isoformat()
1381
screenshots_dir = get_screenshot_dir(datekey)
1382
filename = f"{secure_filename(url)}.webp"
1383
filepath = os.path.join(screenshots_dir, filename)
1384
1385
if os.path.exists(filepath):
1386
return Path(filepath).read_bytes()
1387
1388
self.navigate(url)
1389
# Wait for network activity to settle before capturing
1390
self.wait_for_network_idle()
1391
# Try to click any "Accept" or "Einwilligen" cookie consent buttons
1392
self.click_accept_button()
1393
time.sleep(0.5)
1394
result = self.call("Page.captureScreenshot")
1395
image_bytes = base64.b64decode(result["data"])
1396
1397
# Resize to 1200x675 and save as WebP to reduce file size
1398
if has_pillow:
1399
from io import BytesIO
1400
image = Image.open(BytesIO(image_bytes))
1401
image = image.resize((1200, 675), Image.Resampling.LANCZOS)
1402
width, height = image.size
1403
image = image.crop((0, 0, max(0, width - 10), height))
1404
image = image.convert("RGB")
1405
output = BytesIO()
1406
image.save(output, format="WEBP", quality=85, method=6)
1407
image_bytes = output.getvalue()
1408
1409
Path(filepath).write_bytes(image_bytes)
1410
return image_bytes
1135
release_shared_browser_ref()