@icntswm/skillcheck 1.0.2 → 1.0.4
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 +1 -0
- package/dist/agents/claude.js +43 -1
- package/dist/budget.js +45 -8
- package/dist/cli.js +8 -5
- package/dist/junit.js +2 -1
- package/dist/report.js +7 -4
- package/dist/results.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# skillcheck
|
|
2
2
|
|
|
3
|
+
[](https://github.com/icntswm/skillcheck/actions/workflows/ci.yml)
|
|
3
4
|
[](LICENSE)
|
|
4
5
|
[](https://www.npmjs.com/package/@icntswm/skillcheck)
|
|
5
6
|

|
package/dist/agents/claude.js
CHANGED
|
@@ -12,6 +12,23 @@ export const DEFAULT_DIRECTIVE =
|
|
|
12
12
|
function stringList(v) {
|
|
13
13
|
return Array.isArray(v) ? v.filter((s) => typeof s === "string") : null;
|
|
14
14
|
}
|
|
15
|
+
function count(v) {
|
|
16
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
17
|
+
}
|
|
18
|
+
/** Sum API `usage` objects; cache writes without a 1h/5m split count as 5m. */
|
|
19
|
+
function sumUsage(model, usages) {
|
|
20
|
+
const total = { model, input: 0, output: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0 };
|
|
21
|
+
for (const u of usages) {
|
|
22
|
+
const split = u.cache_creation;
|
|
23
|
+
const write1h = count(split?.ephemeral_1h_input_tokens);
|
|
24
|
+
total.input += count(u.input_tokens);
|
|
25
|
+
total.output += count(u.output_tokens);
|
|
26
|
+
total.cacheRead += count(u.cache_read_input_tokens);
|
|
27
|
+
total.cacheWrite1h += write1h;
|
|
28
|
+
total.cacheWrite5m += Math.max(0, count(u.cache_creation_input_tokens) - write1h);
|
|
29
|
+
}
|
|
30
|
+
return total;
|
|
31
|
+
}
|
|
15
32
|
/**
|
|
16
33
|
* Incremental parser of claude's stream-json stdout. One event per line.
|
|
17
34
|
* Pushing lines also computes the early-stop signal, so the adapter can
|
|
@@ -23,6 +40,10 @@ export class ClaudeStream {
|
|
|
23
40
|
resultText = "";
|
|
24
41
|
sawResult = false;
|
|
25
42
|
costUsd = null;
|
|
43
|
+
model = null;
|
|
44
|
+
/** latest usage per API message; a message streams as several events */
|
|
45
|
+
messageUsage = new Map();
|
|
46
|
+
resultUsage = null;
|
|
26
47
|
availableSkills = null;
|
|
27
48
|
initSkillList = null;
|
|
28
49
|
initSlashList = null;
|
|
@@ -88,12 +109,20 @@ export class ClaudeStream {
|
|
|
88
109
|
}
|
|
89
110
|
if (typeof ev.total_cost_usd === "number")
|
|
90
111
|
this.costUsd = ev.total_cost_usd;
|
|
112
|
+
if (typeof ev.usage === "object" && ev.usage !== null)
|
|
113
|
+
this.resultUsage = ev.usage;
|
|
91
114
|
return true;
|
|
92
115
|
}
|
|
93
116
|
return false;
|
|
94
117
|
}
|
|
95
118
|
handleAssistant(ev) {
|
|
96
119
|
const message = ev.message;
|
|
120
|
+
if (typeof message?.model === "string")
|
|
121
|
+
this.model = message.model;
|
|
122
|
+
if (typeof message?.usage === "object" && message.usage !== null) {
|
|
123
|
+
const id = typeof message.id === "string" ? message.id : `#${this.messageUsage.size}`;
|
|
124
|
+
this.messageUsage.set(id, message.usage);
|
|
125
|
+
}
|
|
97
126
|
const blocks = message?.content;
|
|
98
127
|
if (!Array.isArray(blocks))
|
|
99
128
|
return false;
|
|
@@ -118,6 +147,18 @@ export class ClaudeStream {
|
|
|
118
147
|
this.sawSkill = true;
|
|
119
148
|
return sawSkillHere;
|
|
120
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Billed tokens: the result event's total, or the sum over assistant
|
|
152
|
+
* messages when the run was killed first. Streamed output counts are
|
|
153
|
+
* partial, so the latter runs a little low.
|
|
154
|
+
*/
|
|
155
|
+
get usage() {
|
|
156
|
+
if (this.resultUsage)
|
|
157
|
+
return sumUsage(this.model, [this.resultUsage]);
|
|
158
|
+
if (this.messageUsage.size === 0)
|
|
159
|
+
return null;
|
|
160
|
+
return sumUsage(this.model, [...this.messageUsage.values()]);
|
|
161
|
+
}
|
|
121
162
|
/** Error reported by claude itself in the result event, null if none. */
|
|
122
163
|
get error() {
|
|
123
164
|
return this.failure;
|
|
@@ -265,7 +306,8 @@ export class ClaudeAdapter {
|
|
|
265
306
|
});
|
|
266
307
|
const { loaded, text, costUsd, availableSkills } = out.stream.result;
|
|
267
308
|
const error = withLoginHint(out.error, opts.configDir);
|
|
268
|
-
|
|
309
|
+
const usage = out.stream.usage;
|
|
310
|
+
return { loaded, text, costUsd, usage, availableSkills, error, stoppedEarly: out.stoppedEarly, durationMs: out.durationMs };
|
|
269
311
|
}
|
|
270
312
|
finally {
|
|
271
313
|
await rm(workdir, { recursive: true, force: true });
|
package/dist/budget.js
CHANGED
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Tokens in input-token equivalents. Anthropic prices every model the same
|
|
3
|
+
* way relative to its input price: output 5x, cache reads 0.1x, 5-minute
|
|
4
|
+
* cache writes 1.25x, 1-hour cache writes 2x.
|
|
5
|
+
*/
|
|
6
|
+
export function inputEquivalent(u) {
|
|
7
|
+
return u.input + 5 * u.output + 0.1 * u.cacheRead + 1.25 * u.cacheWrite5m + 2 * u.cacheWrite1h;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* USD per input token until a finished run shows the real rate. Rounded up
|
|
11
|
+
* where unsure: the budget is a cap, so overestimating is the safe side.
|
|
12
|
+
*/
|
|
13
|
+
function listInputPrice(model) {
|
|
14
|
+
if (model?.includes("haiku"))
|
|
15
|
+
return 1e-6;
|
|
16
|
+
if (model?.includes("sonnet"))
|
|
17
|
+
return 3e-6;
|
|
18
|
+
return 5e-6;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Running spend estimate for --budget. claude usually reports its cost even
|
|
22
|
+
* when early-stop kills it, but a run killed before the result event does
|
|
23
|
+
* not; such runs are priced from their token usage: at the rate seen in
|
|
24
|
+
* finished runs, or at list price before any run finishes. Runs with neither
|
|
25
|
+
* cost nor usage count at the average known cost.
|
|
5
26
|
*/
|
|
6
27
|
export class Budget {
|
|
7
28
|
limitUsd;
|
|
8
29
|
knownSum = 0;
|
|
9
30
|
knownCount = 0;
|
|
10
31
|
unknownCount = 0;
|
|
32
|
+
rateCost = 0;
|
|
33
|
+
rateTokens = 0;
|
|
34
|
+
unpriced = [];
|
|
11
35
|
constructor(limitUsd) {
|
|
12
36
|
this.limitUsd = limitUsd;
|
|
13
37
|
}
|
|
14
|
-
add(cost) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
else {
|
|
38
|
+
add(cost, usage = null) {
|
|
39
|
+
const tokens = usage ? inputEquivalent(usage) : 0;
|
|
40
|
+
if (cost !== null) {
|
|
18
41
|
this.knownSum += cost;
|
|
19
42
|
this.knownCount++;
|
|
43
|
+
if (tokens > 0) {
|
|
44
|
+
this.rateCost += cost;
|
|
45
|
+
this.rateTokens += tokens;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (usage && tokens > 0) {
|
|
49
|
+
this.unpriced.push(usage);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.unknownCount++;
|
|
20
53
|
}
|
|
21
54
|
}
|
|
22
55
|
get spent() {
|
|
23
56
|
const avg = this.knownCount > 0 ? this.knownSum / this.knownCount : 0;
|
|
24
|
-
|
|
57
|
+
const estimated = this.unpriced.reduce((sum, u) => {
|
|
58
|
+
const rate = this.rateTokens > 0 ? this.rateCost / this.rateTokens : listInputPrice(u.model);
|
|
59
|
+
return sum + inputEquivalent(u) * rate;
|
|
60
|
+
}, 0);
|
|
61
|
+
return this.knownSum + estimated + this.unknownCount * avg;
|
|
25
62
|
}
|
|
26
63
|
get exceeded() {
|
|
27
64
|
return this.spent >= this.limitUsd;
|
package/dist/cli.js
CHANGED
|
@@ -440,7 +440,7 @@ class FatalStop {
|
|
|
440
440
|
this.message = error;
|
|
441
441
|
}
|
|
442
442
|
canStart(budget) {
|
|
443
|
-
return this.message === null &&
|
|
443
|
+
return this.message === null && !budget.exceeded;
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
async function runCommand(file, flags, io, deps) {
|
|
@@ -466,7 +466,8 @@ async function runCommand(file, flags, io, deps) {
|
|
|
466
466
|
const n = c.repeat ?? repeat;
|
|
467
467
|
return { c, jobIndex: i, threshold: c.threshold ?? threshold, runs: Array.from({ length: n }), done: 0 };
|
|
468
468
|
});
|
|
469
|
-
|
|
469
|
+
// without --budget it only estimates the spend for the report
|
|
470
|
+
const budget = new Budget(flags.budget ?? Infinity);
|
|
470
471
|
// "--json -" owns stdout, so the human report has to go to stderr
|
|
471
472
|
const reporter = new Reporter(flags.json === "-" ? io.stderr : io.stdout);
|
|
472
473
|
const startedAtMs = Date.now();
|
|
@@ -492,7 +493,8 @@ async function runCommand(file, flags, io, deps) {
|
|
|
492
493
|
unavailable,
|
|
493
494
|
confusion: pairs,
|
|
494
495
|
skipped: skipped.length,
|
|
495
|
-
budget: skipped.length > 0
|
|
496
|
+
budget: skipped.length > 0 ? { limitUsd: flags.budget, spent: budget.spent, notStartedRuns } : undefined,
|
|
497
|
+
estimatedUsd: budget.spent,
|
|
496
498
|
});
|
|
497
499
|
if (flags.batch) {
|
|
498
500
|
// cases that only errored have no answer to confirm
|
|
@@ -511,6 +513,7 @@ async function runCommand(file, flags, io, deps) {
|
|
|
511
513
|
cases: items.map((item) => ({ c: item.c, threshold: item.threshold, result: outcome.results[item.jobIndex] ?? null })),
|
|
512
514
|
unavailable,
|
|
513
515
|
confusion: pairs,
|
|
516
|
+
estimatedCostUsd: budget.spent,
|
|
514
517
|
budgetUsd: flags.budget ?? null,
|
|
515
518
|
budgetReached: skipped.length > 0,
|
|
516
519
|
});
|
|
@@ -558,7 +561,7 @@ async function runIndividually(adapter, items, flags, model, directive, repeat,
|
|
|
558
561
|
available.add(name);
|
|
559
562
|
}
|
|
560
563
|
const verdict = judge(job.item.c, r);
|
|
561
|
-
budget
|
|
564
|
+
budget.add(r.costUsd, r.usage);
|
|
562
565
|
job.item.runs[job.runIndex] = verdict;
|
|
563
566
|
job.item.done++;
|
|
564
567
|
if (job.item.done === job.item.runs.length) {
|
|
@@ -606,7 +609,7 @@ async function runBatched(runBatch, items, flags, model, repeat, reporter, budge
|
|
|
606
609
|
durationMs: br.durationMs,
|
|
607
610
|
};
|
|
608
611
|
const verdict = judge(item.c, r);
|
|
609
|
-
budget
|
|
612
|
+
budget.add(verdict.costUsd);
|
|
610
613
|
item.runs[job.round] = verdict;
|
|
611
614
|
item.done++;
|
|
612
615
|
if (item.done === item.runs.length) {
|
package/dist/junit.js
CHANGED
|
@@ -14,9 +14,10 @@ export function toJunit(report) {
|
|
|
14
14
|
if (report.model !== null)
|
|
15
15
|
properties.push(` <property name="model" value="${esc(report.model)}"/>`);
|
|
16
16
|
properties.push(` <property name="costUsd" value="${report.summary.costUsd.toFixed(2)}"/>`);
|
|
17
|
-
//
|
|
17
|
+
// a run killed before its result event reports no cost, so costUsd alone understates the spend
|
|
18
18
|
if (report.summary.unknownCostRuns > 0) {
|
|
19
19
|
properties.push(` <property name="unknownCostRuns" value="${report.summary.unknownCostRuns}"/>`);
|
|
20
|
+
properties.push(` <property name="estimatedCostUsd" value="${report.summary.estimatedCostUsd.toFixed(2)}"/>`);
|
|
20
21
|
}
|
|
21
22
|
properties.push(" </properties>");
|
|
22
23
|
const cases = rows.map(({ c, kind }) => testcase(c, kind, suiteName));
|
package/dist/report.js
CHANGED
|
@@ -68,7 +68,7 @@ export class Reporter {
|
|
|
68
68
|
const failed = results.filter((r) => !r.ok).length;
|
|
69
69
|
const skipped = extra?.skipped ?? 0;
|
|
70
70
|
const skippedPart = skipped > 0 ? `, ${skipped} skipped` : "";
|
|
71
|
-
this.write(`${failed} failed${skippedPart} of ${results.length + skipped} · runs ${verdicts.length} · ${costLine(verdicts.map((v) => v.costUsd))}\n`);
|
|
71
|
+
this.write(`${failed} failed${skippedPart} of ${results.length + skipped} · runs ${verdicts.length} · ${costLine(verdicts.map((v) => v.costUsd), extra?.estimatedUsd)}\n`);
|
|
72
72
|
}
|
|
73
73
|
paint(code, text) {
|
|
74
74
|
return this.useColor ? `\u001b[${code}m${text}${RESET}` : text;
|
|
@@ -77,12 +77,15 @@ export class Reporter {
|
|
|
77
77
|
this.out.write(text);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
function costLine(costs) {
|
|
80
|
+
function costLine(costs, estimated) {
|
|
81
81
|
const known = costs.filter((c) => c !== null);
|
|
82
|
-
if (known.length === 0)
|
|
83
|
-
return "cost ?";
|
|
84
82
|
const sum = known.reduce((a, b) => a + b, 0);
|
|
85
83
|
const unknown = costs.length - known.length;
|
|
84
|
+
if (unknown > 0 && estimated !== undefined && estimated > sum) {
|
|
85
|
+
return `cost ~$${estimated.toFixed(2)} (${unknown} runs estimated from tokens)`;
|
|
86
|
+
}
|
|
87
|
+
if (known.length === 0)
|
|
88
|
+
return "cost ?";
|
|
86
89
|
const tail = unknown > 0 ? ` (+? for ${unknown} runs)` : "";
|
|
87
90
|
return `cost $${sum.toFixed(2)}${tail}`;
|
|
88
91
|
}
|
package/dist/results.js
CHANGED
|
@@ -19,6 +19,7 @@ export function buildReport(input) {
|
|
|
19
19
|
runs: verdicts.length,
|
|
20
20
|
costUsd: known.reduce((a, b) => a + b, 0),
|
|
21
21
|
unknownCostRuns: costs.length - known.length,
|
|
22
|
+
estimatedCostUsd: input.estimatedCostUsd,
|
|
22
23
|
diagnoses: verdicts.filter((v) => v.diagnosis).length,
|
|
23
24
|
budgetUsd: input.budgetUsd,
|
|
24
25
|
budgetReached: input.budgetReached,
|