scenescout 3.14.1 → 3.16.0
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/CHANGELOG.md +94 -0
- package/README.md +90 -18
- package/dist/browsers.js +28 -0
- package/dist/check-run.js +211 -24
- package/dist/ci-run.js +374 -62
- package/dist/cli.js +221 -47
- package/dist/commands.js +42 -4
- package/dist/engine/baseline.js +377 -0
- package/dist/engine/brief.js +16 -7
- package/dist/engine/browser.js +1660 -386
- package/dist/engine/calibration.js +61 -30
- package/dist/engine/capture.js +164 -0
- package/dist/engine/check.js +258 -68
- package/dist/engine/ci-lanes.js +215 -0
- package/dist/engine/ci.js +233 -18
- package/dist/engine/claims.js +234 -12
- package/dist/engine/collector.js +851 -78
- package/dist/engine/crawl.js +49 -0
- package/dist/engine/dedup.js +329 -9
- package/dist/engine/design.js +355 -43
- package/dist/engine/export.js +877 -0
- package/dist/engine/fingerprint.js +92 -4
- package/dist/engine/flow.js +18 -6
- package/dist/engine/forms.js +198 -18
- package/dist/engine/journey.js +29 -1
- package/dist/engine/lane.js +13 -3
- package/dist/engine/launch.js +45 -6
- package/dist/engine/limits.js +7 -0
- package/dist/engine/live-page.js +49 -2
- package/dist/engine/live.js +4 -1
- package/dist/engine/memory.js +668 -70
- package/dist/engine/open.js +118 -0
- package/dist/engine/oracles.js +167 -2
- package/dist/engine/plain.js +268 -0
- package/dist/engine/png.js +127 -0
- package/dist/engine/policy.js +379 -9
- package/dist/engine/probes.js +3 -2
- package/dist/engine/profiles.js +45 -9
- package/dist/engine/project-folder.js +191 -0
- package/dist/engine/provider.js +4 -3
- package/dist/engine/refresh.js +338 -25
- package/dist/engine/replay.js +63 -10
- package/dist/engine/report.js +248 -40
- package/dist/engine/request.js +317 -23
- package/dist/engine/sarif.js +120 -0
- package/dist/engine/scripted-login.js +342 -62
- package/dist/engine/settle.js +67 -0
- package/dist/engine/signed-in.js +256 -0
- package/dist/engine/tickets.js +671 -0
- package/dist/engine/unload.js +3 -2
- package/dist/export-run.js +633 -0
- package/dist/first-run.js +581 -0
- package/dist/installer.js +267 -3
- package/dist/intake.js +104 -0
- package/dist/login-run.js +386 -71
- package/dist/mcp-server.js +637 -68
- package/dist/playbook.js +5 -0
- package/package.json +6 -3
- package/skills/scenescout/SKILL.md +51 -18
package/dist/engine/profiles.js
CHANGED
|
@@ -22,6 +22,7 @@ import path from "node:path";
|
|
|
22
22
|
import { BROWSER_ENGINES } from "../browsers.js";
|
|
23
23
|
import { MEMORY_DIRNAME, writeSelfIgnore } from "./memory.js";
|
|
24
24
|
import { SCRIPT_FLAGS } from "./scripted-login.js";
|
|
25
|
+
import { SAVE_MODES } from "./signed-in.js";
|
|
25
26
|
/** The directory under .scenescout/ that holds one file per role. */
|
|
26
27
|
export const AUTH_DIRNAME = "auth";
|
|
27
28
|
/** Owner read/write only: a profile is a live session. */
|
|
@@ -279,14 +280,36 @@ export function permissionNote(file, mode, platform = process.platform) {
|
|
|
279
280
|
return null;
|
|
280
281
|
return `The profile at ${file} can be read by other accounts on this machine (mode ${(mode & 0o777).toString(8)}); it holds a live session. Tighten it: chmod 600 "${file}"`;
|
|
281
282
|
}
|
|
282
|
-
/**
|
|
283
|
-
export function
|
|
284
|
-
|
|
283
|
+
/** One argument quoted for the platform's usual shell: double quotes on Windows, single quotes elsewhere. */
|
|
284
|
+
export function shellQuote(value, platform = process.platform) {
|
|
285
|
+
if (platform === "win32")
|
|
286
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
287
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The command that records a role's profile, with the URL when it is known,
|
|
291
|
+
* and `--project` when the profile must go somewhere other than the folder
|
|
292
|
+
* the command is run from (a project folder SceneScout chose itself).
|
|
293
|
+
*/
|
|
294
|
+
export function loginCommand(role, url, project, platform = process.platform) {
|
|
295
|
+
return `scenescout login ${url ?? "<url>"} --role ${role}${project ? ` --project ${shellQuote(project, platform)}` : ""}`;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The command that records a role's profile again for a session: with
|
|
299
|
+
* `--project` only when SceneScout chose the session's folder itself
|
|
300
|
+
* (`projectChosen`), since the command otherwise saves into the folder it is
|
|
301
|
+
* run from and the next attach would not find it. Used by a refusal for a
|
|
302
|
+
* missing role and by every hint for an expired one.
|
|
303
|
+
*/
|
|
304
|
+
export function reloginCommand(opts) {
|
|
305
|
+
return loginCommand(opts.role, opts.url, opts.projectChosen ? opts.projectDir : undefined, opts.platform);
|
|
285
306
|
}
|
|
286
307
|
/**
|
|
287
308
|
* Turn scout_attach's `role` and `storageStatePath` into the one storage state
|
|
288
309
|
* the session loads. Both at once is refused rather than letting one win
|
|
289
|
-
* silently. A missing profile names the command that records it
|
|
310
|
+
* silently. A missing profile names the command that records it, with
|
|
311
|
+
* `--project` when SceneScout chose the folder (`projectChosen`), so the
|
|
312
|
+
* command saves where the next attach looks.
|
|
290
313
|
*/
|
|
291
314
|
export function resolveAttachAuth(opts, exists = fs.existsSync, listRoles = listProfiles) {
|
|
292
315
|
if (opts.role !== undefined && opts.storageStatePath !== undefined) {
|
|
@@ -305,7 +328,7 @@ export function resolveAttachAuth(opts, exists = fs.existsSync, listRoles = list
|
|
|
305
328
|
catch {
|
|
306
329
|
/* the list is a hint in the message; the refusal stands without it */
|
|
307
330
|
}
|
|
308
|
-
throw new Error(`no sign-in is saved for role "${checked.role}" in this project. Run \`${
|
|
331
|
+
throw new Error(`no sign-in is saved for role "${checked.role}" in this project. Run \`${reloginCommand({ role: checked.role, url: opts.url, projectDir: opts.projectDir, projectChosen: opts.projectChosen, platform: opts.platform })}\` (or, from a conversation, scout_login { role: "${checked.role}" }), sign in in the window it opens, then attach again.` +
|
|
309
332
|
(saved.length > 0 ? ` Saved roles: ${saved.join(", ")}.` : ""));
|
|
310
333
|
}
|
|
311
334
|
return { kind: "role", role: checked.role, storageStatePath: file };
|
|
@@ -322,11 +345,14 @@ export function roleLabel(auth) {
|
|
|
322
345
|
return path.basename(auth.storageStatePath).replace(/\.json$/i, "");
|
|
323
346
|
return "anonymous";
|
|
324
347
|
}
|
|
325
|
-
export const LOGIN_OPTION_NAMES = ["role", "project", "browser"];
|
|
348
|
+
export const LOGIN_OPTION_NAMES = ["role", "project", "browser", "save"];
|
|
349
|
+
/** Of the `--script` flags, those the window reads too. */
|
|
350
|
+
const WINDOW_SCRIPT_FLAGS = new Set(["success-url"]);
|
|
326
351
|
/**
|
|
327
352
|
* Parse `scenescout login <url> --role <name> [--project dir] [--browser engine]
|
|
328
|
-
* [--
|
|
329
|
-
* flags only it reads are refused without it
|
|
353
|
+
* [--save auto|enter] [--success-url …] [--script [--timeout s] …]`. `--script`
|
|
354
|
+
* takes no value; the flags only it reads are refused without it, and `--save`,
|
|
355
|
+
* which only the window reads, is refused with it.
|
|
330
356
|
*/
|
|
331
357
|
export function parseLoginArgs(args, cwd) {
|
|
332
358
|
const positional = [];
|
|
@@ -354,8 +380,10 @@ export function parseLoginArgs(args, cwd) {
|
|
|
354
380
|
const known = new Set(LOGIN_OPTION_NAMES);
|
|
355
381
|
const scriptOnly = new Set(SCRIPT_FLAGS);
|
|
356
382
|
for (const name of flags.keys()) {
|
|
357
|
-
if (scriptOnly.has(name) && !script)
|
|
383
|
+
if (scriptOnly.has(name) && !script && !WINDOW_SCRIPT_FLAGS.has(name))
|
|
358
384
|
return { ok: false, error: `--${name} only applies with --script` };
|
|
385
|
+
if (name === "save" && script)
|
|
386
|
+
return { ok: false, error: "--save applies to the window, not to --script, which saves once its sign-in succeeds" };
|
|
359
387
|
if (!known.has(name) && !scriptOnly.has(name))
|
|
360
388
|
return { ok: false, error: `unknown option --${name}` };
|
|
361
389
|
}
|
|
@@ -379,6 +407,12 @@ export function parseLoginArgs(args, cwd) {
|
|
|
379
407
|
if (browser !== undefined && !BROWSER_ENGINES.includes(browser)) {
|
|
380
408
|
return { ok: false, error: `--browser must be one of ${BROWSER_ENGINES.join(", ")}` };
|
|
381
409
|
}
|
|
410
|
+
const save = flags.get("save");
|
|
411
|
+
if (save !== undefined && !SAVE_MODES.includes(save))
|
|
412
|
+
return { ok: false, error: `--save must be one of ${SAVE_MODES.join(", ")}` };
|
|
413
|
+
const successUrl = flags.get("success-url");
|
|
414
|
+
if (successUrl !== undefined && successUrl.trim() === "")
|
|
415
|
+
return { ok: false, error: "--success-url needs a value" };
|
|
382
416
|
return {
|
|
383
417
|
ok: true,
|
|
384
418
|
options: {
|
|
@@ -387,6 +421,8 @@ export function parseLoginArgs(args, cwd) {
|
|
|
387
421
|
projectDir: path.resolve(cwd, flags.get("project") ?? "."),
|
|
388
422
|
...(browser ? { browser: browser } : {}),
|
|
389
423
|
...(script ? { script: new Map([...flags].filter(([name]) => scriptOnly.has(name))) } : {}),
|
|
424
|
+
...(save ? { save: save } : {}),
|
|
425
|
+
...(!script && successUrl !== undefined ? { successUrl } : {}),
|
|
390
426
|
},
|
|
391
427
|
};
|
|
392
428
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a run keeps its notes, memory and report when the agent names no
|
|
3
|
+
* project folder (scout_attach `projectPath`).
|
|
4
|
+
*
|
|
5
|
+
* A client with no workspace (a desktop chat app, say) has no folder to offer,
|
|
6
|
+
* and a person testing a site should not be asked to invent one. So each tested
|
|
7
|
+
* site gets its own folder under the user's documents folder, such as
|
|
8
|
+
* `Documents/SceneScout/localhost-3000/`, created on first use.
|
|
9
|
+
*
|
|
10
|
+
* What wins, first to last:
|
|
11
|
+
* 1. the `projectPath` the attach names, always;
|
|
12
|
+
* 2. the client's workspace folder, when it offers one (MCP roots);
|
|
13
|
+
* 3. the per-site default, unless PROJECTS_DIR_ENV is `off`.
|
|
14
|
+
* The default is never placed inside a git repository below the home folder: a
|
|
15
|
+
* folder of recordings and saved logins does not belong in somebody's commits
|
|
16
|
+
* unless they chose it. A home folder that is itself a repository (dotfiles)
|
|
17
|
+
* does not count.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is pure (the filesystem is passed in), so it is table-tested
|
|
20
|
+
* in memory-test.
|
|
21
|
+
*/
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { domainToUnicode, fileURLToPath } from "node:url";
|
|
24
|
+
import { MEMORY_DIRNAME } from "./memory.js";
|
|
25
|
+
/** The setting: an absolute folder that holds one folder per tested site, or `off`. */
|
|
26
|
+
export const PROJECTS_DIR_ENV = "SCENESCOUT_PROJECTS_DIR";
|
|
27
|
+
/** The folder made under the documents folder when the setting is unset. */
|
|
28
|
+
export const DEFAULT_PROJECTS_DIRNAME = "SceneScout";
|
|
29
|
+
/**
|
|
30
|
+
* The user's documents folder. Windows: `%USERPROFILE%\Documents`; Linux and
|
|
31
|
+
* others: XDG_DOCUMENTS_DIR from the environment or user-dirs.dirs (which the
|
|
32
|
+
* server reads on Linux only), else
|
|
33
|
+
* `~/Documents`; macOS: `~/Documents`.
|
|
34
|
+
*/
|
|
35
|
+
export function documentsDir(home) {
|
|
36
|
+
if (home.platform === "win32") {
|
|
37
|
+
const profile = home.env.USERPROFILE && path.win32.isAbsolute(home.env.USERPROFILE) ? home.env.USERPROFILE : home.homedir;
|
|
38
|
+
return path.win32.join(profile, "Documents");
|
|
39
|
+
}
|
|
40
|
+
if (home.platform !== "darwin") {
|
|
41
|
+
const fromEnv = home.env.XDG_DOCUMENTS_DIR;
|
|
42
|
+
const fromFile = home.userDirs?.match(/^\s*XDG_DOCUMENTS_DIR\s*=\s*"([^"\n]*)"/m)?.[1];
|
|
43
|
+
for (const raw of [fromEnv, fromFile]) {
|
|
44
|
+
if (!raw)
|
|
45
|
+
continue;
|
|
46
|
+
const expanded = raw.replace(/^\$HOME(?=\/|$)/, home.homedir);
|
|
47
|
+
if (!path.posix.isAbsolute(expanded))
|
|
48
|
+
continue;
|
|
49
|
+
// resolve() drops a trailing slash, so "$HOME/" compares equal to the home folder.
|
|
50
|
+
const dir = path.posix.resolve(expanded);
|
|
51
|
+
// XDG says a documents folder equal to $HOME means "none": fall through to ~/Documents.
|
|
52
|
+
if (dir !== path.posix.resolve(home.homedir))
|
|
53
|
+
return dir;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return path.posix.join(home.homedir, "Documents");
|
|
57
|
+
}
|
|
58
|
+
/** Names Windows reserves, which no folder may take, whatever its extension. */
|
|
59
|
+
const RESERVED = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\.|$)/i;
|
|
60
|
+
/**
|
|
61
|
+
* The folder name for a tested site: its host as a person would write it, with
|
|
62
|
+
* the port when there is one. `http://localhost:3000` → `localhost-3000`,
|
|
63
|
+
* `https://xn--bcher-kva.example/` → `bücher.example`, `http://[::1]:8080` →
|
|
64
|
+
* `ipv6-__1-8080`. Two addresses of one site (http and https, any path) share
|
|
65
|
+
* a folder; a different port is a different site. Throws for an address with
|
|
66
|
+
* no host, which has no site to name.
|
|
67
|
+
*/
|
|
68
|
+
export function siteFolderName(url) {
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = new URL(url);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error(`"${url}" is not a full address, so there is no site to name a folder after. Pass projectPath.`);
|
|
75
|
+
}
|
|
76
|
+
let host = parsed.hostname.toLowerCase().replace(/\.+$/, "");
|
|
77
|
+
if (!host)
|
|
78
|
+
throw new Error(`"${url}" has no host, so there is no site to name a folder after. Pass projectPath.`);
|
|
79
|
+
if (host.startsWith("["))
|
|
80
|
+
host = `ipv6-${host.slice(1, -1).replace(/:/g, "_")}`;
|
|
81
|
+
else
|
|
82
|
+
host = domainToUnicode(host).normalize("NFC") || host;
|
|
83
|
+
// A hostname holds only letters, digits, hyphens and dots, but say so here
|
|
84
|
+
// rather than trust it: this becomes one path segment.
|
|
85
|
+
host = host.replace(/[^\p{L}\p{M}\p{N}._-]/gu, "_").replace(/^\.+/, "_");
|
|
86
|
+
if (RESERVED.test(host))
|
|
87
|
+
host = `site-${host}`;
|
|
88
|
+
return parsed.port ? `${host}-${parsed.port}` : host;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The folder holding one folder per site: the setting when it names an
|
|
92
|
+
* absolute folder, `null` when it is `off`, else `<documents>/SceneScout`.
|
|
93
|
+
* A setting that is neither throws, naming the variable.
|
|
94
|
+
*/
|
|
95
|
+
export function projectsRoot(home) {
|
|
96
|
+
const setting = home.env[PROJECTS_DIR_ENV]?.trim();
|
|
97
|
+
const p = home.platform === "win32" ? path.win32 : path.posix;
|
|
98
|
+
if (setting) {
|
|
99
|
+
if (setting.toLowerCase() === "off")
|
|
100
|
+
return null;
|
|
101
|
+
if (!p.isAbsolute(setting))
|
|
102
|
+
throw new Error(`${PROJECTS_DIR_ENV} must be an absolute folder or "off", not "${setting}".`);
|
|
103
|
+
return p.normalize(setting);
|
|
104
|
+
}
|
|
105
|
+
return p.join(documentsDir(home), DEFAULT_PROJECTS_DIRNAME);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The git repository a folder would sit inside: the nearest folder, the folder
|
|
109
|
+
* itself included, holding a `.git` (a directory, or a file in a worktree).
|
|
110
|
+
* `exists` is fs.existsSync in use; the folder need not exist yet.
|
|
111
|
+
*
|
|
112
|
+
* With `stopAt` (the home folder), the walk ends below it: a home folder that
|
|
113
|
+
* is itself a repository, as a dotfiles setup makes it, does not count, since
|
|
114
|
+
* refusing every default folder there is worse than one untracked folder in
|
|
115
|
+
* it. A folder outside `stopAt` is walked to the root.
|
|
116
|
+
*/
|
|
117
|
+
export function enclosingRepo(dir, exists, platform = process.platform, stopAt) {
|
|
118
|
+
const p = platform === "win32" ? path.win32 : path.posix;
|
|
119
|
+
const same = (a, b) => (platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b);
|
|
120
|
+
const stop = stopAt === undefined ? undefined : p.resolve(stopAt);
|
|
121
|
+
for (let at = p.resolve(dir);;) {
|
|
122
|
+
if (stop !== undefined && same(at, stop))
|
|
123
|
+
return null;
|
|
124
|
+
if (exists(p.join(at, ".git")))
|
|
125
|
+
return at;
|
|
126
|
+
const up = p.dirname(at);
|
|
127
|
+
if (up === at)
|
|
128
|
+
return null;
|
|
129
|
+
at = up;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The first workspace folder a client offers as MCP roots, or null. Only `file:`
|
|
134
|
+
* roots name a folder, read as `platform` reads them: `file:///C:/work/app` is
|
|
135
|
+
* `C:\\work\\app` on Windows, and a root with no drive letter is no folder there.
|
|
136
|
+
*/
|
|
137
|
+
export function workspaceFromRoots(roots, platform = process.platform) {
|
|
138
|
+
for (const root of roots ?? []) {
|
|
139
|
+
if (!root.uri.startsWith("file:"))
|
|
140
|
+
continue;
|
|
141
|
+
try {
|
|
142
|
+
return fileURLToPath(root.uri, { windows: platform === "win32" });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
continue; // a file: URI naming another host, or no absolute path on this platform: not a folder here
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Which folder a run keeps its files in, and the plain line that says so.
|
|
152
|
+
* `given` is the attach's projectPath; `workspace` the client's folder, if any.
|
|
153
|
+
*/
|
|
154
|
+
export function chooseProjectFolder(input) {
|
|
155
|
+
if (input.given !== undefined)
|
|
156
|
+
return { dir: input.given, source: "given", note: "" };
|
|
157
|
+
if (input.workspace) {
|
|
158
|
+
return {
|
|
159
|
+
dir: input.workspace,
|
|
160
|
+
source: "workspace",
|
|
161
|
+
note: `📁 FILES: no projectPath was given, so this run's notes, memory, sign-ins and report are kept in your workspace folder, under ${input.workspace}.`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
let root;
|
|
165
|
+
let name;
|
|
166
|
+
try {
|
|
167
|
+
root = projectsRoot(input.home);
|
|
168
|
+
name = siteFolderName(input.url);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
return { refused: `No projectPath was given and no default folder could be chosen: ${err.message}` };
|
|
172
|
+
}
|
|
173
|
+
if (root === null)
|
|
174
|
+
return {
|
|
175
|
+
refused: `No projectPath was given, and ${PROJECTS_DIR_ENV} is "off", so there is no default folder. Pass projectPath: the folder this run's notes, memory and report go in.`,
|
|
176
|
+
};
|
|
177
|
+
const p = input.home.platform === "win32" ? path.win32 : path.posix;
|
|
178
|
+
const dir = p.join(root, name);
|
|
179
|
+
const repo = enclosingRepo(dir, input.exists, input.home.platform, input.home.homedir);
|
|
180
|
+
if (repo)
|
|
181
|
+
return {
|
|
182
|
+
refused: `No projectPath was given, and the default folder ${dir} would be inside the git repository ${repo}, where a run's recordings and saved logins could be committed. ` +
|
|
183
|
+
`Pass projectPath to choose a folder (inside that repository too, if that is what you want), or set ${PROJECTS_DIR_ENV} to a folder outside it.`,
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
dir,
|
|
187
|
+
source: "default",
|
|
188
|
+
note: `📁 FILES: this site's notes, memory, sign-ins and report are kept in ${dir} — a folder SceneScout made for this site, since none was given. ` +
|
|
189
|
+
`The report will be ${p.join(dir, MEMORY_DIRNAME, "report.md")}. Pass projectPath, or set ${PROJECTS_DIR_ENV}, to keep them elsewhere.`,
|
|
190
|
+
};
|
|
191
|
+
}
|
package/dist/engine/provider.js
CHANGED
|
@@ -3,7 +3,8 @@ export class MalformedReply extends Error {
|
|
|
3
3
|
}
|
|
4
4
|
/** The most output one model call may produce. Large enough for a plan of steps; small enough that one reply cannot spend the budget. */
|
|
5
5
|
export const MAX_OUTPUT_TOKENS = 16_000;
|
|
6
|
-
|
|
6
|
+
/** A plain object as a record, or null for anything else (an array, null, a string). */
|
|
7
|
+
export const obj = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : null);
|
|
7
8
|
const num = (v) => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0);
|
|
8
9
|
function parseArgs(raw) {
|
|
9
10
|
if (raw === undefined || raw === null || raw === "")
|
|
@@ -31,7 +32,7 @@ export class AnthropicConversation {
|
|
|
31
32
|
headers: { "content-type": "application/json", "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
32
33
|
body: {
|
|
33
34
|
model: this.o.model,
|
|
34
|
-
max_tokens: MAX_OUTPUT_TOKENS,
|
|
35
|
+
max_tokens: this.o.maxOutputTokens ?? MAX_OUTPUT_TOKENS,
|
|
35
36
|
// The method and the tools are the same on every call: cached once, read back at a tenth of the price.
|
|
36
37
|
system: [{ type: "text", text: this.o.system, cache_control: { type: "ephemeral" } }],
|
|
37
38
|
tools: this.o.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters })),
|
|
@@ -102,7 +103,7 @@ export class OpenAIConversation {
|
|
|
102
103
|
input: this.input,
|
|
103
104
|
tools: this.o.tools.map((t) => ({ type: "function", name: t.name, description: t.description, parameters: t.parameters, strict: false })),
|
|
104
105
|
reasoning: { effort: this.o.effort },
|
|
105
|
-
max_output_tokens: MAX_OUTPUT_TOKENS,
|
|
106
|
+
max_output_tokens: this.o.maxOutputTokens ?? MAX_OUTPUT_TOKENS,
|
|
106
107
|
// Stateless: nothing of the tested app is kept on the provider's side
|
|
107
108
|
// between calls, and the reasoning comes back encrypted so it can be
|
|
108
109
|
// handed back with the rest of the turn.
|