@plune-ai/cli 0.27.0 → 0.28.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 +22 -6
- package/dist/cli.cjs +519 -161
- 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/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -54,6 +54,10 @@ __export(src_exports, {
|
|
|
54
54
|
});
|
|
55
55
|
module.exports = __toCommonJS(src_exports);
|
|
56
56
|
|
|
57
|
+
// node_modules/.pnpm/tsup@8.5.1_postcss@8.5.15_tsx@4.22.4_typescript@5.9.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
|
|
58
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
59
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
60
|
+
|
|
57
61
|
// src/cli/commands/run.ts
|
|
58
62
|
var fs5 = __toESM(require("fs"), 1);
|
|
59
63
|
var path4 = __toESM(require("path"), 1);
|
|
@@ -508,7 +512,8 @@ function makeAnthropicProvider(config, env, pricing) {
|
|
|
508
512
|
usage: {
|
|
509
513
|
input_tokens: res.usage.input_tokens,
|
|
510
514
|
output_tokens: res.usage.output_tokens
|
|
511
|
-
}
|
|
515
|
+
},
|
|
516
|
+
...res.stop_reason === "max_tokens" ? { finish_reason: "length" } : res.stop_reason !== null && res.stop_reason !== void 0 ? { finish_reason: res.stop_reason } : {}
|
|
512
517
|
};
|
|
513
518
|
} catch (err) {
|
|
514
519
|
throw normalizeProviderError(err, apiKey, ENV_VAR);
|
|
@@ -555,8 +560,11 @@ function makeOpenAiCompatibleProvider(opts) {
|
|
|
555
560
|
{ max_retries: maxRetries }
|
|
556
561
|
);
|
|
557
562
|
const reportedCost = reportsCost ? res.usage?.cost : void 0;
|
|
563
|
+
const choice = res.choices[0];
|
|
564
|
+
const finish = choice?.error !== void 0 ? "error" : choice?.finish_reason;
|
|
558
565
|
return {
|
|
559
|
-
output:
|
|
566
|
+
output: choice?.message?.content ?? "",
|
|
567
|
+
...typeof finish === "string" ? { finish_reason: finish } : {},
|
|
560
568
|
usage: {
|
|
561
569
|
input_tokens: res.usage?.prompt_tokens ?? 0,
|
|
562
570
|
output_tokens: res.usage?.completion_tokens ?? 0
|
|
@@ -691,21 +699,110 @@ function getDefaultEmbedder() {
|
|
|
691
699
|
}
|
|
692
700
|
|
|
693
701
|
// src/cache/cache.ts
|
|
694
|
-
var
|
|
702
|
+
var import_node_module = require("module");
|
|
703
|
+
|
|
704
|
+
// src/util/hash.ts
|
|
705
|
+
var import_node_crypto = require("crypto");
|
|
706
|
+
|
|
707
|
+
// src/util/canonical-json.ts
|
|
708
|
+
function normalizeValue(value) {
|
|
709
|
+
if (value === null) return null;
|
|
710
|
+
if (typeof value === "boolean") return value;
|
|
711
|
+
if (typeof value === "number") {
|
|
712
|
+
if (!isFinite(value)) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
`canonicalJson: non-finite number rejected: ${String(value)}`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
return value === 0 ? 0 : value;
|
|
718
|
+
}
|
|
719
|
+
if (typeof value === "string") {
|
|
720
|
+
return value.normalize("NFC");
|
|
721
|
+
}
|
|
722
|
+
if (Array.isArray(value)) {
|
|
723
|
+
return value.map(normalizeValue);
|
|
724
|
+
}
|
|
725
|
+
if (typeof value === "object") {
|
|
726
|
+
const proto = Object.getPrototypeOf(value);
|
|
727
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
728
|
+
const name = value.constructor?.name ?? "unknown";
|
|
729
|
+
throw new Error(
|
|
730
|
+
`canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
const obj = value;
|
|
734
|
+
const sorted = {};
|
|
735
|
+
for (const key of Object.keys(obj).sort()) {
|
|
736
|
+
sorted[key] = normalizeValue(obj[key]);
|
|
737
|
+
}
|
|
738
|
+
return sorted;
|
|
739
|
+
}
|
|
740
|
+
throw new Error(
|
|
741
|
+
`canonicalJson: unsupported value type: ${typeof value}`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
function canonicalJson(value) {
|
|
745
|
+
return JSON.stringify(normalizeValue(value));
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/util/hash.ts
|
|
749
|
+
function sha256(input) {
|
|
750
|
+
return (0, import_node_crypto.createHash)("sha256").update(input, "utf8").digest("hex");
|
|
751
|
+
}
|
|
752
|
+
var CACHE_POLICY_VERSION = 1;
|
|
753
|
+
function cacheKey(inputs, policyVersion = CACHE_POLICY_VERSION) {
|
|
754
|
+
const { provider, model, temperature, max_tokens, prompt_resolved } = inputs;
|
|
755
|
+
return sha256(
|
|
756
|
+
canonicalJson({ provider, model, temperature, max_tokens, prompt_resolved, policy: policyVersion })
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
function configHash(config) {
|
|
760
|
+
return sha256(canonicalJson(config));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/cache/cache.ts
|
|
695
764
|
var SCHEMA_VERSION = 1;
|
|
696
765
|
var META_DDL = "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);";
|
|
697
766
|
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);";
|
|
767
|
+
var UNCACHEABLE_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "error", "content_filter", "pause_turn"]);
|
|
768
|
+
function isCacheable(res) {
|
|
769
|
+
if (res.output.trim() === "") return false;
|
|
770
|
+
return res.finish_reason === void 0 || !UNCACHEABLE_FINISH.has(res.finish_reason);
|
|
771
|
+
}
|
|
772
|
+
var missingNoted = false;
|
|
773
|
+
function loadDatabase() {
|
|
774
|
+
try {
|
|
775
|
+
return (0, import_node_module.createRequire)(importMetaUrl)("better-sqlite3");
|
|
776
|
+
} catch {
|
|
777
|
+
if (!missingNoted) {
|
|
778
|
+
missingNoted = true;
|
|
779
|
+
console.error(
|
|
780
|
+
"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."
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
698
786
|
function openCache(file, opts = {}) {
|
|
699
787
|
const now = opts.now ?? Date.now;
|
|
788
|
+
const Database = loadDatabase();
|
|
789
|
+
if (Database === void 0) return new NoOpCache();
|
|
700
790
|
try {
|
|
701
|
-
const db = new
|
|
791
|
+
const db = new Database(file);
|
|
702
792
|
try {
|
|
703
793
|
ensureSchema(db);
|
|
704
794
|
} catch (err) {
|
|
705
795
|
db.close();
|
|
706
796
|
throw err;
|
|
707
797
|
}
|
|
708
|
-
|
|
798
|
+
let stale;
|
|
799
|
+
try {
|
|
800
|
+
stale = markPolicy(db, opts.policyVersion ?? CACHE_POLICY_VERSION);
|
|
801
|
+
} catch (err) {
|
|
802
|
+
db.close();
|
|
803
|
+
throw err;
|
|
804
|
+
}
|
|
805
|
+
return new SqliteCache(db, now, stale);
|
|
709
806
|
} catch {
|
|
710
807
|
return new NoOpCache();
|
|
711
808
|
}
|
|
@@ -720,17 +817,33 @@ function ensureSchema(db) {
|
|
|
720
817
|
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("schema_version", current);
|
|
721
818
|
}
|
|
722
819
|
}
|
|
820
|
+
function markPolicy(db, version) {
|
|
821
|
+
const current = String(version);
|
|
822
|
+
const stored = db.prepare("SELECT value FROM meta WHERE key = 'policy_version'").get()?.value;
|
|
823
|
+
if (stored === current) return 0;
|
|
824
|
+
return db.transaction(() => {
|
|
825
|
+
const { n } = db.prepare("SELECT COUNT(*) AS n FROM completions").get();
|
|
826
|
+
db.exec("DELETE FROM completions");
|
|
827
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("policy_version", current);
|
|
828
|
+
return n;
|
|
829
|
+
})();
|
|
830
|
+
}
|
|
723
831
|
var SqliteCache = class {
|
|
724
|
-
constructor(db, now) {
|
|
832
|
+
constructor(db, now, staleEntries) {
|
|
725
833
|
this.db = db;
|
|
726
834
|
this.now = now;
|
|
835
|
+
this.staleEntries = staleEntries;
|
|
727
836
|
}
|
|
728
837
|
db;
|
|
729
838
|
now;
|
|
839
|
+
staleEntries;
|
|
730
840
|
get(key, opts = {}) {
|
|
731
841
|
try {
|
|
732
842
|
const row = this.db.prepare("SELECT output, input_tokens, output_tokens, created_at FROM completions WHERE cache_key = ?").get(key);
|
|
733
843
|
if (row === void 0) return void 0;
|
|
844
|
+
if (typeof row.output !== "string" || !Number.isFinite(row.input_tokens) || !Number.isFinite(row.output_tokens) || !Number.isFinite(row.created_at)) {
|
|
845
|
+
return void 0;
|
|
846
|
+
}
|
|
734
847
|
if (opts.maxAgeMs !== void 0 && this.now() - row.created_at > opts.maxAgeMs) {
|
|
735
848
|
return void 0;
|
|
736
849
|
}
|
|
@@ -756,6 +869,12 @@ var SqliteCache = class {
|
|
|
756
869
|
} catch {
|
|
757
870
|
}
|
|
758
871
|
}
|
|
872
|
+
delete(key) {
|
|
873
|
+
try {
|
|
874
|
+
this.db.prepare("DELETE FROM completions WHERE cache_key = ?").run(key);
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
877
|
+
}
|
|
759
878
|
clear() {
|
|
760
879
|
try {
|
|
761
880
|
this.db.exec("DELETE FROM completions");
|
|
@@ -772,6 +891,8 @@ var NoOpCache = class {
|
|
|
772
891
|
}
|
|
773
892
|
set() {
|
|
774
893
|
}
|
|
894
|
+
delete() {
|
|
895
|
+
}
|
|
775
896
|
clear() {
|
|
776
897
|
}
|
|
777
898
|
close() {
|
|
@@ -782,60 +903,72 @@ var NoOpCache = class {
|
|
|
782
903
|
var fs3 = __toESM(require("fs"), 1);
|
|
783
904
|
var path2 = __toESM(require("path"), 1);
|
|
784
905
|
|
|
785
|
-
// src/
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
function normalizeValue(value) {
|
|
790
|
-
if (value === null) return null;
|
|
791
|
-
if (typeof value === "boolean") return value;
|
|
792
|
-
if (typeof value === "number") {
|
|
793
|
-
if (!isFinite(value)) {
|
|
794
|
-
throw new Error(
|
|
795
|
-
`canonicalJson: non-finite number rejected: ${String(value)}`
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
return value === 0 ? 0 : value;
|
|
799
|
-
}
|
|
800
|
-
if (typeof value === "string") {
|
|
801
|
-
return value.normalize("NFC");
|
|
906
|
+
// src/assertions/json-extract.ts
|
|
907
|
+
function extractJson(output, mode) {
|
|
908
|
+
if (mode === "strict") {
|
|
909
|
+
return tryParse(output.trim());
|
|
802
910
|
}
|
|
803
|
-
|
|
804
|
-
|
|
911
|
+
const fenced = extractFencedBlock(output);
|
|
912
|
+
if (fenced !== null) {
|
|
913
|
+
const r = tryParse(fenced);
|
|
914
|
+
if (r.ok) return r;
|
|
805
915
|
}
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
if (
|
|
809
|
-
const name = value.constructor?.name ?? "unknown";
|
|
810
|
-
throw new Error(
|
|
811
|
-
`canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
|
|
812
|
-
);
|
|
813
|
-
}
|
|
814
|
-
const obj = value;
|
|
815
|
-
const sorted = {};
|
|
816
|
-
for (const key of Object.keys(obj).sort()) {
|
|
817
|
-
sorted[key] = normalizeValue(obj[key]);
|
|
818
|
-
}
|
|
819
|
-
return sorted;
|
|
916
|
+
for (const run of balancedRuns(output)) {
|
|
917
|
+
const r = tryParse(run);
|
|
918
|
+
if (r.ok) return r;
|
|
820
919
|
}
|
|
821
|
-
|
|
822
|
-
`canonicalJson: unsupported value type: ${typeof value}`
|
|
823
|
-
);
|
|
920
|
+
return { ok: false };
|
|
824
921
|
}
|
|
825
|
-
function
|
|
826
|
-
|
|
922
|
+
function tryParse(s) {
|
|
923
|
+
try {
|
|
924
|
+
return { ok: true, value: JSON.parse(s) };
|
|
925
|
+
} catch {
|
|
926
|
+
return { ok: false };
|
|
927
|
+
}
|
|
827
928
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return (0, import_node_crypto.createHash)("sha256").update(input, "utf8").digest("hex");
|
|
929
|
+
function extractFencedBlock(s) {
|
|
930
|
+
const m = s.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
|
|
931
|
+
return m?.[1]?.trim() ?? null;
|
|
832
932
|
}
|
|
833
|
-
function
|
|
834
|
-
|
|
835
|
-
|
|
933
|
+
function* balancedRuns(s) {
|
|
934
|
+
let i = 0;
|
|
935
|
+
while (i < s.length) {
|
|
936
|
+
const rel = s.slice(i).search(/[{[]/);
|
|
937
|
+
if (rel === -1) return;
|
|
938
|
+
const start = i + rel;
|
|
939
|
+
const run = balancedFrom(s, start);
|
|
940
|
+
if (run === null) {
|
|
941
|
+
i = start + 1;
|
|
942
|
+
} else {
|
|
943
|
+
yield run;
|
|
944
|
+
i = start + run.length;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
836
947
|
}
|
|
837
|
-
function
|
|
838
|
-
|
|
948
|
+
function balancedFrom(s, start) {
|
|
949
|
+
const open = s[start];
|
|
950
|
+
const close = open === "{" ? "}" : "]";
|
|
951
|
+
let depth = 0;
|
|
952
|
+
let inString = false;
|
|
953
|
+
let escaped = false;
|
|
954
|
+
for (let i = start; i < s.length; i++) {
|
|
955
|
+
const ch = s[i];
|
|
956
|
+
if (inString) {
|
|
957
|
+
if (escaped) escaped = false;
|
|
958
|
+
else if (ch === "\\") escaped = true;
|
|
959
|
+
else if (ch === '"') inString = false;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (ch === '"') {
|
|
963
|
+
inString = true;
|
|
964
|
+
} else if (ch === open) {
|
|
965
|
+
depth++;
|
|
966
|
+
} else if (ch === close) {
|
|
967
|
+
depth--;
|
|
968
|
+
if (depth === 0) return s.slice(start, i + 1);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return null;
|
|
839
972
|
}
|
|
840
973
|
|
|
841
974
|
// src/assertions/interpolate.ts
|
|
@@ -911,76 +1044,6 @@ var containsAll = {
|
|
|
911
1044
|
|
|
912
1045
|
// src/assertions/json-schema.ts
|
|
913
1046
|
var import_ajv = require("ajv");
|
|
914
|
-
|
|
915
|
-
// src/assertions/json-extract.ts
|
|
916
|
-
function extractJson(output, mode) {
|
|
917
|
-
if (mode === "strict") {
|
|
918
|
-
return tryParse(output.trim());
|
|
919
|
-
}
|
|
920
|
-
const fenced = extractFencedBlock(output);
|
|
921
|
-
if (fenced !== null) {
|
|
922
|
-
const r = tryParse(fenced);
|
|
923
|
-
if (r.ok) return r;
|
|
924
|
-
}
|
|
925
|
-
for (const run of balancedRuns(output)) {
|
|
926
|
-
const r = tryParse(run);
|
|
927
|
-
if (r.ok) return r;
|
|
928
|
-
}
|
|
929
|
-
return { ok: false };
|
|
930
|
-
}
|
|
931
|
-
function tryParse(s) {
|
|
932
|
-
try {
|
|
933
|
-
return { ok: true, value: JSON.parse(s) };
|
|
934
|
-
} catch {
|
|
935
|
-
return { ok: false };
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
function extractFencedBlock(s) {
|
|
939
|
-
const m = s.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
|
|
940
|
-
return m?.[1]?.trim() ?? null;
|
|
941
|
-
}
|
|
942
|
-
function* balancedRuns(s) {
|
|
943
|
-
let i = 0;
|
|
944
|
-
while (i < s.length) {
|
|
945
|
-
const rel = s.slice(i).search(/[{[]/);
|
|
946
|
-
if (rel === -1) return;
|
|
947
|
-
const start = i + rel;
|
|
948
|
-
const run = balancedFrom(s, start);
|
|
949
|
-
if (run === null) {
|
|
950
|
-
i = start + 1;
|
|
951
|
-
} else {
|
|
952
|
-
yield run;
|
|
953
|
-
i = start + run.length;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
function balancedFrom(s, start) {
|
|
958
|
-
const open = s[start];
|
|
959
|
-
const close = open === "{" ? "}" : "]";
|
|
960
|
-
let depth = 0;
|
|
961
|
-
let inString = false;
|
|
962
|
-
let escaped = false;
|
|
963
|
-
for (let i = start; i < s.length; i++) {
|
|
964
|
-
const ch = s[i];
|
|
965
|
-
if (inString) {
|
|
966
|
-
if (escaped) escaped = false;
|
|
967
|
-
else if (ch === "\\") escaped = true;
|
|
968
|
-
else if (ch === '"') inString = false;
|
|
969
|
-
continue;
|
|
970
|
-
}
|
|
971
|
-
if (ch === '"') {
|
|
972
|
-
inString = true;
|
|
973
|
-
} else if (ch === open) {
|
|
974
|
-
depth++;
|
|
975
|
-
} else if (ch === close) {
|
|
976
|
-
depth--;
|
|
977
|
-
if (depth === 0) return s.slice(start, i + 1);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
return null;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// src/assertions/json-schema.ts
|
|
984
1047
|
var jsonSchema = {
|
|
985
1048
|
run(ctx) {
|
|
986
1049
|
const mode = ctx.params.extract ?? "auto";
|
|
@@ -1340,6 +1403,12 @@ var RunConfigError = class extends Error {
|
|
|
1340
1403
|
this.name = "RunConfigError";
|
|
1341
1404
|
}
|
|
1342
1405
|
};
|
|
1406
|
+
var CACHE_MISS_CODE = "CACHE_MISS";
|
|
1407
|
+
function answerParses(output, assertions) {
|
|
1408
|
+
return assertions.every(
|
|
1409
|
+
(a) => a.type !== "json-schema" || extractJson(output, a.extract ?? "auto").ok
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1343
1412
|
function resolvePrompt(template, vars) {
|
|
1344
1413
|
return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => {
|
|
1345
1414
|
if (!(key in vars)) {
|
|
@@ -1367,17 +1436,35 @@ async function runRow(p) {
|
|
|
1367
1436
|
const usage2 = { ...est, cost_usd: p.provider.estimateCost(est).cost_usd };
|
|
1368
1437
|
return { vars: p.row.vars, output: null, cached: false, usage: usage2, assertions: [] };
|
|
1369
1438
|
}
|
|
1370
|
-
const key = cacheKey(
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1439
|
+
const key = cacheKey(
|
|
1440
|
+
{
|
|
1441
|
+
provider: p.providerConfig.type,
|
|
1442
|
+
model: p.providerConfig.model,
|
|
1443
|
+
temperature: temperature ?? 0,
|
|
1444
|
+
max_tokens: maxTokens,
|
|
1445
|
+
prompt_resolved: prompt5
|
|
1446
|
+
},
|
|
1447
|
+
p.policyVersion
|
|
1448
|
+
);
|
|
1377
1449
|
let output;
|
|
1378
1450
|
let usage;
|
|
1379
1451
|
let cached = false;
|
|
1380
|
-
|
|
1452
|
+
let cacheSaved;
|
|
1453
|
+
let hit = p.noCache ? void 0 : p.cache.get(key, p.cacheMaxAgeMs !== void 0 ? { maxAgeMs: p.cacheMaxAgeMs } : {});
|
|
1454
|
+
if (hit !== void 0 && !answerParses(hit.output, p.assertions)) {
|
|
1455
|
+
if (p.cacheStrict !== true) p.cache.delete(key);
|
|
1456
|
+
hit = void 0;
|
|
1457
|
+
}
|
|
1458
|
+
if (hit === void 0 && p.cacheStrict === true) {
|
|
1459
|
+
return {
|
|
1460
|
+
vars: p.row.vars,
|
|
1461
|
+
output: null,
|
|
1462
|
+
cached: false,
|
|
1463
|
+
latency_ms: p.now() - t0,
|
|
1464
|
+
error: { code: CACHE_MISS_CODE, message: "no usable cache entry (--cache-strict)" },
|
|
1465
|
+
assertions: []
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1381
1468
|
if (hit !== void 0) {
|
|
1382
1469
|
output = hit.output;
|
|
1383
1470
|
cached = true;
|
|
@@ -1386,6 +1473,11 @@ async function runRow(p) {
|
|
|
1386
1473
|
output_tokens: hit.usage.output_tokens,
|
|
1387
1474
|
cost_usd: 0
|
|
1388
1475
|
};
|
|
1476
|
+
cacheSaved = {
|
|
1477
|
+
input_tokens: hit.usage.input_tokens,
|
|
1478
|
+
output_tokens: hit.usage.output_tokens,
|
|
1479
|
+
cost_usd: p.provider.estimateCost(hit.usage).cost_usd
|
|
1480
|
+
};
|
|
1389
1481
|
} else {
|
|
1390
1482
|
try {
|
|
1391
1483
|
const res = await p.provider.complete({
|
|
@@ -1403,7 +1495,9 @@ async function runRow(p) {
|
|
|
1403
1495
|
// lets a config pricing override win, else falls back to the table estimate (ADR-PRC01).
|
|
1404
1496
|
cost_usd: p.provider.estimateCost(res.usage, res.cost_usd).cost_usd
|
|
1405
1497
|
};
|
|
1406
|
-
if (!p.noCache)
|
|
1498
|
+
if (!p.noCache && isCacheable(res) && answerParses(res.output, p.assertions)) {
|
|
1499
|
+
p.cache.set(key, res);
|
|
1500
|
+
}
|
|
1407
1501
|
} catch (err) {
|
|
1408
1502
|
return {
|
|
1409
1503
|
vars: p.row.vars,
|
|
@@ -1449,6 +1543,7 @@ async function runRow(p) {
|
|
|
1449
1543
|
cached,
|
|
1450
1544
|
usage: finalUsage,
|
|
1451
1545
|
latency_ms: p.now() - t0,
|
|
1546
|
+
...cacheSaved !== void 0 ? { cache_saved: cacheSaved } : {},
|
|
1452
1547
|
...assertionError !== void 0 ? { error: assertionError } : {},
|
|
1453
1548
|
assertions: records
|
|
1454
1549
|
};
|
|
@@ -1483,19 +1578,41 @@ function summarize(evals, durationMs) {
|
|
|
1483
1578
|
cost_usd: 0,
|
|
1484
1579
|
duration_ms: durationMs
|
|
1485
1580
|
};
|
|
1581
|
+
const cache = {
|
|
1582
|
+
from_cache: 0,
|
|
1583
|
+
from_model: 0,
|
|
1584
|
+
missed: 0,
|
|
1585
|
+
errored: 0,
|
|
1586
|
+
saved: { input_tokens: 0, output_tokens: 0, cost_usd: 0 }
|
|
1587
|
+
};
|
|
1486
1588
|
for (const ev of evals) {
|
|
1487
1589
|
for (const row of ev.rows) {
|
|
1488
1590
|
summary.total += 1;
|
|
1489
1591
|
summary[rowStatus(row)] += 1;
|
|
1490
1592
|
summary.cost_usd += row.usage?.cost_usd ?? 0;
|
|
1593
|
+
if (row.cached) {
|
|
1594
|
+
cache.from_cache += 1;
|
|
1595
|
+
cache.saved.input_tokens += row.cache_saved?.input_tokens ?? 0;
|
|
1596
|
+
cache.saved.output_tokens += row.cache_saved?.output_tokens ?? 0;
|
|
1597
|
+
cache.saved.cost_usd += row.cache_saved?.cost_usd ?? 0;
|
|
1598
|
+
} else if (row.error?.code === CACHE_MISS_CODE) cache.missed += 1;
|
|
1599
|
+
else if (row.output !== null) cache.from_model += 1;
|
|
1600
|
+
else if (row.error !== void 0) cache.errored += 1;
|
|
1491
1601
|
}
|
|
1492
1602
|
}
|
|
1493
|
-
return summary;
|
|
1603
|
+
return { ...summary, cache };
|
|
1494
1604
|
}
|
|
1495
1605
|
async function runOrchestration(config, options, deps) {
|
|
1606
|
+
const noCache = options.noCache ?? false;
|
|
1607
|
+
const cacheStrict = options.cacheStrict ?? false;
|
|
1608
|
+
if (noCache && cacheStrict) {
|
|
1609
|
+
throw new RunConfigError("--no-cache and --cache-strict cannot be used together");
|
|
1610
|
+
}
|
|
1611
|
+
if (noCache && options.cacheMaxAgeMs !== void 0) {
|
|
1612
|
+
throw new RunConfigError("--no-cache and --cache-max-age cannot be used together");
|
|
1613
|
+
}
|
|
1496
1614
|
const started = deps.now();
|
|
1497
1615
|
const dryRun = options.dryRun ?? false;
|
|
1498
|
-
const noCache = options.noCache ?? false;
|
|
1499
1616
|
const evalResults = [];
|
|
1500
1617
|
const prepared = selectEvals(config.evals, options.only).map((ev) => {
|
|
1501
1618
|
const template = resolveTemplate(ev, deps.baseDir);
|
|
@@ -1522,7 +1639,9 @@ async function runOrchestration(config, options, deps) {
|
|
|
1522
1639
|
cache: deps.cache,
|
|
1523
1640
|
now: deps.now,
|
|
1524
1641
|
dryRun,
|
|
1525
|
-
noCache
|
|
1642
|
+
noCache,
|
|
1643
|
+
cacheStrict,
|
|
1644
|
+
...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
|
|
1526
1645
|
})
|
|
1527
1646
|
);
|
|
1528
1647
|
const passed = rowResults.every((r) => rowStatus(r) === "passed");
|
|
@@ -1555,6 +1674,7 @@ function loadDataset(ref, baseDir) {
|
|
|
1555
1674
|
|
|
1556
1675
|
// src/cli/commands/run.ts
|
|
1557
1676
|
var NOOP_CACHE = { get: () => void 0, set: () => {
|
|
1677
|
+
}, delete: () => {
|
|
1558
1678
|
}, clear: () => {
|
|
1559
1679
|
}, close: () => {
|
|
1560
1680
|
} };
|
|
@@ -1591,6 +1711,11 @@ async function handleRun(options, depsFactory) {
|
|
|
1591
1711
|
const factory = depsFactory ?? ((c, d) => buildRealDeps(c, d, options.dryRun));
|
|
1592
1712
|
const deps = factory(config, baseDir);
|
|
1593
1713
|
try {
|
|
1714
|
+
const stale = deps.cache.staleEntries ?? 0;
|
|
1715
|
+
if (stale > 0) {
|
|
1716
|
+
process.stderr.write(`cache policy changed, ${stale} entries are stale (dropped)
|
|
1717
|
+
`);
|
|
1718
|
+
}
|
|
1594
1719
|
const result = await runOrchestration(
|
|
1595
1720
|
config,
|
|
1596
1721
|
{
|
|
@@ -1598,7 +1723,9 @@ async function handleRun(options, depsFactory) {
|
|
|
1598
1723
|
...options.only !== void 0 ? { only: options.only } : {},
|
|
1599
1724
|
...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
|
|
1600
1725
|
...options.noCache !== void 0 ? { noCache: options.noCache } : {},
|
|
1601
|
-
...options.bail !== void 0 ? { bail: options.bail } : {}
|
|
1726
|
+
...options.bail !== void 0 ? { bail: options.bail } : {},
|
|
1727
|
+
...options.cacheStrict === true ? { cacheStrict: true } : {},
|
|
1728
|
+
...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
|
|
1602
1729
|
},
|
|
1603
1730
|
deps
|
|
1604
1731
|
);
|
|
@@ -1645,6 +1772,7 @@ var rowResultSchema = import_zod3.z.object({
|
|
|
1645
1772
|
latency_ms: import_zod3.z.number().optional(),
|
|
1646
1773
|
error: runErrorSchema.optional(),
|
|
1647
1774
|
assertions: import_zod3.z.array(assertionResultRecordSchema2),
|
|
1775
|
+
cache_saved: import_zod3.z.object({ input_tokens: import_zod3.z.number(), output_tokens: import_zod3.z.number(), cost_usd: import_zod3.z.number() }).optional(),
|
|
1648
1776
|
/** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
|
|
1649
1777
|
inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
|
|
1650
1778
|
});
|
|
@@ -1657,7 +1785,14 @@ var summarySchema = import_zod3.z.object({
|
|
|
1657
1785
|
blocked: import_zod3.z.number().optional(),
|
|
1658
1786
|
skipped: import_zod3.z.number().optional(),
|
|
1659
1787
|
cost_usd: import_zod3.z.number(),
|
|
1660
|
-
duration_ms: import_zod3.z.number()
|
|
1788
|
+
duration_ms: import_zod3.z.number(),
|
|
1789
|
+
cache: import_zod3.z.object({
|
|
1790
|
+
from_cache: import_zod3.z.number(),
|
|
1791
|
+
from_model: import_zod3.z.number(),
|
|
1792
|
+
missed: import_zod3.z.number(),
|
|
1793
|
+
errored: import_zod3.z.number(),
|
|
1794
|
+
saved: import_zod3.z.object({ input_tokens: import_zod3.z.number(), output_tokens: import_zod3.z.number(), cost_usd: import_zod3.z.number() })
|
|
1795
|
+
}).optional()
|
|
1661
1796
|
});
|
|
1662
1797
|
var evalResultSchema = import_zod3.z.object({
|
|
1663
1798
|
id: import_zod3.z.string(),
|
package/dist/index.d.cts
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";
|