browser-use 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -18
- package/dist/actor/element.js +24 -3
- package/dist/actor/mouse.js +21 -3
- package/dist/actor/page.js +33 -11
- package/dist/agent/gif.js +28 -3
- package/dist/agent/message-manager/service.js +2 -22
- package/dist/agent/message-manager/utils.js +15 -2
- package/dist/agent/message-manager/views.d.ts +7 -7
- package/dist/agent/message-manager/views.js +1 -0
- package/dist/agent/prompts.d.ts +3 -0
- package/dist/agent/prompts.js +22 -12
- package/dist/agent/service.d.ts +9 -1
- package/dist/agent/service.js +204 -79
- package/dist/agent/system_prompt.md +12 -11
- package/dist/agent/system_prompt_anthropic_flash.md +6 -5
- package/dist/agent/system_prompt_no_thinking.md +12 -11
- package/dist/agent/views.d.ts +2 -0
- package/dist/agent/views.js +48 -36
- package/dist/browser/extensions.js +20 -10
- package/dist/browser/profile.d.ts +4 -0
- package/dist/browser/profile.js +107 -4
- package/dist/browser/session.d.ts +28 -1
- package/dist/browser/session.js +1436 -528
- package/dist/browser/watchdogs/default-action-watchdog.js +32 -3
- package/dist/browser/watchdogs/downloads-watchdog.d.ts +4 -0
- package/dist/browser/watchdogs/downloads-watchdog.js +105 -9
- package/dist/browser/watchdogs/har-recording-watchdog.d.ts +1 -0
- package/dist/browser/watchdogs/har-recording-watchdog.js +54 -2
- package/dist/browser/watchdogs/permissions-watchdog.d.ts +5 -0
- package/dist/browser/watchdogs/permissions-watchdog.js +106 -3
- package/dist/browser/watchdogs/recording-watchdog.d.ts +2 -0
- package/dist/browser/watchdogs/recording-watchdog.js +54 -2
- package/dist/browser/watchdogs/security-watchdog.d.ts +1 -0
- package/dist/browser/watchdogs/security-watchdog.js +47 -7
- package/dist/browser/watchdogs/storage-state-watchdog.d.ts +6 -0
- package/dist/browser/watchdogs/storage-state-watchdog.js +206 -14
- package/dist/cli.d.ts +13 -2
- package/dist/cli.js +190 -9
- package/dist/code-use/namespace.js +52 -7
- package/dist/code-use/notebook-export.js +18 -2
- package/dist/code-use/service.js +1 -0
- package/dist/config.js +26 -4
- package/dist/controller/action-timeout.d.ts +9 -0
- package/dist/controller/action-timeout.js +95 -0
- package/dist/controller/registry/service.d.ts +1 -0
- package/dist/controller/registry/service.js +28 -1
- package/dist/controller/service.d.ts +2 -1
- package/dist/controller/service.js +494 -329
- package/dist/entrypoint.d.ts +1 -0
- package/dist/entrypoint.js +27 -0
- package/dist/filesystem/file-system.js +38 -8
- package/dist/integrations/gmail/service.js +30 -6
- package/dist/llm/browser-use/chat.js +2 -2
- package/dist/llm/codex/auth.d.ts +118 -0
- package/dist/llm/codex/auth.js +599 -0
- package/dist/llm/codex/chat.d.ts +70 -0
- package/dist/llm/codex/chat.js +392 -0
- package/dist/llm/codex/index.d.ts +2 -0
- package/dist/llm/codex/index.js +2 -0
- package/dist/llm/google/chat.js +18 -1
- package/dist/logging-config.js +22 -11
- package/dist/mcp/client.d.ts +1 -0
- package/dist/mcp/client.js +12 -10
- package/dist/mcp/redaction.d.ts +3 -0
- package/dist/mcp/redaction.js +132 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +64 -22
- package/dist/screenshots/service.js +25 -2
- package/dist/skill-cli/direct.d.ts +4 -1
- package/dist/skill-cli/direct.js +263 -66
- package/dist/skill-cli/server.d.ts +1 -0
- package/dist/skill-cli/server.js +115 -25
- package/dist/skill-cli/tunnel.d.ts +1 -0
- package/dist/skill-cli/tunnel.js +16 -4
- package/dist/sync/auth.js +22 -9
- package/dist/telemetry/service.js +21 -2
- package/dist/telemetry/views.js +31 -8
- package/dist/tokens/custom-pricing.js +2 -2
- package/dist/tokens/openrouter-pricing.d.ts +11 -0
- package/dist/tokens/openrouter-pricing.js +102 -0
- package/dist/tokens/service.js +20 -16
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +3 -1
- package/package.json +68 -27
package/dist/skill-cli/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { promises as fsp } from 'node:fs';
|
|
1
|
+
import fs, { promises as fsp } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Request, Response } from './protocol.js';
|
|
4
4
|
import { SessionRegistry } from './sessions.js';
|
|
@@ -6,6 +6,22 @@ const normalizeCookieDomain = (value) => String(value ?? '')
|
|
|
6
6
|
.trim()
|
|
7
7
|
.replace(/^\./, '')
|
|
8
8
|
.toLowerCase();
|
|
9
|
+
const chmodPrivateFile = (filePath) => {
|
|
10
|
+
if (process.platform !== 'win32') {
|
|
11
|
+
fs.chmodSync(filePath, 0o600);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const writePrivateJsonFile = async (filePath, data) => {
|
|
15
|
+
await fsp.writeFile(filePath, JSON.stringify(data, null, 2), {
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
mode: 0o600,
|
|
18
|
+
});
|
|
19
|
+
chmodPrivateFile(filePath);
|
|
20
|
+
};
|
|
21
|
+
const writePrivateBinaryFile = async (filePath, data) => {
|
|
22
|
+
await fsp.writeFile(filePath, data, { mode: 0o600 });
|
|
23
|
+
chmodPrivateFile(filePath);
|
|
24
|
+
};
|
|
9
25
|
const parseCookieHostname = (url) => {
|
|
10
26
|
const value = String(url ?? '').trim();
|
|
11
27
|
if (!value) {
|
|
@@ -49,9 +65,7 @@ const cookieMatchesUrl = (cookie, url) => {
|
|
|
49
65
|
if (!hostname || !domain) {
|
|
50
66
|
return false;
|
|
51
67
|
}
|
|
52
|
-
if (!(hostname === domain ||
|
|
53
|
-
hostname.endsWith(`.${domain}`) ||
|
|
54
|
-
domain.endsWith(`.${hostname}`))) {
|
|
68
|
+
if (!(hostname === domain || hostname.endsWith(`.${domain}`))) {
|
|
55
69
|
return false;
|
|
56
70
|
}
|
|
57
71
|
if (!cookiePathMatches(cookie.path, parsedUrl?.pathname || '/')) {
|
|
@@ -84,6 +98,33 @@ const parseCookieExpires = (value) => {
|
|
|
84
98
|
const parsed = Number(value);
|
|
85
99
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
86
100
|
};
|
|
101
|
+
const getCookieDenialReason = (session, cookie) => {
|
|
102
|
+
const checker = session?._get_cookie_access_denial_reason;
|
|
103
|
+
if (typeof checker !== 'function') {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return checker.call(session, cookie);
|
|
107
|
+
};
|
|
108
|
+
const filterAllowedCookies = (session, cookies) => cookies.filter((cookie) => !getCookieDenialReason(session, cookie));
|
|
109
|
+
const partitionAllowedCookies = (session, cookies) => {
|
|
110
|
+
const allowedCookies = [];
|
|
111
|
+
const blockedCookies = [];
|
|
112
|
+
for (const cookie of cookies) {
|
|
113
|
+
if (getCookieDenialReason(session, cookie)) {
|
|
114
|
+
blockedCookies.push(cookie);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
allowedCookies.push(cookie);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { allowedCookies, blockedCookies };
|
|
121
|
+
};
|
|
122
|
+
const assertCookieUrlAllowed = (session, url) => {
|
|
123
|
+
const denialReason = getCookieDenialReason(session, { url });
|
|
124
|
+
if (denialReason) {
|
|
125
|
+
throw new Error(`Cookie URL blocked by domain policy: ${denialReason}`);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
87
128
|
export class SkillCliServer {
|
|
88
129
|
registry;
|
|
89
130
|
constructor(options = {}) {
|
|
@@ -102,6 +143,16 @@ export class SkillCliServer {
|
|
|
102
143
|
}
|
|
103
144
|
return node;
|
|
104
145
|
}
|
|
146
|
+
async _run_with_page_validation(browser_session, action) {
|
|
147
|
+
const page = await browser_session.get_current_page?.();
|
|
148
|
+
await browser_session.validate_page_after_action?.(page);
|
|
149
|
+
try {
|
|
150
|
+
return await action();
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await browser_session.validate_page_after_action?.(page);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
105
156
|
async _read_node_data(browser_session, node, kind) {
|
|
106
157
|
if (!node?.xpath) {
|
|
107
158
|
throw new Error('DOM element does not include an XPath selector');
|
|
@@ -110,7 +161,7 @@ export class SkillCliServer {
|
|
|
110
161
|
if (!page?.evaluate) {
|
|
111
162
|
throw new Error('No active page available');
|
|
112
163
|
}
|
|
113
|
-
return await page.evaluate(({ xpath, dataKind }) => {
|
|
164
|
+
return await this._run_with_page_validation(browser_session, () => page.evaluate(({ xpath, dataKind }) => {
|
|
114
165
|
const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
115
166
|
if (!element) {
|
|
116
167
|
return null;
|
|
@@ -143,7 +194,7 @@ export class SkillCliServer {
|
|
|
143
194
|
};
|
|
144
195
|
}
|
|
145
196
|
return null;
|
|
146
|
-
}, { xpath: node.xpath, dataKind: kind });
|
|
197
|
+
}, { xpath: node.xpath, dataKind: kind }));
|
|
147
198
|
}
|
|
148
199
|
async _handle_browser_action(action, sessionName, params) {
|
|
149
200
|
const session = await this.registry.get_or_create_session(sessionName);
|
|
@@ -176,7 +227,7 @@ export class SkillCliServer {
|
|
|
176
227
|
if (!locator?.hover) {
|
|
177
228
|
throw new Error('Hover is not available for this element');
|
|
178
229
|
}
|
|
179
|
-
await locator.hover({ timeout: 5000 });
|
|
230
|
+
await this._run_with_page_validation(browser_session, async () => locator.hover({ timeout: 5000 }));
|
|
180
231
|
return { hovered: Number(params.index) };
|
|
181
232
|
}
|
|
182
233
|
if (action === 'dblclick') {
|
|
@@ -188,7 +239,7 @@ export class SkillCliServer {
|
|
|
188
239
|
if (!locator?.dblclick) {
|
|
189
240
|
throw new Error('Double-click is not available for this element');
|
|
190
241
|
}
|
|
191
|
-
await locator.dblclick({ timeout: 5000 });
|
|
242
|
+
await this._run_with_page_validation(browser_session, async () => locator.dblclick({ timeout: 5000 }));
|
|
192
243
|
return { double_clicked: Number(params.index) };
|
|
193
244
|
}
|
|
194
245
|
if (action === 'rightclick') {
|
|
@@ -200,7 +251,7 @@ export class SkillCliServer {
|
|
|
200
251
|
if (!locator?.click) {
|
|
201
252
|
throw new Error('Right-click is not available for this element');
|
|
202
253
|
}
|
|
203
|
-
await locator.click({ button: 'right', timeout: 5000 });
|
|
254
|
+
await this._run_with_page_validation(browser_session, async () => locator.click({ button: 'right', timeout: 5000 }));
|
|
204
255
|
return { right_clicked: Number(params.index) };
|
|
205
256
|
}
|
|
206
257
|
if (action === 'type') {
|
|
@@ -243,7 +294,7 @@ export class SkillCliServer {
|
|
|
243
294
|
return { screenshot };
|
|
244
295
|
}
|
|
245
296
|
const filePath = path.resolve(file);
|
|
246
|
-
await
|
|
297
|
+
await writePrivateBinaryFile(filePath, Buffer.from(screenshot, 'base64'));
|
|
247
298
|
return { file: filePath };
|
|
248
299
|
}
|
|
249
300
|
if (action === 'wait_selector') {
|
|
@@ -265,7 +316,7 @@ export class SkillCliServer {
|
|
|
265
316
|
if (!page?.waitForFunction) {
|
|
266
317
|
throw new Error('No active page available for wait_text');
|
|
267
318
|
}
|
|
268
|
-
await page.waitForFunction((needle) => document.body?.innerText?.includes(needle) ?? false, text, { timeout });
|
|
319
|
+
await this._run_with_page_validation(browser_session, () => page.waitForFunction((needle) => document.body?.innerText?.includes(needle) ?? false, text, { timeout }));
|
|
269
320
|
return { waited_for: 'text', text, timeout };
|
|
270
321
|
}
|
|
271
322
|
if (action === 'scroll') {
|
|
@@ -344,10 +395,10 @@ export class SkillCliServer {
|
|
|
344
395
|
if (!page?.evaluate) {
|
|
345
396
|
throw new Error('No active page available for html');
|
|
346
397
|
}
|
|
347
|
-
const html = await page.evaluate((targetSelector) => {
|
|
398
|
+
const html = await this._run_with_page_validation(browser_session, () => page.evaluate((targetSelector) => {
|
|
348
399
|
const element = document.querySelector(targetSelector);
|
|
349
400
|
return element ? element.outerHTML : null;
|
|
350
|
-
}, selector);
|
|
401
|
+
}, selector));
|
|
351
402
|
if (typeof html !== 'string' || html.length === 0) {
|
|
352
403
|
throw new Error(`No element found for selector: ${selector}`);
|
|
353
404
|
}
|
|
@@ -378,7 +429,7 @@ export class SkillCliServer {
|
|
|
378
429
|
throw new Error('No active page available for get_title');
|
|
379
430
|
}
|
|
380
431
|
return {
|
|
381
|
-
title: await page.title(),
|
|
432
|
+
title: await this._run_with_page_validation(browser_session, () => page.title()),
|
|
382
433
|
};
|
|
383
434
|
}
|
|
384
435
|
if (action === 'get_html') {
|
|
@@ -407,10 +458,14 @@ export class SkillCliServer {
|
|
|
407
458
|
}
|
|
408
459
|
if (action === 'cookies_get') {
|
|
409
460
|
const url = typeof params.url === 'string' ? params.url.trim() : '';
|
|
461
|
+
if (url) {
|
|
462
|
+
assertCookieUrlAllowed(browser_session, url);
|
|
463
|
+
}
|
|
410
464
|
const allCookies = (await browser_session.get_cookies());
|
|
465
|
+
const allowedCookies = filterAllowedCookies(browser_session, allCookies);
|
|
411
466
|
const cookies = url
|
|
412
|
-
?
|
|
413
|
-
:
|
|
467
|
+
? allowedCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
|
|
468
|
+
: allowedCookies;
|
|
414
469
|
return { cookies, count: cookies.length };
|
|
415
470
|
}
|
|
416
471
|
if (action === 'cookies_set') {
|
|
@@ -443,6 +498,10 @@ export class SkillCliServer {
|
|
|
443
498
|
if (!cookie.url && !cookie.domain) {
|
|
444
499
|
throw new Error('Provide cookie url/domain or open a page first');
|
|
445
500
|
}
|
|
501
|
+
const denialReason = getCookieDenialReason(browser_session, cookie);
|
|
502
|
+
if (denialReason) {
|
|
503
|
+
throw new Error(`Cookie target blocked by domain policy: ${denialReason}`);
|
|
504
|
+
}
|
|
446
505
|
await browser_session.browser_context.addCookies([cookie]);
|
|
447
506
|
return { set: name };
|
|
448
507
|
}
|
|
@@ -452,15 +511,39 @@ export class SkillCliServer {
|
|
|
452
511
|
}
|
|
453
512
|
const url = typeof params.url === 'string' ? params.url.trim() : '';
|
|
454
513
|
if (!url) {
|
|
514
|
+
if (typeof browser_session.get_cookies !== 'function') {
|
|
515
|
+
await browser_session.browser_context.clearCookies();
|
|
516
|
+
return { cleared: true };
|
|
517
|
+
}
|
|
518
|
+
const allCookies = (await browser_session.get_cookies({
|
|
519
|
+
include_blocked: true,
|
|
520
|
+
}));
|
|
521
|
+
const { allowedCookies, blockedCookies } = partitionAllowedCookies(browser_session, allCookies);
|
|
522
|
+
if (allowedCookies.length === 0 && blockedCookies.length > 0) {
|
|
523
|
+
return { cleared: true, count: 0 };
|
|
524
|
+
}
|
|
525
|
+
if (blockedCookies.length > 0 &&
|
|
526
|
+
!browser_session.browser_context.addCookies) {
|
|
527
|
+
throw new Error('Browser context does not support preserving blocked cookies');
|
|
528
|
+
}
|
|
455
529
|
await browser_session.browser_context.clearCookies();
|
|
456
|
-
|
|
530
|
+
if (blockedCookies.length > 0) {
|
|
531
|
+
await browser_session.browser_context.addCookies(blockedCookies);
|
|
532
|
+
}
|
|
533
|
+
return { cleared: true, count: allowedCookies.length };
|
|
457
534
|
}
|
|
458
|
-
|
|
535
|
+
assertCookieUrlAllowed(browser_session, url);
|
|
536
|
+
const allCookies = (await browser_session.get_cookies({
|
|
537
|
+
include_blocked: true,
|
|
538
|
+
}));
|
|
459
539
|
const remainingCookies = allCookies.filter((cookie) => !cookieMatchesUrl(cookie, url));
|
|
460
540
|
const removedCount = allCookies.length - remainingCookies.length;
|
|
461
|
-
await browser_session.browser_context.clearCookies();
|
|
462
541
|
if (remainingCookies.length > 0 &&
|
|
463
|
-
browser_session.browser_context.addCookies) {
|
|
542
|
+
!browser_session.browser_context.addCookies) {
|
|
543
|
+
throw new Error('Browser context does not support preserving non-matching cookies');
|
|
544
|
+
}
|
|
545
|
+
await browser_session.browser_context.clearCookies();
|
|
546
|
+
if (remainingCookies.length > 0) {
|
|
464
547
|
await browser_session.browser_context.addCookies(remainingCookies);
|
|
465
548
|
}
|
|
466
549
|
return { cleared: true, url, count: removedCount };
|
|
@@ -471,12 +554,16 @@ export class SkillCliServer {
|
|
|
471
554
|
throw new Error('Missing file');
|
|
472
555
|
}
|
|
473
556
|
const url = typeof params.url === 'string' ? params.url.trim() : '';
|
|
557
|
+
if (url) {
|
|
558
|
+
assertCookieUrlAllowed(browser_session, url);
|
|
559
|
+
}
|
|
474
560
|
const allCookies = (await browser_session.get_cookies());
|
|
561
|
+
const allowedCookies = filterAllowedCookies(browser_session, allCookies);
|
|
475
562
|
const cookies = url
|
|
476
|
-
?
|
|
477
|
-
:
|
|
563
|
+
? allowedCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
|
|
564
|
+
: allowedCookies;
|
|
478
565
|
const filePath = path.resolve(file);
|
|
479
|
-
await
|
|
566
|
+
await writePrivateJsonFile(filePath, cookies);
|
|
480
567
|
return { file: filePath, count: cookies.length };
|
|
481
568
|
}
|
|
482
569
|
if (action === 'cookies_import') {
|
|
@@ -504,8 +591,11 @@ export class SkillCliServer {
|
|
|
504
591
|
}
|
|
505
592
|
return typedCookie;
|
|
506
593
|
});
|
|
507
|
-
|
|
508
|
-
|
|
594
|
+
const allowedCookies = filterAllowedCookies(browser_session, importedCookies);
|
|
595
|
+
if (allowedCookies.length > 0) {
|
|
596
|
+
await browser_session.browser_context.addCookies(allowedCookies);
|
|
597
|
+
}
|
|
598
|
+
return { file: filePath, imported: allowedCookies.length };
|
|
509
599
|
}
|
|
510
600
|
if (action === 'close') {
|
|
511
601
|
await this.registry.close_session(sessionName);
|
|
@@ -48,6 +48,7 @@ export declare class TunnelManager {
|
|
|
48
48
|
constructor(options?: TunnelManagerOptions);
|
|
49
49
|
private get_tunnel_file;
|
|
50
50
|
private get_tunnel_log_file;
|
|
51
|
+
private ensure_tunnel_dir;
|
|
51
52
|
private save_tunnel_info;
|
|
52
53
|
private load_tunnel_info;
|
|
53
54
|
get_binary_path(): string;
|
package/dist/skill-cli/tunnel.js
CHANGED
|
@@ -42,9 +42,18 @@ export class TunnelManager {
|
|
|
42
42
|
get_tunnel_log_file(port) {
|
|
43
43
|
return path.join(this.tunnel_dir, `${port}.log`);
|
|
44
44
|
}
|
|
45
|
+
ensure_tunnel_dir() {
|
|
46
|
+
fs.mkdirSync(this.tunnel_dir, { recursive: true, mode: 0o700 });
|
|
47
|
+
if (process.platform !== 'win32') {
|
|
48
|
+
fs.chmodSync(this.tunnel_dir, 0o700);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
45
51
|
save_tunnel_info(port, pid, url) {
|
|
46
|
-
|
|
47
|
-
fs.writeFileSync(this.get_tunnel_file(port), JSON.stringify({ port, pid, url }), 'utf-8');
|
|
52
|
+
this.ensure_tunnel_dir();
|
|
53
|
+
fs.writeFileSync(this.get_tunnel_file(port), JSON.stringify({ port, pid, url }), { encoding: 'utf-8', mode: 0o600 });
|
|
54
|
+
if (process.platform !== 'win32') {
|
|
55
|
+
fs.chmodSync(this.get_tunnel_file(port), 0o600);
|
|
56
|
+
}
|
|
48
57
|
}
|
|
49
58
|
load_tunnel_info(port) {
|
|
50
59
|
const filePath = this.get_tunnel_file(port);
|
|
@@ -122,9 +131,12 @@ export class TunnelManager {
|
|
|
122
131
|
catch (error) {
|
|
123
132
|
return { error: error.message };
|
|
124
133
|
}
|
|
125
|
-
|
|
134
|
+
this.ensure_tunnel_dir();
|
|
126
135
|
const logPath = this.get_tunnel_log_file(port);
|
|
127
|
-
const logFd = fs.openSync(logPath, 'w');
|
|
136
|
+
const logFd = fs.openSync(logPath, 'w', 0o600);
|
|
137
|
+
if (process.platform !== 'win32') {
|
|
138
|
+
fs.chmodSync(logPath, 0o600);
|
|
139
|
+
}
|
|
128
140
|
try {
|
|
129
141
|
const child = this.spawn_impl(binaryPath, ['tunnel', '--url', `http://localhost:${port}`], {
|
|
130
142
|
detached: true,
|
package/dist/sync/auth.js
CHANGED
|
@@ -10,7 +10,26 @@ const CONFIG_DIR = () => CONFIG.BROWSER_USE_CONFIG_DIR ?? path.join(process.cwd(
|
|
|
10
10
|
const DEVICE_ID_PATH = () => path.join(CONFIG_DIR(), 'device_id');
|
|
11
11
|
const CLOUD_AUTH_PATH = () => path.join(CONFIG_DIR(), 'cloud_auth.json');
|
|
12
12
|
const ensureDir = () => {
|
|
13
|
-
fs.mkdirSync(CONFIG_DIR(), { recursive: true });
|
|
13
|
+
fs.mkdirSync(CONFIG_DIR(), { recursive: true, mode: 0o700 });
|
|
14
|
+
if (process.platform !== 'win32') {
|
|
15
|
+
try {
|
|
16
|
+
fs.chmodSync(CONFIG_DIR(), 0o700);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* noop */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const writePrivateFile = (filePath, contents) => {
|
|
24
|
+
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', mode: 0o600 });
|
|
25
|
+
if (process.platform !== 'win32') {
|
|
26
|
+
try {
|
|
27
|
+
fs.chmodSync(filePath, 0o600);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* noop */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
14
33
|
};
|
|
15
34
|
const loadAuthConfig = () => {
|
|
16
35
|
try {
|
|
@@ -28,13 +47,7 @@ const loadAuthConfig = () => {
|
|
|
28
47
|
};
|
|
29
48
|
const saveAuthConfig = (config) => {
|
|
30
49
|
ensureDir();
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
fs.chmodSync(CLOUD_AUTH_PATH(), 0o600);
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
/* noop */
|
|
37
|
-
}
|
|
50
|
+
writePrivateFile(CLOUD_AUTH_PATH(), JSON.stringify(config, null, 2));
|
|
38
51
|
};
|
|
39
52
|
export const load_cloud_auth_config = () => loadAuthConfig();
|
|
40
53
|
export const save_cloud_api_token = (api_token, user_id = TEMP_USER_ID) => {
|
|
@@ -60,7 +73,7 @@ const getOrCreateDeviceId = () => {
|
|
|
60
73
|
/* continue */
|
|
61
74
|
}
|
|
62
75
|
const deviceId = uuid7str();
|
|
63
|
-
|
|
76
|
+
writePrivateFile(DEVICE_ID_PATH(), deviceId);
|
|
64
77
|
return deviceId;
|
|
65
78
|
};
|
|
66
79
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -8,6 +8,17 @@ const logger = createLogger('browser_use.telemetry');
|
|
|
8
8
|
const POSTHOG_EVENT_SETTINGS = {
|
|
9
9
|
process_person_profile: true,
|
|
10
10
|
};
|
|
11
|
+
const chmodPrivate = (target, mode) => {
|
|
12
|
+
if (process.platform === 'win32') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
fs.chmodSync(target, mode);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* noop */
|
|
20
|
+
}
|
|
21
|
+
};
|
|
11
22
|
export class ProductTelemetry {
|
|
12
23
|
client = null;
|
|
13
24
|
debugLogging;
|
|
@@ -68,13 +79,21 @@ export class ProductTelemetry {
|
|
|
68
79
|
}
|
|
69
80
|
try {
|
|
70
81
|
if (!fs.existsSync(this.userIdFile)) {
|
|
71
|
-
fs.mkdirSync(path.dirname(this.userIdFile), {
|
|
82
|
+
fs.mkdirSync(path.dirname(this.userIdFile), {
|
|
83
|
+
recursive: true,
|
|
84
|
+
mode: 0o700,
|
|
85
|
+
});
|
|
86
|
+
chmodPrivate(path.dirname(this.userIdFile), 0o700);
|
|
72
87
|
this.cachedUserId = uuid7str();
|
|
73
|
-
fs.writeFileSync(this.userIdFile, this.cachedUserId,
|
|
88
|
+
fs.writeFileSync(this.userIdFile, this.cachedUserId, {
|
|
89
|
+
encoding: 'utf-8',
|
|
90
|
+
mode: 0o600,
|
|
91
|
+
});
|
|
74
92
|
}
|
|
75
93
|
else {
|
|
76
94
|
this.cachedUserId = fs.readFileSync(this.userIdFile, 'utf-8');
|
|
77
95
|
}
|
|
96
|
+
chmodPrivate(this.userIdFile, 0o600);
|
|
78
97
|
}
|
|
79
98
|
catch {
|
|
80
99
|
this.cachedUserId = 'UNKNOWN_USER_ID';
|
package/dist/telemetry/views.js
CHANGED
|
@@ -8,6 +8,29 @@ export class BaseTelemetryEvent {
|
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
const REDACTED_TELEMETRY_VALUE = '<redacted>';
|
|
12
|
+
const redactSequence = (value) => {
|
|
13
|
+
if (!Array.isArray(value)) {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
return value.map((entry) => entry == null ? entry : REDACTED_TELEMETRY_VALUE);
|
|
17
|
+
};
|
|
18
|
+
const summarizeActionHistory = (value) => {
|
|
19
|
+
if (!Array.isArray(value)) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
return value.map((step) => {
|
|
23
|
+
if (!Array.isArray(step)) {
|
|
24
|
+
return step;
|
|
25
|
+
}
|
|
26
|
+
return step.map((action) => ({
|
|
27
|
+
action: action && typeof action === 'object'
|
|
28
|
+
? (Object.keys(action)[0] ?? 'unknown')
|
|
29
|
+
: 'unknown',
|
|
30
|
+
}));
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const redactNullableString = (value) => value ? REDACTED_TELEMETRY_VALUE : null;
|
|
11
34
|
export class AgentTelemetryEvent extends BaseTelemetryEvent {
|
|
12
35
|
name = 'agent_event';
|
|
13
36
|
task;
|
|
@@ -39,7 +62,7 @@ export class AgentTelemetryEvent extends BaseTelemetryEvent {
|
|
|
39
62
|
judge_impossible_task;
|
|
40
63
|
constructor(payload) {
|
|
41
64
|
super();
|
|
42
|
-
this.task = payload.task;
|
|
65
|
+
this.task = payload.task ? REDACTED_TELEMETRY_VALUE : '';
|
|
43
66
|
this.model = payload.model;
|
|
44
67
|
this.model_provider = payload.model_provider;
|
|
45
68
|
this.max_steps = payload.max_steps;
|
|
@@ -49,9 +72,9 @@ export class AgentTelemetryEvent extends BaseTelemetryEvent {
|
|
|
49
72
|
this.source = payload.source;
|
|
50
73
|
this.cdp_url = payload.cdp_url;
|
|
51
74
|
this.agent_type = payload.agent_type;
|
|
52
|
-
this.action_errors = payload.action_errors;
|
|
53
|
-
this.action_history = payload.action_history;
|
|
54
|
-
this.urls_visited = payload.urls_visited;
|
|
75
|
+
this.action_errors = redactSequence(payload.action_errors);
|
|
76
|
+
this.action_history = summarizeActionHistory(payload.action_history);
|
|
77
|
+
this.urls_visited = redactSequence(payload.urls_visited);
|
|
55
78
|
this.steps = payload.steps;
|
|
56
79
|
this.total_input_tokens = payload.total_input_tokens;
|
|
57
80
|
this.total_output_tokens = payload.total_output_tokens;
|
|
@@ -59,11 +82,11 @@ export class AgentTelemetryEvent extends BaseTelemetryEvent {
|
|
|
59
82
|
this.total_tokens = payload.total_tokens;
|
|
60
83
|
this.total_duration_seconds = payload.total_duration_seconds;
|
|
61
84
|
this.success = payload.success;
|
|
62
|
-
this.final_result_response = payload.final_result_response;
|
|
63
|
-
this.error_message = payload.error_message;
|
|
85
|
+
this.final_result_response = redactNullableString(payload.final_result_response);
|
|
86
|
+
this.error_message = redactNullableString(payload.error_message);
|
|
64
87
|
this.judge_verdict = payload.judge_verdict ?? null;
|
|
65
|
-
this.judge_reasoning = payload.judge_reasoning
|
|
66
|
-
this.judge_failure_reason = payload.judge_failure_reason
|
|
88
|
+
this.judge_reasoning = redactNullableString(payload.judge_reasoning);
|
|
89
|
+
this.judge_failure_reason = redactNullableString(payload.judge_failure_reason);
|
|
67
90
|
this.judge_reached_captcha = payload.judge_reached_captcha ?? null;
|
|
68
91
|
this.judge_impossible_task = payload.judge_impossible_task ?? null;
|
|
69
92
|
}
|
|
@@ -18,5 +18,5 @@ export const CUSTOM_MODEL_PRICING = {
|
|
|
18
18
|
max_output_tokens: null,
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
|
-
CUSTOM_MODEL_PRICING['bu-latest'] = CUSTOM_MODEL_PRICING['bu-
|
|
22
|
-
CUSTOM_MODEL_PRICING.smart = CUSTOM_MODEL_PRICING['bu-
|
|
21
|
+
CUSTOM_MODEL_PRICING['bu-latest'] = CUSTOM_MODEL_PRICING['bu-2-0'];
|
|
22
|
+
CUSTOM_MODEL_PRICING.smart = CUSTOM_MODEL_PRICING['bu-2-0'];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ModelPricing } from './views.js';
|
|
2
|
+
export declare const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
3
|
+
export declare const OPENROUTER_MODELS_CACHE_MS: number;
|
|
4
|
+
type OpenRouterMetadata = Record<string, any>;
|
|
5
|
+
export declare const isOpenRouterPricingModel: (modelName: string) => boolean;
|
|
6
|
+
export declare const resetOpenRouterPricingCacheForTesting: () => void;
|
|
7
|
+
export declare function getOpenRouterModelsMetadata(refresh?: boolean): Promise<Record<string, OpenRouterMetadata>>;
|
|
8
|
+
export declare function getOpenRouterModelMetadata(modelName: string, refresh?: boolean): Promise<OpenRouterMetadata | null>;
|
|
9
|
+
export declare function modelPricingFromOpenRouterMetadata(modelName: string, metadata: OpenRouterMetadata): ModelPricing | null;
|
|
10
|
+
export declare function getOpenRouterModelPricing(modelName: string, refresh?: boolean): Promise<ModelPricing | null>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createLogger } from '../logging-config.js';
|
|
3
|
+
const logger = createLogger('browser_use.tokens.openrouter');
|
|
4
|
+
export const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';
|
|
5
|
+
export const OPENROUTER_MODELS_CACHE_MS = 60 * 60 * 1000;
|
|
6
|
+
let openRouterModelsCache = null;
|
|
7
|
+
let openRouterModelsCacheFetchedAt = 0;
|
|
8
|
+
const floatOrNull = (value) => {
|
|
9
|
+
if (value == null || value === '')
|
|
10
|
+
return null;
|
|
11
|
+
const parsed = Number(value);
|
|
12
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
13
|
+
};
|
|
14
|
+
const intOrNull = (value) => {
|
|
15
|
+
if (value == null || value === '')
|
|
16
|
+
return null;
|
|
17
|
+
const parsed = Number(value);
|
|
18
|
+
return Number.isFinite(parsed) ? Math.trunc(parsed) : null;
|
|
19
|
+
};
|
|
20
|
+
const normalizeOpenRouterModelId = (modelName) => {
|
|
21
|
+
let modelId = modelName;
|
|
22
|
+
if (modelId.startsWith('openrouter/')) {
|
|
23
|
+
modelId = modelId.slice('openrouter/'.length);
|
|
24
|
+
}
|
|
25
|
+
else if (modelId.startsWith('openrouter-')) {
|
|
26
|
+
modelId = modelId.slice('openrouter-'.length);
|
|
27
|
+
}
|
|
28
|
+
return modelId.includes('/') ? modelId : null;
|
|
29
|
+
};
|
|
30
|
+
export const isOpenRouterPricingModel = (modelName) => modelName.startsWith('openrouter/') || modelName.startsWith('openrouter-');
|
|
31
|
+
export const resetOpenRouterPricingCacheForTesting = () => {
|
|
32
|
+
openRouterModelsCache = null;
|
|
33
|
+
openRouterModelsCacheFetchedAt = 0;
|
|
34
|
+
};
|
|
35
|
+
export async function getOpenRouterModelsMetadata(refresh = false) {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
if (!refresh &&
|
|
38
|
+
openRouterModelsCache &&
|
|
39
|
+
now - openRouterModelsCacheFetchedAt < OPENROUTER_MODELS_CACHE_MS) {
|
|
40
|
+
return openRouterModelsCache;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const response = await axios.get(OPENROUTER_MODELS_URL, { timeout: 30_000 });
|
|
44
|
+
const models = Array.isArray(response.data?.data) ? response.data.data : [];
|
|
45
|
+
openRouterModelsCache = {};
|
|
46
|
+
for (const model of models) {
|
|
47
|
+
if (model &&
|
|
48
|
+
typeof model === 'object' &&
|
|
49
|
+
typeof model.id === 'string') {
|
|
50
|
+
openRouterModelsCache[model.id] =
|
|
51
|
+
model;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
openRouterModelsCacheFetchedAt = now;
|
|
55
|
+
return openRouterModelsCache;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
logger.debug(`Failed to fetch OpenRouter pricing: ${error.message}`);
|
|
59
|
+
return openRouterModelsCache ?? {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function getOpenRouterModelMetadata(modelName, refresh = false) {
|
|
63
|
+
const modelId = normalizeOpenRouterModelId(modelName);
|
|
64
|
+
if (!modelId) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const models = await getOpenRouterModelsMetadata(refresh);
|
|
68
|
+
return models[modelId] ?? null;
|
|
69
|
+
}
|
|
70
|
+
export function modelPricingFromOpenRouterMetadata(modelName, metadata) {
|
|
71
|
+
const pricing = metadata.pricing;
|
|
72
|
+
if (!pricing || typeof pricing !== 'object') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const inputCost = floatOrNull(pricing.prompt);
|
|
76
|
+
const outputCost = floatOrNull(pricing.completion);
|
|
77
|
+
if (inputCost == null && outputCost == null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const contextLength = intOrNull(metadata.context_length);
|
|
81
|
+
const topProvider = metadata.top_provider;
|
|
82
|
+
const maxOutputTokens = topProvider && typeof topProvider === 'object'
|
|
83
|
+
? intOrNull(topProvider.max_completion_tokens)
|
|
84
|
+
: null;
|
|
85
|
+
return {
|
|
86
|
+
model: modelName,
|
|
87
|
+
input_cost_per_token: inputCost,
|
|
88
|
+
output_cost_per_token: outputCost,
|
|
89
|
+
cache_read_input_token_cost: floatOrNull(pricing.input_cache_read),
|
|
90
|
+
cache_creation_input_token_cost: floatOrNull(pricing.input_cache_write),
|
|
91
|
+
max_tokens: contextLength,
|
|
92
|
+
max_input_tokens: contextLength,
|
|
93
|
+
max_output_tokens: maxOutputTokens,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function getOpenRouterModelPricing(modelName, refresh = false) {
|
|
97
|
+
const metadata = await getOpenRouterModelMetadata(modelName, refresh);
|
|
98
|
+
if (!metadata) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return modelPricingFromOpenRouterMetadata(modelName, metadata);
|
|
102
|
+
}
|
package/dist/tokens/service.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createLogger } from '../logging-config.js';
|
|
|
7
7
|
import { create_task_with_error_handling } from '../utils.js';
|
|
8
8
|
import { CUSTOM_MODEL_PRICING } from './custom-pricing.js';
|
|
9
9
|
import { MODEL_TO_LITELLM } from './mappings.js';
|
|
10
|
+
import { getOpenRouterModelPricing, isOpenRouterPricingModel, } from './openrouter-pricing.js';
|
|
10
11
|
const logger = createLogger('browser_use.tokens');
|
|
11
12
|
const costLogger = createLogger('browser_use.tokens.cost');
|
|
12
13
|
const PRICING_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
@@ -263,25 +264,28 @@ export class TokenCost {
|
|
|
263
264
|
max_output_tokens: customPricing.max_output_tokens ?? null,
|
|
264
265
|
};
|
|
265
266
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
if (isOpenRouterPricingModel(modelName)) {
|
|
268
|
+
const openRouterPricing = await getOpenRouterModelPricing(modelName);
|
|
269
|
+
if (openRouterPricing) {
|
|
270
|
+
return openRouterPricing;
|
|
271
|
+
}
|
|
269
272
|
}
|
|
273
|
+
await this.ensurePricingLoaded();
|
|
270
274
|
const litellmModelName = MODEL_TO_LITELLM[modelName] ?? modelName;
|
|
271
|
-
const pricing = this.pricingData[litellmModelName];
|
|
272
|
-
if (
|
|
273
|
-
return
|
|
275
|
+
const pricing = this.pricingData?.[litellmModelName];
|
|
276
|
+
if (pricing) {
|
|
277
|
+
return {
|
|
278
|
+
model: modelName,
|
|
279
|
+
input_cost_per_token: pricing.input_cost_per_token ?? null,
|
|
280
|
+
output_cost_per_token: pricing.output_cost_per_token ?? null,
|
|
281
|
+
cache_read_input_token_cost: pricing.cache_read_input_token_cost ?? null,
|
|
282
|
+
cache_creation_input_token_cost: pricing.cache_creation_input_token_cost ?? null,
|
|
283
|
+
max_tokens: pricing.max_tokens ?? null,
|
|
284
|
+
max_input_tokens: pricing.max_input_tokens ?? null,
|
|
285
|
+
max_output_tokens: pricing.max_output_tokens ?? null,
|
|
286
|
+
};
|
|
274
287
|
}
|
|
275
|
-
return
|
|
276
|
-
model: modelName,
|
|
277
|
-
input_cost_per_token: pricing.input_cost_per_token ?? null,
|
|
278
|
-
output_cost_per_token: pricing.output_cost_per_token ?? null,
|
|
279
|
-
cache_read_input_token_cost: pricing.cache_read_input_token_cost ?? null,
|
|
280
|
-
cache_creation_input_token_cost: pricing.cache_creation_input_token_cost ?? null,
|
|
281
|
-
max_tokens: pricing.max_tokens ?? null,
|
|
282
|
-
max_input_tokens: pricing.max_input_tokens ?? null,
|
|
283
|
-
max_output_tokens: pricing.max_output_tokens ?? null,
|
|
284
|
-
};
|
|
288
|
+
return await getOpenRouterModelPricing(modelName);
|
|
285
289
|
}
|
|
286
290
|
get_usage_tokens_for_model(model) {
|
|
287
291
|
const filtered = this.usageHistory.filter((entry) => entry.model === model);
|