@plune-ai/cli 0.27.0 → 0.29.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/README.md +38 -6
- package/dist/cli.cjs +806 -193
- package/dist/index.cjs +272 -137
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +267 -136
- package/dist/reporter-core.cjs +15 -2
- package/dist/reporter-core.d.cts +11 -1
- package/dist/reporter-core.d.ts +11 -1
- package/dist/reporter-core.js +14 -2
- package/package.json +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,24 @@ interface Summary {
|
|
|
15
15
|
skipped?: number;
|
|
16
16
|
cost_usd: number;
|
|
17
17
|
duration_ms: number;
|
|
18
|
+
/** plune-ai/plune#1696. Where the answers came from. Optional and additive; absent in older runs. */
|
|
19
|
+
cache?: CacheSummary;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `from_cache` rows were answered by the store, `from_model` by a model call, `missed` had no entry
|
|
23
|
+
* under `--cache-strict` and were not answered. The `saved` totals are the sum of the rows' `cache_saved`.
|
|
24
|
+
*/
|
|
25
|
+
interface CacheSummary {
|
|
26
|
+
from_cache: number;
|
|
27
|
+
from_model: number;
|
|
28
|
+
missed: number;
|
|
29
|
+
/** Rows the provider failed on: answered by neither, so the four counts add up to `total` (dry-run rows aside). */
|
|
30
|
+
errored: number;
|
|
31
|
+
saved: {
|
|
32
|
+
input_tokens: number;
|
|
33
|
+
output_tokens: number;
|
|
34
|
+
cost_usd: number;
|
|
35
|
+
};
|
|
18
36
|
}
|
|
19
37
|
interface AssertionResultRecord {
|
|
20
38
|
type: string;
|
|
@@ -87,6 +105,12 @@ interface RowResult {
|
|
|
87
105
|
latency_ms?: number;
|
|
88
106
|
error?: RunError;
|
|
89
107
|
assertions: AssertionResultRecord[];
|
|
108
|
+
/** plune-ai/plune#1696. On a cache hit: what the call would have cost. Optional and additive. */
|
|
109
|
+
cache_saved?: {
|
|
110
|
+
input_tokens: number;
|
|
111
|
+
output_tokens: number;
|
|
112
|
+
cost_usd: number;
|
|
113
|
+
};
|
|
90
114
|
/**
|
|
91
115
|
* ADR 0045. Say it when the test was never judged, rather than letting `failed` stand for both
|
|
92
116
|
* "the environment broke" and "the code is wrong".
|
|
@@ -231,6 +255,11 @@ interface CompletionResponse {
|
|
|
231
255
|
* estimated downstream (ADR-PRC02). Additive + optional: existing providers omit it.
|
|
232
256
|
*/
|
|
233
257
|
cost_usd?: number;
|
|
258
|
+
/**
|
|
259
|
+
* Why the model stopped, normalised: `length` (hit the token limit), `error` or `content_filter`
|
|
260
|
+
* mark an answer the cache must not keep. Absent when the provider does not say. Optional and additive.
|
|
261
|
+
*/
|
|
262
|
+
finish_reason?: string;
|
|
234
263
|
}
|
|
235
264
|
interface CostEstimate {
|
|
236
265
|
cost_usd: number;
|
|
@@ -260,6 +289,10 @@ interface Cache {
|
|
|
260
289
|
maxAgeMs?: number;
|
|
261
290
|
}): CompletionResponse | undefined;
|
|
262
291
|
set(key: string, value: CompletionResponse): void;
|
|
292
|
+
/** Remove one entry (a cached answer that turned out unusable). Best-effort. */
|
|
293
|
+
delete(key: string): void;
|
|
294
|
+
/** Entries left by an older policy version, found when the store was opened (0 when none or unknown). */
|
|
295
|
+
readonly staleEntries?: number;
|
|
263
296
|
clear(): void;
|
|
264
297
|
close(): void;
|
|
265
298
|
}
|
|
@@ -280,6 +313,9 @@ interface RunOptions {
|
|
|
280
313
|
concurrency?: number;
|
|
281
314
|
noCache?: boolean;
|
|
282
315
|
bail?: boolean;
|
|
316
|
+
cacheStrict?: boolean;
|
|
317
|
+
/** Milliseconds; parsed from `--cache-max-age` by the caller. */
|
|
318
|
+
cacheMaxAgeMs?: number;
|
|
283
319
|
}
|
|
284
320
|
declare function handleRun(options: RunOptions, depsFactory?: (config: Config, baseDir: string) => RunDeps): Promise<RunResult>;
|
|
285
321
|
|
|
@@ -449,6 +485,17 @@ declare const runResultSchema: z.ZodObject<{
|
|
|
449
485
|
skipped: z.ZodOptional<z.ZodNumber>;
|
|
450
486
|
cost_usd: z.ZodNumber;
|
|
451
487
|
duration_ms: z.ZodNumber;
|
|
488
|
+
cache: z.ZodOptional<z.ZodObject<{
|
|
489
|
+
from_cache: z.ZodNumber;
|
|
490
|
+
from_model: z.ZodNumber;
|
|
491
|
+
missed: z.ZodNumber;
|
|
492
|
+
errored: z.ZodNumber;
|
|
493
|
+
saved: z.ZodObject<{
|
|
494
|
+
input_tokens: z.ZodNumber;
|
|
495
|
+
output_tokens: z.ZodNumber;
|
|
496
|
+
cost_usd: z.ZodNumber;
|
|
497
|
+
}, z.core.$strip>;
|
|
498
|
+
}, z.core.$strip>>;
|
|
452
499
|
}, z.core.$strip>;
|
|
453
500
|
evals: z.ZodArray<z.ZodObject<{
|
|
454
501
|
id: z.ZodString;
|
|
@@ -477,6 +524,11 @@ declare const runResultSchema: z.ZodObject<{
|
|
|
477
524
|
passed: z.ZodBoolean;
|
|
478
525
|
}, z.core.$strip>>>;
|
|
479
526
|
}, z.core.$strip>>;
|
|
527
|
+
cache_saved: z.ZodOptional<z.ZodObject<{
|
|
528
|
+
input_tokens: z.ZodNumber;
|
|
529
|
+
output_tokens: z.ZodNumber;
|
|
530
|
+
cost_usd: z.ZodNumber;
|
|
531
|
+
}, z.core.$strip>>;
|
|
480
532
|
inconclusive: z.ZodOptional<z.ZodEnum<{
|
|
481
533
|
blocked: "blocked";
|
|
482
534
|
skipped: "skipped";
|
package/dist/index.js
CHANGED
|
@@ -452,7 +452,8 @@ function makeAnthropicProvider(config, env, pricing) {
|
|
|
452
452
|
usage: {
|
|
453
453
|
input_tokens: res.usage.input_tokens,
|
|
454
454
|
output_tokens: res.usage.output_tokens
|
|
455
|
-
}
|
|
455
|
+
},
|
|
456
|
+
...res.stop_reason === "max_tokens" ? { finish_reason: "length" } : res.stop_reason !== null && res.stop_reason !== void 0 ? { finish_reason: res.stop_reason } : {}
|
|
456
457
|
};
|
|
457
458
|
} catch (err) {
|
|
458
459
|
throw normalizeProviderError(err, apiKey, ENV_VAR);
|
|
@@ -499,8 +500,11 @@ function makeOpenAiCompatibleProvider(opts) {
|
|
|
499
500
|
{ max_retries: maxRetries }
|
|
500
501
|
);
|
|
501
502
|
const reportedCost = reportsCost ? res.usage?.cost : void 0;
|
|
503
|
+
const choice = res.choices[0];
|
|
504
|
+
const finish = choice?.error !== void 0 ? "error" : choice?.finish_reason;
|
|
502
505
|
return {
|
|
503
|
-
output:
|
|
506
|
+
output: choice?.message?.content ?? "",
|
|
507
|
+
...typeof finish === "string" ? { finish_reason: finish } : {},
|
|
504
508
|
usage: {
|
|
505
509
|
input_tokens: res.usage?.prompt_tokens ?? 0,
|
|
506
510
|
output_tokens: res.usage?.completion_tokens ?? 0
|
|
@@ -635,12 +639,94 @@ function getDefaultEmbedder() {
|
|
|
635
639
|
}
|
|
636
640
|
|
|
637
641
|
// src/cache/cache.ts
|
|
638
|
-
import
|
|
642
|
+
import { createRequire } from "module";
|
|
643
|
+
|
|
644
|
+
// src/util/hash.ts
|
|
645
|
+
import { createHash } from "crypto";
|
|
646
|
+
|
|
647
|
+
// src/util/canonical-json.ts
|
|
648
|
+
function normalizeValue(value) {
|
|
649
|
+
if (value === null) return null;
|
|
650
|
+
if (typeof value === "boolean") return value;
|
|
651
|
+
if (typeof value === "number") {
|
|
652
|
+
if (!isFinite(value)) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`canonicalJson: non-finite number rejected: ${String(value)}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return value === 0 ? 0 : value;
|
|
658
|
+
}
|
|
659
|
+
if (typeof value === "string") {
|
|
660
|
+
return value.normalize("NFC");
|
|
661
|
+
}
|
|
662
|
+
if (Array.isArray(value)) {
|
|
663
|
+
return value.map(normalizeValue);
|
|
664
|
+
}
|
|
665
|
+
if (typeof value === "object") {
|
|
666
|
+
const proto = Object.getPrototypeOf(value);
|
|
667
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
668
|
+
const name = value.constructor?.name ?? "unknown";
|
|
669
|
+
throw new Error(
|
|
670
|
+
`canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const obj = value;
|
|
674
|
+
const sorted = {};
|
|
675
|
+
for (const key of Object.keys(obj).sort()) {
|
|
676
|
+
sorted[key] = normalizeValue(obj[key]);
|
|
677
|
+
}
|
|
678
|
+
return sorted;
|
|
679
|
+
}
|
|
680
|
+
throw new Error(
|
|
681
|
+
`canonicalJson: unsupported value type: ${typeof value}`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
function canonicalJson(value) {
|
|
685
|
+
return JSON.stringify(normalizeValue(value));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/util/hash.ts
|
|
689
|
+
function sha256(input) {
|
|
690
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
691
|
+
}
|
|
692
|
+
var CACHE_POLICY_VERSION = 1;
|
|
693
|
+
function cacheKey(inputs, policyVersion = CACHE_POLICY_VERSION) {
|
|
694
|
+
const { provider, model, temperature, max_tokens, prompt_resolved } = inputs;
|
|
695
|
+
return sha256(
|
|
696
|
+
canonicalJson({ provider, model, temperature, max_tokens, prompt_resolved, policy: policyVersion })
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
function configHash(config) {
|
|
700
|
+
return sha256(canonicalJson(config));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/cache/cache.ts
|
|
639
704
|
var SCHEMA_VERSION = 1;
|
|
640
705
|
var META_DDL = "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);";
|
|
641
706
|
var COMPLETIONS_DDL = "CREATE TABLE IF NOT EXISTS completions (cache_key TEXT PRIMARY KEY, output TEXT NOT NULL, input_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL, created_at INTEGER NOT NULL);";
|
|
707
|
+
var UNCACHEABLE_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "error", "content_filter", "pause_turn"]);
|
|
708
|
+
function isCacheable(res) {
|
|
709
|
+
if (res.output.trim() === "") return false;
|
|
710
|
+
return res.finish_reason === void 0 || !UNCACHEABLE_FINISH.has(res.finish_reason);
|
|
711
|
+
}
|
|
712
|
+
var missingNoted = false;
|
|
713
|
+
function loadDatabase() {
|
|
714
|
+
try {
|
|
715
|
+
return createRequire(import.meta.url)("better-sqlite3");
|
|
716
|
+
} catch {
|
|
717
|
+
if (!missingNoted) {
|
|
718
|
+
missingNoted = true;
|
|
719
|
+
console.error(
|
|
720
|
+
"plune: completion cache disabled (better-sqlite3 is not installed or failed to load). Reinstall with a C++ toolchain and Python available, or ignore: runs work without the cache."
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
642
726
|
function openCache(file, opts = {}) {
|
|
643
727
|
const now = opts.now ?? Date.now;
|
|
728
|
+
const Database = loadDatabase();
|
|
729
|
+
if (Database === void 0) return new NoOpCache();
|
|
644
730
|
try {
|
|
645
731
|
const db = new Database(file);
|
|
646
732
|
try {
|
|
@@ -649,7 +735,14 @@ function openCache(file, opts = {}) {
|
|
|
649
735
|
db.close();
|
|
650
736
|
throw err;
|
|
651
737
|
}
|
|
652
|
-
|
|
738
|
+
let stale;
|
|
739
|
+
try {
|
|
740
|
+
stale = markPolicy(db, opts.policyVersion ?? CACHE_POLICY_VERSION);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
db.close();
|
|
743
|
+
throw err;
|
|
744
|
+
}
|
|
745
|
+
return new SqliteCache(db, now, stale);
|
|
653
746
|
} catch {
|
|
654
747
|
return new NoOpCache();
|
|
655
748
|
}
|
|
@@ -664,17 +757,33 @@ function ensureSchema(db) {
|
|
|
664
757
|
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("schema_version", current);
|
|
665
758
|
}
|
|
666
759
|
}
|
|
760
|
+
function markPolicy(db, version) {
|
|
761
|
+
const current = String(version);
|
|
762
|
+
const stored = db.prepare("SELECT value FROM meta WHERE key = 'policy_version'").get()?.value;
|
|
763
|
+
if (stored === current) return 0;
|
|
764
|
+
return db.transaction(() => {
|
|
765
|
+
const { n } = db.prepare("SELECT COUNT(*) AS n FROM completions").get();
|
|
766
|
+
db.exec("DELETE FROM completions");
|
|
767
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("policy_version", current);
|
|
768
|
+
return n;
|
|
769
|
+
})();
|
|
770
|
+
}
|
|
667
771
|
var SqliteCache = class {
|
|
668
|
-
constructor(db, now) {
|
|
772
|
+
constructor(db, now, staleEntries) {
|
|
669
773
|
this.db = db;
|
|
670
774
|
this.now = now;
|
|
775
|
+
this.staleEntries = staleEntries;
|
|
671
776
|
}
|
|
672
777
|
db;
|
|
673
778
|
now;
|
|
779
|
+
staleEntries;
|
|
674
780
|
get(key, opts = {}) {
|
|
675
781
|
try {
|
|
676
782
|
const row = this.db.prepare("SELECT output, input_tokens, output_tokens, created_at FROM completions WHERE cache_key = ?").get(key);
|
|
677
783
|
if (row === void 0) return void 0;
|
|
784
|
+
if (typeof row.output !== "string" || !Number.isFinite(row.input_tokens) || !Number.isFinite(row.output_tokens) || !Number.isFinite(row.created_at)) {
|
|
785
|
+
return void 0;
|
|
786
|
+
}
|
|
678
787
|
if (opts.maxAgeMs !== void 0 && this.now() - row.created_at > opts.maxAgeMs) {
|
|
679
788
|
return void 0;
|
|
680
789
|
}
|
|
@@ -700,6 +809,12 @@ var SqliteCache = class {
|
|
|
700
809
|
} catch {
|
|
701
810
|
}
|
|
702
811
|
}
|
|
812
|
+
delete(key) {
|
|
813
|
+
try {
|
|
814
|
+
this.db.prepare("DELETE FROM completions WHERE cache_key = ?").run(key);
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}
|
|
703
818
|
clear() {
|
|
704
819
|
try {
|
|
705
820
|
this.db.exec("DELETE FROM completions");
|
|
@@ -716,6 +831,8 @@ var NoOpCache = class {
|
|
|
716
831
|
}
|
|
717
832
|
set() {
|
|
718
833
|
}
|
|
834
|
+
delete() {
|
|
835
|
+
}
|
|
719
836
|
clear() {
|
|
720
837
|
}
|
|
721
838
|
close() {
|
|
@@ -726,60 +843,72 @@ var NoOpCache = class {
|
|
|
726
843
|
import * as fs3 from "fs";
|
|
727
844
|
import * as path2 from "path";
|
|
728
845
|
|
|
729
|
-
// src/
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
function normalizeValue(value) {
|
|
734
|
-
if (value === null) return null;
|
|
735
|
-
if (typeof value === "boolean") return value;
|
|
736
|
-
if (typeof value === "number") {
|
|
737
|
-
if (!isFinite(value)) {
|
|
738
|
-
throw new Error(
|
|
739
|
-
`canonicalJson: non-finite number rejected: ${String(value)}`
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
return value === 0 ? 0 : value;
|
|
743
|
-
}
|
|
744
|
-
if (typeof value === "string") {
|
|
745
|
-
return value.normalize("NFC");
|
|
846
|
+
// src/assertions/json-extract.ts
|
|
847
|
+
function extractJson(output, mode) {
|
|
848
|
+
if (mode === "strict") {
|
|
849
|
+
return tryParse(output.trim());
|
|
746
850
|
}
|
|
747
|
-
|
|
748
|
-
|
|
851
|
+
const fenced = extractFencedBlock(output);
|
|
852
|
+
if (fenced !== null) {
|
|
853
|
+
const r = tryParse(fenced);
|
|
854
|
+
if (r.ok) return r;
|
|
749
855
|
}
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
if (
|
|
753
|
-
const name = value.constructor?.name ?? "unknown";
|
|
754
|
-
throw new Error(
|
|
755
|
-
`canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
const obj = value;
|
|
759
|
-
const sorted = {};
|
|
760
|
-
for (const key of Object.keys(obj).sort()) {
|
|
761
|
-
sorted[key] = normalizeValue(obj[key]);
|
|
762
|
-
}
|
|
763
|
-
return sorted;
|
|
856
|
+
for (const run of balancedRuns(output)) {
|
|
857
|
+
const r = tryParse(run);
|
|
858
|
+
if (r.ok) return r;
|
|
764
859
|
}
|
|
765
|
-
|
|
766
|
-
`canonicalJson: unsupported value type: ${typeof value}`
|
|
767
|
-
);
|
|
860
|
+
return { ok: false };
|
|
768
861
|
}
|
|
769
|
-
function
|
|
770
|
-
|
|
862
|
+
function tryParse(s) {
|
|
863
|
+
try {
|
|
864
|
+
return { ok: true, value: JSON.parse(s) };
|
|
865
|
+
} catch {
|
|
866
|
+
return { ok: false };
|
|
867
|
+
}
|
|
771
868
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
869
|
+
function extractFencedBlock(s) {
|
|
870
|
+
const m = s.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
|
|
871
|
+
return m?.[1]?.trim() ?? null;
|
|
776
872
|
}
|
|
777
|
-
function
|
|
778
|
-
|
|
779
|
-
|
|
873
|
+
function* balancedRuns(s) {
|
|
874
|
+
let i = 0;
|
|
875
|
+
while (i < s.length) {
|
|
876
|
+
const rel = s.slice(i).search(/[{[]/);
|
|
877
|
+
if (rel === -1) return;
|
|
878
|
+
const start = i + rel;
|
|
879
|
+
const run = balancedFrom(s, start);
|
|
880
|
+
if (run === null) {
|
|
881
|
+
i = start + 1;
|
|
882
|
+
} else {
|
|
883
|
+
yield run;
|
|
884
|
+
i = start + run.length;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
780
887
|
}
|
|
781
|
-
function
|
|
782
|
-
|
|
888
|
+
function balancedFrom(s, start) {
|
|
889
|
+
const open = s[start];
|
|
890
|
+
const close = open === "{" ? "}" : "]";
|
|
891
|
+
let depth = 0;
|
|
892
|
+
let inString = false;
|
|
893
|
+
let escaped = false;
|
|
894
|
+
for (let i = start; i < s.length; i++) {
|
|
895
|
+
const ch = s[i];
|
|
896
|
+
if (inString) {
|
|
897
|
+
if (escaped) escaped = false;
|
|
898
|
+
else if (ch === "\\") escaped = true;
|
|
899
|
+
else if (ch === '"') inString = false;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (ch === '"') {
|
|
903
|
+
inString = true;
|
|
904
|
+
} else if (ch === open) {
|
|
905
|
+
depth++;
|
|
906
|
+
} else if (ch === close) {
|
|
907
|
+
depth--;
|
|
908
|
+
if (depth === 0) return s.slice(start, i + 1);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return null;
|
|
783
912
|
}
|
|
784
913
|
|
|
785
914
|
// src/assertions/interpolate.ts
|
|
@@ -855,76 +984,6 @@ var containsAll = {
|
|
|
855
984
|
|
|
856
985
|
// src/assertions/json-schema.ts
|
|
857
986
|
import { Ajv } from "ajv";
|
|
858
|
-
|
|
859
|
-
// src/assertions/json-extract.ts
|
|
860
|
-
function extractJson(output, mode) {
|
|
861
|
-
if (mode === "strict") {
|
|
862
|
-
return tryParse(output.trim());
|
|
863
|
-
}
|
|
864
|
-
const fenced = extractFencedBlock(output);
|
|
865
|
-
if (fenced !== null) {
|
|
866
|
-
const r = tryParse(fenced);
|
|
867
|
-
if (r.ok) return r;
|
|
868
|
-
}
|
|
869
|
-
for (const run of balancedRuns(output)) {
|
|
870
|
-
const r = tryParse(run);
|
|
871
|
-
if (r.ok) return r;
|
|
872
|
-
}
|
|
873
|
-
return { ok: false };
|
|
874
|
-
}
|
|
875
|
-
function tryParse(s) {
|
|
876
|
-
try {
|
|
877
|
-
return { ok: true, value: JSON.parse(s) };
|
|
878
|
-
} catch {
|
|
879
|
-
return { ok: false };
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
function extractFencedBlock(s) {
|
|
883
|
-
const m = s.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
|
|
884
|
-
return m?.[1]?.trim() ?? null;
|
|
885
|
-
}
|
|
886
|
-
function* balancedRuns(s) {
|
|
887
|
-
let i = 0;
|
|
888
|
-
while (i < s.length) {
|
|
889
|
-
const rel = s.slice(i).search(/[{[]/);
|
|
890
|
-
if (rel === -1) return;
|
|
891
|
-
const start = i + rel;
|
|
892
|
-
const run = balancedFrom(s, start);
|
|
893
|
-
if (run === null) {
|
|
894
|
-
i = start + 1;
|
|
895
|
-
} else {
|
|
896
|
-
yield run;
|
|
897
|
-
i = start + run.length;
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
function balancedFrom(s, start) {
|
|
902
|
-
const open = s[start];
|
|
903
|
-
const close = open === "{" ? "}" : "]";
|
|
904
|
-
let depth = 0;
|
|
905
|
-
let inString = false;
|
|
906
|
-
let escaped = false;
|
|
907
|
-
for (let i = start; i < s.length; i++) {
|
|
908
|
-
const ch = s[i];
|
|
909
|
-
if (inString) {
|
|
910
|
-
if (escaped) escaped = false;
|
|
911
|
-
else if (ch === "\\") escaped = true;
|
|
912
|
-
else if (ch === '"') inString = false;
|
|
913
|
-
continue;
|
|
914
|
-
}
|
|
915
|
-
if (ch === '"') {
|
|
916
|
-
inString = true;
|
|
917
|
-
} else if (ch === open) {
|
|
918
|
-
depth++;
|
|
919
|
-
} else if (ch === close) {
|
|
920
|
-
depth--;
|
|
921
|
-
if (depth === 0) return s.slice(start, i + 1);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
return null;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// src/assertions/json-schema.ts
|
|
928
987
|
var jsonSchema = {
|
|
929
988
|
run(ctx) {
|
|
930
989
|
const mode = ctx.params.extract ?? "auto";
|
|
@@ -1284,6 +1343,12 @@ var RunConfigError = class extends Error {
|
|
|
1284
1343
|
this.name = "RunConfigError";
|
|
1285
1344
|
}
|
|
1286
1345
|
};
|
|
1346
|
+
var CACHE_MISS_CODE = "CACHE_MISS";
|
|
1347
|
+
function answerParses(output, assertions) {
|
|
1348
|
+
return assertions.every(
|
|
1349
|
+
(a) => a.type !== "json-schema" || extractJson(output, a.extract ?? "auto").ok
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1287
1352
|
function resolvePrompt(template, vars) {
|
|
1288
1353
|
return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => {
|
|
1289
1354
|
if (!(key in vars)) {
|
|
@@ -1311,17 +1376,35 @@ async function runRow(p) {
|
|
|
1311
1376
|
const usage2 = { ...est, cost_usd: p.provider.estimateCost(est).cost_usd };
|
|
1312
1377
|
return { vars: p.row.vars, output: null, cached: false, usage: usage2, assertions: [] };
|
|
1313
1378
|
}
|
|
1314
|
-
const key = cacheKey(
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1379
|
+
const key = cacheKey(
|
|
1380
|
+
{
|
|
1381
|
+
provider: p.providerConfig.type,
|
|
1382
|
+
model: p.providerConfig.model,
|
|
1383
|
+
temperature: temperature ?? 0,
|
|
1384
|
+
max_tokens: maxTokens,
|
|
1385
|
+
prompt_resolved: prompt5
|
|
1386
|
+
},
|
|
1387
|
+
p.policyVersion
|
|
1388
|
+
);
|
|
1321
1389
|
let output;
|
|
1322
1390
|
let usage;
|
|
1323
1391
|
let cached = false;
|
|
1324
|
-
|
|
1392
|
+
let cacheSaved;
|
|
1393
|
+
let hit = p.noCache ? void 0 : p.cache.get(key, p.cacheMaxAgeMs !== void 0 ? { maxAgeMs: p.cacheMaxAgeMs } : {});
|
|
1394
|
+
if (hit !== void 0 && !answerParses(hit.output, p.assertions)) {
|
|
1395
|
+
if (p.cacheStrict !== true) p.cache.delete(key);
|
|
1396
|
+
hit = void 0;
|
|
1397
|
+
}
|
|
1398
|
+
if (hit === void 0 && p.cacheStrict === true) {
|
|
1399
|
+
return {
|
|
1400
|
+
vars: p.row.vars,
|
|
1401
|
+
output: null,
|
|
1402
|
+
cached: false,
|
|
1403
|
+
latency_ms: p.now() - t0,
|
|
1404
|
+
error: { code: CACHE_MISS_CODE, message: "no usable cache entry (--cache-strict)" },
|
|
1405
|
+
assertions: []
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1325
1408
|
if (hit !== void 0) {
|
|
1326
1409
|
output = hit.output;
|
|
1327
1410
|
cached = true;
|
|
@@ -1330,6 +1413,11 @@ async function runRow(p) {
|
|
|
1330
1413
|
output_tokens: hit.usage.output_tokens,
|
|
1331
1414
|
cost_usd: 0
|
|
1332
1415
|
};
|
|
1416
|
+
cacheSaved = {
|
|
1417
|
+
input_tokens: hit.usage.input_tokens,
|
|
1418
|
+
output_tokens: hit.usage.output_tokens,
|
|
1419
|
+
cost_usd: p.provider.estimateCost(hit.usage).cost_usd
|
|
1420
|
+
};
|
|
1333
1421
|
} else {
|
|
1334
1422
|
try {
|
|
1335
1423
|
const res = await p.provider.complete({
|
|
@@ -1347,7 +1435,9 @@ async function runRow(p) {
|
|
|
1347
1435
|
// lets a config pricing override win, else falls back to the table estimate (ADR-PRC01).
|
|
1348
1436
|
cost_usd: p.provider.estimateCost(res.usage, res.cost_usd).cost_usd
|
|
1349
1437
|
};
|
|
1350
|
-
if (!p.noCache)
|
|
1438
|
+
if (!p.noCache && isCacheable(res) && answerParses(res.output, p.assertions)) {
|
|
1439
|
+
p.cache.set(key, res);
|
|
1440
|
+
}
|
|
1351
1441
|
} catch (err) {
|
|
1352
1442
|
return {
|
|
1353
1443
|
vars: p.row.vars,
|
|
@@ -1393,6 +1483,7 @@ async function runRow(p) {
|
|
|
1393
1483
|
cached,
|
|
1394
1484
|
usage: finalUsage,
|
|
1395
1485
|
latency_ms: p.now() - t0,
|
|
1486
|
+
...cacheSaved !== void 0 ? { cache_saved: cacheSaved } : {},
|
|
1396
1487
|
...assertionError !== void 0 ? { error: assertionError } : {},
|
|
1397
1488
|
assertions: records
|
|
1398
1489
|
};
|
|
@@ -1427,19 +1518,41 @@ function summarize(evals, durationMs) {
|
|
|
1427
1518
|
cost_usd: 0,
|
|
1428
1519
|
duration_ms: durationMs
|
|
1429
1520
|
};
|
|
1521
|
+
const cache = {
|
|
1522
|
+
from_cache: 0,
|
|
1523
|
+
from_model: 0,
|
|
1524
|
+
missed: 0,
|
|
1525
|
+
errored: 0,
|
|
1526
|
+
saved: { input_tokens: 0, output_tokens: 0, cost_usd: 0 }
|
|
1527
|
+
};
|
|
1430
1528
|
for (const ev of evals) {
|
|
1431
1529
|
for (const row of ev.rows) {
|
|
1432
1530
|
summary.total += 1;
|
|
1433
1531
|
summary[rowStatus(row)] += 1;
|
|
1434
1532
|
summary.cost_usd += row.usage?.cost_usd ?? 0;
|
|
1533
|
+
if (row.cached) {
|
|
1534
|
+
cache.from_cache += 1;
|
|
1535
|
+
cache.saved.input_tokens += row.cache_saved?.input_tokens ?? 0;
|
|
1536
|
+
cache.saved.output_tokens += row.cache_saved?.output_tokens ?? 0;
|
|
1537
|
+
cache.saved.cost_usd += row.cache_saved?.cost_usd ?? 0;
|
|
1538
|
+
} else if (row.error?.code === CACHE_MISS_CODE) cache.missed += 1;
|
|
1539
|
+
else if (row.output !== null) cache.from_model += 1;
|
|
1540
|
+
else if (row.error !== void 0) cache.errored += 1;
|
|
1435
1541
|
}
|
|
1436
1542
|
}
|
|
1437
|
-
return summary;
|
|
1543
|
+
return { ...summary, cache };
|
|
1438
1544
|
}
|
|
1439
1545
|
async function runOrchestration(config, options, deps) {
|
|
1546
|
+
const noCache = options.noCache ?? false;
|
|
1547
|
+
const cacheStrict = options.cacheStrict ?? false;
|
|
1548
|
+
if (noCache && cacheStrict) {
|
|
1549
|
+
throw new RunConfigError("--no-cache and --cache-strict cannot be used together");
|
|
1550
|
+
}
|
|
1551
|
+
if (noCache && options.cacheMaxAgeMs !== void 0) {
|
|
1552
|
+
throw new RunConfigError("--no-cache and --cache-max-age cannot be used together");
|
|
1553
|
+
}
|
|
1440
1554
|
const started = deps.now();
|
|
1441
1555
|
const dryRun = options.dryRun ?? false;
|
|
1442
|
-
const noCache = options.noCache ?? false;
|
|
1443
1556
|
const evalResults = [];
|
|
1444
1557
|
const prepared = selectEvals(config.evals, options.only).map((ev) => {
|
|
1445
1558
|
const template = resolveTemplate(ev, deps.baseDir);
|
|
@@ -1466,7 +1579,9 @@ async function runOrchestration(config, options, deps) {
|
|
|
1466
1579
|
cache: deps.cache,
|
|
1467
1580
|
now: deps.now,
|
|
1468
1581
|
dryRun,
|
|
1469
|
-
noCache
|
|
1582
|
+
noCache,
|
|
1583
|
+
cacheStrict,
|
|
1584
|
+
...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
|
|
1470
1585
|
})
|
|
1471
1586
|
);
|
|
1472
1587
|
const passed = rowResults.every((r) => rowStatus(r) === "passed");
|
|
@@ -1499,6 +1614,7 @@ function loadDataset(ref, baseDir) {
|
|
|
1499
1614
|
|
|
1500
1615
|
// src/cli/commands/run.ts
|
|
1501
1616
|
var NOOP_CACHE = { get: () => void 0, set: () => {
|
|
1617
|
+
}, delete: () => {
|
|
1502
1618
|
}, clear: () => {
|
|
1503
1619
|
}, close: () => {
|
|
1504
1620
|
} };
|
|
@@ -1535,6 +1651,11 @@ async function handleRun(options, depsFactory) {
|
|
|
1535
1651
|
const factory = depsFactory ?? ((c, d) => buildRealDeps(c, d, options.dryRun));
|
|
1536
1652
|
const deps = factory(config, baseDir);
|
|
1537
1653
|
try {
|
|
1654
|
+
const stale = deps.cache.staleEntries ?? 0;
|
|
1655
|
+
if (stale > 0) {
|
|
1656
|
+
process.stderr.write(`cache policy changed, ${stale} entries are stale (dropped)
|
|
1657
|
+
`);
|
|
1658
|
+
}
|
|
1538
1659
|
const result = await runOrchestration(
|
|
1539
1660
|
config,
|
|
1540
1661
|
{
|
|
@@ -1542,7 +1663,9 @@ async function handleRun(options, depsFactory) {
|
|
|
1542
1663
|
...options.only !== void 0 ? { only: options.only } : {},
|
|
1543
1664
|
...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
|
|
1544
1665
|
...options.noCache !== void 0 ? { noCache: options.noCache } : {},
|
|
1545
|
-
...options.bail !== void 0 ? { bail: options.bail } : {}
|
|
1666
|
+
...options.bail !== void 0 ? { bail: options.bail } : {},
|
|
1667
|
+
...options.cacheStrict === true ? { cacheStrict: true } : {},
|
|
1668
|
+
...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
|
|
1546
1669
|
},
|
|
1547
1670
|
deps
|
|
1548
1671
|
);
|
|
@@ -1589,6 +1712,7 @@ var rowResultSchema = z3.object({
|
|
|
1589
1712
|
latency_ms: z3.number().optional(),
|
|
1590
1713
|
error: runErrorSchema.optional(),
|
|
1591
1714
|
assertions: z3.array(assertionResultRecordSchema2),
|
|
1715
|
+
cache_saved: z3.object({ input_tokens: z3.number(), output_tokens: z3.number(), cost_usd: z3.number() }).optional(),
|
|
1592
1716
|
/** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
|
|
1593
1717
|
inconclusive: z3.enum(ROW_INCONCLUSIVE).optional()
|
|
1594
1718
|
});
|
|
@@ -1601,7 +1725,14 @@ var summarySchema = z3.object({
|
|
|
1601
1725
|
blocked: z3.number().optional(),
|
|
1602
1726
|
skipped: z3.number().optional(),
|
|
1603
1727
|
cost_usd: z3.number(),
|
|
1604
|
-
duration_ms: z3.number()
|
|
1728
|
+
duration_ms: z3.number(),
|
|
1729
|
+
cache: z3.object({
|
|
1730
|
+
from_cache: z3.number(),
|
|
1731
|
+
from_model: z3.number(),
|
|
1732
|
+
missed: z3.number(),
|
|
1733
|
+
errored: z3.number(),
|
|
1734
|
+
saved: z3.object({ input_tokens: z3.number(), output_tokens: z3.number(), cost_usd: z3.number() })
|
|
1735
|
+
}).optional()
|
|
1605
1736
|
});
|
|
1606
1737
|
var evalResultSchema = z3.object({
|
|
1607
1738
|
id: z3.string(),
|