cortad 0.4.0 → 0.4.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/lib/replay.mjs +31 -18
- package/lib/start.mjs +4 -3
- package/local.mjs +1 -1
- package/package.json +1 -1
package/lib/replay.mjs
CHANGED
|
@@ -152,7 +152,7 @@ export function makeCapture({ work, keepSecret, onProof = () => {}, writes = nul
|
|
|
152
152
|
// What your app spent on its model providers since it started, as the hook saw each call. Null
|
|
153
153
|
// when no hook is in your app (an app this command did not start): absent, never zero. With a
|
|
154
154
|
// `turn`, the calls pinned to that turn also say what their model was told.
|
|
155
|
-
usage: (turn) => { poll(); return alive ? meter.report(TURN.test(turn ?? "") ? turn : undefined) : null; },
|
|
155
|
+
usage: (turn, totalsOnly = false) => { poll(); return alive ? meter.report(TURN.test(turn ?? "") ? turn : undefined, totalsOnly) : null; },
|
|
156
156
|
// The app's routes as its framework holds them: the table of the process on `port`, else the
|
|
157
157
|
// newest any hooked process wrote. Empty when no hook read one.
|
|
158
158
|
registry: (port) => { poll(); return tables.get(port) ?? newestTable ?? {}; },
|
|
@@ -339,14 +339,36 @@ const toldOf = (call) => {
|
|
|
339
339
|
// The model's words behind a call, and the lines of the app that wrote the reply of the request it
|
|
340
340
|
// was made in, go out with the rows pinned to the turn asked about, as what the model was told does.
|
|
341
341
|
const MODEL_WORDS = 4000, SITES_KEPT = 500;
|
|
342
|
+
function tally(totals, call, count) {
|
|
343
|
+
const host = call.host.slice(0, 253);
|
|
344
|
+
const model = String(call.model ?? "").slice(0, 160);
|
|
345
|
+
const answered = typeof call.answered === "string" && call.answered ? call.answered.slice(0, 160) : undefined;
|
|
346
|
+
const key = `${host}|${model}`;
|
|
347
|
+
const t = totals.get(key) ?? { host, model, calls: 0, promptTokens: 0, cachedTokens: 0, completionTokens: 0, unmetered: 0, answered, mixed: false };
|
|
348
|
+
t.calls += 1;
|
|
349
|
+
// The model that answered for this one, when every call of it was answered by one other model.
|
|
350
|
+
if (t.answered !== answered) t.mixed = true;
|
|
351
|
+
t.promptTokens += count(call.promptTokens);
|
|
352
|
+
t.cachedTokens += count(call.cachedTokens);
|
|
353
|
+
t.completionTokens += count(call.completionTokens);
|
|
354
|
+
// An answered call the provider sent no counts for: it was spent, and what it cost is not known.
|
|
355
|
+
const status = count(call.status);
|
|
356
|
+
if (call.usage !== true && status > 0 && status < 400) t.unmetered += 1;
|
|
357
|
+
totals.set(key, t);
|
|
358
|
+
}
|
|
359
|
+
|
|
342
360
|
const meter = (() => {
|
|
343
361
|
const rows = [];
|
|
344
362
|
const deps = [];
|
|
363
|
+
// What the app has spent since it started, per host and model, kept as it goes: the rows are
|
|
364
|
+
// trimmed to the newest ROWS, and totals summed from them lost every call before those.
|
|
365
|
+
const totals = new Map();
|
|
345
366
|
const sitesOfEx = new Map();
|
|
346
367
|
const count = (v) => (Number.isInteger(v) && v > 0 ? v : 0);
|
|
347
368
|
return {
|
|
348
369
|
add(call) {
|
|
349
370
|
if (!call || typeof call.host !== "string" || !call.host) return;
|
|
371
|
+
tally(totals, call, count);
|
|
350
372
|
rows.push({
|
|
351
373
|
at: count(call.at), ms: count(call.ms), host: call.host.slice(0, 253), model: String(call.model ?? "").slice(0, 160), ...(typeof call.answered === "string" && call.answered ? { answered: call.answered.slice(0, 160) } : {}), status: count(call.status),
|
|
352
374
|
promptTokens: count(call.promptTokens), cachedTokens: count(call.cachedTokens), completionTokens: count(call.completionTokens),
|
|
@@ -373,24 +395,15 @@ const meter = (() => {
|
|
|
373
395
|
},
|
|
374
396
|
// `turn`: the rows pinned to it carry what their model was told. The instructions go out with the
|
|
375
397
|
// reply they are read with, never with every call of the run at every turn.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// The model that answered for this one, when every call of it was answered by one other model.
|
|
383
|
-
if (t.answered !== r.answered) t.answered = undefined;
|
|
384
|
-
t.promptTokens += r.promptTokens;
|
|
385
|
-
t.cachedTokens += r.cachedTokens;
|
|
386
|
-
t.completionTokens += r.completionTokens;
|
|
387
|
-
// An answered call the provider sent no counts for: it was spent, and what it cost is not known.
|
|
388
|
-
if (!r.usage && r.status > 0 && r.status < 400) t.unmetered += 1;
|
|
389
|
-
totals.set(key, t);
|
|
390
|
-
}
|
|
398
|
+
// `totalsOnly`: the spend alone, for the meter read while a run plays and at its end. The rows
|
|
399
|
+
// carry what each call was handed, and on ulaim's run the whole report was 2.9 MB, past what the
|
|
400
|
+
// server takes, so the bill never arrived.
|
|
401
|
+
report(turn, totalsOnly = false) {
|
|
402
|
+
const spent = [...totals.values()].sort((a, b) => b.calls - a.calls).map(({ answered, mixed, ...t }) => (answered && !mixed ? { ...t, answered } : t));
|
|
403
|
+
if (totalsOnly) return { totals: spent };
|
|
391
404
|
return {
|
|
392
|
-
totals:
|
|
393
|
-
rows: rows.slice(-REPORTED).reverse().map(({ at, ms, host, model, status, turn: pinned, rules, tools, called, passages, caller, ex, told, said }) => ({ at, ms, host, model, status, ...(pinned ? { turn: pinned } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}), ...(caller ? { caller } : {}),
|
|
405
|
+
totals: spent,
|
|
406
|
+
rows: rows.slice(-REPORTED).reverse().map(({ at, ms, host, model, status, turn: pinned, rules, tools, called, passages, caller, ex, told, said }) => ({ at, ms, host, model, status, ...(pinned ? { turn: pinned } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages && turn !== undefined && pinned === turn ? { passages } : {}), ...(caller ? { caller } : {}),
|
|
394
407
|
...(turn !== undefined && pinned === turn ? { ...told, ...(said !== undefined ? { said } : {}), ...(sitesOfEx.get(ex)?.length ? { sites: sitesOfEx.get(ex) } : {}) } : {}) })),
|
|
395
408
|
deps: deps.slice(-REPORTED).reverse(),
|
|
396
409
|
};
|
package/lib/start.mjs
CHANGED
|
@@ -125,10 +125,11 @@ const KNOWN = ["package.json", "pyproject.toml", "requirements.txt", "manage.py"
|
|
|
125
125
|
// from its environment whatever folder it runs in; a container, a Go or Java binary does not, and is
|
|
126
126
|
// watched from outside (lib/proxy.mjs). Judged by the command when there is one, else by the folder.
|
|
127
127
|
const HOOKED = /^(?:python3?(?:\.\d+)?|node|nodemon|tsx|ts-node|bun|bunx|deno|npm|npx|pnpm|yarn|uvicorn|gunicorn|hypercorn|flask|fastapi|poetry|uv|pipenv)$/;
|
|
128
|
-
// The program a command runs, past any PORT=3000 in front of it
|
|
128
|
+
// The program a command runs, past any PORT=3000 in front of it, an `env` or `exec` that only sets
|
|
129
|
+
// those, and the quotes or folder around it.
|
|
129
130
|
const runtimeOf = (cmd) => {
|
|
130
|
-
const words = cmd.trim().split(/\s+/).filter((w) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(w));
|
|
131
|
-
return (
|
|
131
|
+
const words = cmd.trim().split(/\s+/).filter((w) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(w)).map((w) => w.replace(/^["']|["']$/g, "").split("/").pop() ?? "");
|
|
132
|
+
return words.find((w) => w !== "env" && w !== "exec" && !w.startsWith("-")) ?? "";
|
|
132
133
|
};
|
|
133
134
|
export const hookable = (dir, cmd = "") => (cmd ? HOOKED.test(runtimeOf(cmd)) : KNOWN.some((f) => existsSync(join(dir, f))));
|
|
134
135
|
|
package/local.mjs
CHANGED
|
@@ -572,7 +572,7 @@ async function verb(job) {
|
|
|
572
572
|
// calls of the `turn` asked about also say what their model was told.
|
|
573
573
|
// `toolsRecorded: false`: the app was started before the read's list of tools, so a tool that ran
|
|
574
574
|
// was not seen; a run says "not recorded", never that none ran.
|
|
575
|
-
case "usage": return capture ? { ...JSON.parse(mask(JSON.stringify(capture.usage(b.turn) ?? {}))), ...(launched && toolsKnown() ? { toolsRecorded: toolsRecorded() } : {}) } : {};
|
|
575
|
+
case "usage": return capture ? { ...JSON.parse(mask(JSON.stringify(capture.usage(b.turn, b.totals === true) ?? {}))), ...(launched && toolsKnown() ? { toolsRecorded: toolsRecorded() } : {}) } : {};
|
|
576
576
|
// Every route your app holds, read by the hook off the app itself, masked like the meter.
|
|
577
577
|
case "routes": return capture ? JSON.parse(mask(JSON.stringify(capture.registry(app?.port)))) : {};
|
|
578
578
|
// A world is ended from this terminal, never from the cloud.
|
package/package.json
CHANGED