@tangle-network/agent-eval 0.143.0 → 0.144.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/CHANGELOG.md +34 -0
- package/README.md +21 -8
- package/dist/analyst/index.d.ts +82 -17
- package/dist/analyst/index.d.ts.map +1 -1
- package/dist/analyst/index.js +4 -4
- package/dist/{benchmark-command-CSddvlq7.js → benchmark-command-95Zqd1e8.js} +333 -111
- package/dist/benchmark-command-95Zqd1e8.js.map +1 -0
- package/dist/benchmarks/index.d.ts +1 -1
- package/dist/benchmarks/index.js +1 -1
- package/dist/{benchmarks-Dg0B56tK.js → benchmarks-CRT4ScEA.js} +4 -4
- package/dist/{benchmarks-Dg0B56tK.js.map → benchmarks-CRT4ScEA.js.map} +1 -1
- package/dist/campaign/index.d.ts +4 -3
- package/dist/campaign/index.js +4 -4
- package/dist/{campaign-C7ZGiZYG.js → campaign-CefP3fPS.js} +12 -10
- package/dist/campaign-CefP3fPS.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.js +3 -3
- package/dist/{default-registry-BupFSXSN.js → default-registry-SOyHB6qG.js} +3 -3
- package/dist/{default-registry-BupFSXSN.js.map → default-registry-SOyHB6qG.js.map} +1 -1
- package/dist/{dspy-rlm-engine-CJ4sN8Wx.js → dspy-rlm-engine-BR7TtB0o.js} +69 -31
- package/dist/dspy-rlm-engine-BR7TtB0o.js.map +1 -0
- package/dist/{eval-campaign-YdkpWWoT.js → eval-campaign-lI7wB9DH.js} +2 -2
- package/dist/{eval-campaign-YdkpWWoT.js.map → eval-campaign-lI7wB9DH.js.map} +1 -1
- package/dist/external-optimizer-contracts-CdmX2K2S.d.ts +165 -0
- package/dist/external-optimizer-contracts-CdmX2K2S.d.ts.map +1 -0
- package/dist/{index-BKjcUH13.d.ts → index-BZUe-ODI.d.ts} +41 -5
- package/dist/index-BZUe-ODI.d.ts.map +1 -0
- package/dist/{index-DY83M1WL.d.ts → index-BrpxwELi.d.ts} +9 -8
- package/dist/{index-DY83M1WL.d.ts.map → index-BrpxwELi.d.ts.map} +1 -1
- package/dist/{index-CaOjqCli.d.ts → index-DRI1DU5z.d.ts} +3 -3
- package/dist/{index-CaOjqCli.d.ts.map → index-DRI1DU5z.d.ts.map} +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +12 -11
- package/dist/index.js.map +1 -1
- package/dist/{integrity-hruKe6oe.js → integrity-DY6tIbl0.js} +48 -18
- package/dist/integrity-DY6tIbl0.js.map +1 -0
- package/dist/{llm-client-B3WXSH5Y.js → llm-client-D3EoChAU.js} +7 -7
- package/dist/llm-client-D3EoChAU.js.map +1 -0
- package/dist/openapi.json +1 -1
- package/dist/rl.js +1 -1
- package/dist/{semantic-concept-judge-DJQtFr95.js → semantic-concept-judge-l8Lb3Kqz.js} +2 -2
- package/dist/{semantic-concept-judge-DJQtFr95.js.map → semantic-concept-judge-l8Lb3Kqz.js.map} +1 -1
- package/dist/{server-Cu4M3NSO.js → server-Df00sdwz.js} +3 -3
- package/dist/{server-Cu4M3NSO.js.map → server-Df00sdwz.js.map} +1 -1
- package/dist/single-run-lock-B-cFnnNn.js +1799 -0
- package/dist/single-run-lock-B-cFnnNn.js.map +1 -0
- package/dist/{skill-usage-CRqaHi0R.d.ts → skill-usage-3vUWdZ9n.d.ts} +24 -5
- package/dist/skill-usage-3vUWdZ9n.d.ts.map +1 -0
- package/dist/{skillopt-optimization-method-Di8Xkalt.js → skillopt-optimization-method-DA5mJmFE.js} +225 -347
- package/dist/skillopt-optimization-method-DA5mJmFE.js.map +1 -0
- package/dist/{skillopt-optimization-method-CwRvi-rL.d.ts → skillopt-optimization-method-DNniakI5.d.ts} +47 -9
- package/dist/skillopt-optimization-method-DNniakI5.d.ts.map +1 -0
- package/dist/supervisor-run/index.d.ts +3 -3
- package/dist/supervisor-run/index.js +3 -3
- package/dist/{supervisor-run-BEns49Nx.js → supervisor-run-DiyQVczd.js} +301 -25
- package/dist/supervisor-run-DiyQVczd.js.map +1 -0
- package/dist/types-BjMFz88h.d.ts.map +1 -1
- package/dist/{types-Dea6tiVI.d.ts → types-yLK8gXE9.d.ts} +15 -2
- package/dist/types-yLK8gXE9.d.ts.map +1 -0
- package/dist/wire/index.js +1 -1
- package/docs/campaign-proposers.md +11 -7
- package/docs/trace-analysis.md +43 -32
- package/package.json +1 -1
- package/dist/benchmark-command-CSddvlq7.js.map +0 -1
- package/dist/campaign-C7ZGiZYG.js.map +0 -1
- package/dist/dspy-rlm-engine-CJ4sN8Wx.js.map +0 -1
- package/dist/external-optimizer-contracts-nb7c_WAR.d.ts +0 -42
- package/dist/external-optimizer-contracts-nb7c_WAR.d.ts.map +0 -1
- package/dist/index-BKjcUH13.d.ts.map +0 -1
- package/dist/integrity-hruKe6oe.js.map +0 -1
- package/dist/llm-client-B3WXSH5Y.js.map +0 -1
- package/dist/single-run-lock-Do-8vBIJ.js +0 -1049
- package/dist/single-run-lock-Do-8vBIJ.js.map +0 -1
- package/dist/skill-usage-CRqaHi0R.d.ts.map +0 -1
- package/dist/skillopt-optimization-method-CwRvi-rL.d.ts.map +0 -1
- package/dist/skillopt-optimization-method-Di8Xkalt.js.map +0 -1
- package/dist/supervisor-run-BEns49Nx.js.map +0 -1
- package/dist/types-Dea6tiVI.d.ts.map +0 -1
|
@@ -0,0 +1,1799 @@
|
|
|
1
|
+
import { c as ValidationError } from "./errors-D-LKuDhb.js";
|
|
2
|
+
import { c as costForTokenPricing, i as CostLedger } from "./cost-ledger-DMFxsLKr.js";
|
|
3
|
+
import { a as appendLedgerLine, d as tryAcquireAtomicFileLock, o as tryWithLedgerFileLock, u as probeAtomicFileLock } from "./ledger-core-Dxz0Rkwa.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { constants as constants$1 } from "node:buffer";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { lstat, mkdtemp, open, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
//#region src/campaign/external-optimizer-contracts.ts
|
|
14
|
+
const MAX_TIMER_DELAY_MS$1 = 2147483647;
|
|
15
|
+
const MAXIMUM_JSON_BYTES = Math.min(constants$1.MAX_LENGTH - 1, constants$1.MAX_STRING_LENGTH);
|
|
16
|
+
const DEFAULT_EXTERNAL_OPTIMIZER_PROCESS_LIMITS = Object.freeze({
|
|
17
|
+
maxInputBytes: 64 * 1024 * 1024,
|
|
18
|
+
maxResultBytes: 4 * 1024 * 1024,
|
|
19
|
+
maxOutputChars: 64e3
|
|
20
|
+
});
|
|
21
|
+
const DEFAULT_EXTERNAL_OPTIMIZER_CALLBACK_LIMITS = Object.freeze({
|
|
22
|
+
maxRequestBytes: 1e6,
|
|
23
|
+
maxResponseBytes: 4e6
|
|
24
|
+
});
|
|
25
|
+
function resolveExternalOptimizerProcessLimits(value, label = "external optimizer process limits") {
|
|
26
|
+
const resolved = {
|
|
27
|
+
...DEFAULT_EXTERNAL_OPTIMIZER_PROCESS_LIMITS,
|
|
28
|
+
...value
|
|
29
|
+
};
|
|
30
|
+
assertBoundedPositiveInteger(resolved.maxInputBytes, `${label}.maxInputBytes`, MAXIMUM_JSON_BYTES);
|
|
31
|
+
assertBoundedPositiveInteger(resolved.maxResultBytes, `${label}.maxResultBytes`, MAXIMUM_JSON_BYTES);
|
|
32
|
+
assertBoundedPositiveInteger(resolved.maxOutputChars, `${label}.maxOutputChars`, constants$1.MAX_STRING_LENGTH);
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
function resolveExternalOptimizerCallbackLimits(value, label = "external optimizer callback limits") {
|
|
36
|
+
const resolved = {
|
|
37
|
+
...DEFAULT_EXTERNAL_OPTIMIZER_CALLBACK_LIMITS,
|
|
38
|
+
...value
|
|
39
|
+
};
|
|
40
|
+
assertBoundedPositiveInteger(resolved.maxRequestBytes, `${label}.maxRequestBytes`, MAXIMUM_JSON_BYTES);
|
|
41
|
+
assertBoundedPositiveInteger(resolved.maxResponseBytes, `${label}.maxResponseBytes`, MAXIMUM_JSON_BYTES);
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
function isCandidateText(value, maxChars) {
|
|
45
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= maxChars;
|
|
46
|
+
}
|
|
47
|
+
function isExternalTextCandidate(value) {
|
|
48
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
49
|
+
if (!isRecord(value)) return false;
|
|
50
|
+
const entries = Object.entries(value);
|
|
51
|
+
return entries.length > 0 && entries.every(([name, content]) => name.trim().length > 0 && name.trim() === name && typeof content === "string");
|
|
52
|
+
}
|
|
53
|
+
function assertJsonValue(value, label, seen = /* @__PURE__ */ new Set()) {
|
|
54
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
55
|
+
if (typeof value === "number" && Number.isFinite(value)) return;
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
if (seen.has(value)) throw new Error(`${label} must be JSON-serializable`);
|
|
58
|
+
seen.add(value);
|
|
59
|
+
for (const item of value) assertJsonValue(item, label, seen);
|
|
60
|
+
seen.delete(value);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === "object") {
|
|
64
|
+
if (seen.has(value)) throw new Error(`${label} must be JSON-serializable`);
|
|
65
|
+
const prototype = Object.getPrototypeOf(value);
|
|
66
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error(`${label} must be JSON-serializable`);
|
|
67
|
+
seen.add(value);
|
|
68
|
+
for (const item of Object.values(value)) assertJsonValue(item, label, seen);
|
|
69
|
+
seen.delete(value);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`${label} must be JSON-serializable`);
|
|
73
|
+
}
|
|
74
|
+
function assertNoCredentialValues(value, path, credentialLocation = "runner.env") {
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
for (const [index, item] of value.entries()) assertNoCredentialValues(item, `${path}[${index}]`, credentialLocation);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!isRecord(value)) return;
|
|
80
|
+
for (const [key, item] of Object.entries(value)) {
|
|
81
|
+
if (isCredentialKey(key)) throw new Error(`${path}.${key} must be supplied through ${credentialLocation}`);
|
|
82
|
+
assertNoCredentialValues(item, `${path}.${key}`, credentialLocation);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function removeCredentialEnvironment(environment) {
|
|
86
|
+
const sanitized = {};
|
|
87
|
+
for (const [key, value] of Object.entries(environment)) if (value !== void 0 && !isCredentialKey(key)) sanitized[key] = value;
|
|
88
|
+
return sanitized;
|
|
89
|
+
}
|
|
90
|
+
function assertExternalOptimizerModelBudget(value, label) {
|
|
91
|
+
if (!value || typeof value !== "object") throw new Error(`${label} is required`);
|
|
92
|
+
for (const [field, entry] of [["maxRequests", value.maxRequests], ["maxOutputTokensPerRequest", value.maxOutputTokensPerRequest]]) if (!Number.isSafeInteger(entry) || entry <= 0) throw new Error(`${label}.${field} must be a positive safe integer`);
|
|
93
|
+
assertBoundedPositiveInteger(value.maxRequestBytes, `${label}.maxRequestBytes`, MAXIMUM_JSON_BYTES);
|
|
94
|
+
assertBoundedPositiveInteger(value.maxResponseBytes, `${label}.maxResponseBytes`, MAXIMUM_JSON_BYTES);
|
|
95
|
+
if (value.maxReasoningTokensPerRequest !== void 0 && (!Number.isSafeInteger(value.maxReasoningTokensPerRequest) || value.maxReasoningTokensPerRequest < 0)) throw new Error(`${label}.maxReasoningTokensPerRequest must be a non-negative safe integer`);
|
|
96
|
+
if (value.maxCostUsd !== void 0 && (!Number.isFinite(value.maxCostUsd) || value.maxCostUsd <= 0)) throw new Error(`${label}.maxCostUsd must be positive and finite when supplied`);
|
|
97
|
+
if (value.requestTimeoutMs !== void 0 && (!Number.isSafeInteger(value.requestTimeoutMs) || value.requestTimeoutMs <= 0 || value.requestTimeoutMs > MAX_TIMER_DELAY_MS$1)) throw new Error(`${label}.requestTimeoutMs must be between 1 and ${MAX_TIMER_DELAY_MS$1}`);
|
|
98
|
+
if (value.pricing !== void 0) costForTokenPricing(value.pricing, {
|
|
99
|
+
inputTokens: 1,
|
|
100
|
+
outputTokens: 1
|
|
101
|
+
});
|
|
102
|
+
if (value.maxCostUsd !== void 0 && value.pricing === void 0) throw new Error(`${label}.pricing is required when maxCostUsd is supplied`);
|
|
103
|
+
}
|
|
104
|
+
function safePathComponent(value) {
|
|
105
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
106
|
+
}
|
|
107
|
+
function assertBoundedPositiveInteger(value, label, maximum) {
|
|
108
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) throw new Error(`${label} must be an integer between 1 and ${maximum}`);
|
|
109
|
+
}
|
|
110
|
+
function isCredentialKey(key) {
|
|
111
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/-/g, "_").toLowerCase().split("_").some((segment) => [
|
|
112
|
+
"auth",
|
|
113
|
+
"authorization",
|
|
114
|
+
"cookie",
|
|
115
|
+
"credential",
|
|
116
|
+
"credentials",
|
|
117
|
+
"key",
|
|
118
|
+
"password",
|
|
119
|
+
"secret",
|
|
120
|
+
"session",
|
|
121
|
+
"token"
|
|
122
|
+
].includes(segment));
|
|
123
|
+
}
|
|
124
|
+
function isRecord(value) {
|
|
125
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/verdict-cache.ts
|
|
129
|
+
/**
|
|
130
|
+
* Content-addressed judge-verdict caching.
|
|
131
|
+
*
|
|
132
|
+
* LAW: cache JUDGE VERDICTS only — judging the same artifact with the same
|
|
133
|
+
* judge+rubric is pure. NEVER cache agent rollouts. (A router that cached
|
|
134
|
+
* identical fanout prompts silently destroyed best-of-N diversity; rollout
|
|
135
|
+
* caching reintroduces that failure class. Judging has no diversity to
|
|
136
|
+
* destroy — same artifact + same rubric ⇒ same verdict is the desired
|
|
137
|
+
* property, not a bug.)
|
|
138
|
+
*
|
|
139
|
+
* The cache key is a sha-256 over the canonical JSON of everything that can
|
|
140
|
+
* change a verdict: the artifact content, the scenario id, the judge name,
|
|
141
|
+
* the full dimension list (key + description — the description IS the rubric
|
|
142
|
+
* text shown to the judge), and a caller-supplied `judgeVersion`.
|
|
143
|
+
* `judgeVersion` is REQUIRED: a judge whose prompt/model/ensemble changes
|
|
144
|
+
* without a version bump would otherwise silently serve stale verdicts.
|
|
145
|
+
*
|
|
146
|
+
* Strict canonicalization (`canonicalJson`) throws on undefined / function /
|
|
147
|
+
* symbol / non-finite numbers — an artifact that cannot be unambiguously
|
|
148
|
+
* serialized cannot be content-addressed, and coercing it would let two
|
|
149
|
+
* different artifacts collide on one key.
|
|
150
|
+
*/
|
|
151
|
+
function canonicalizeAt(value, path) {
|
|
152
|
+
if (value === null) return "null";
|
|
153
|
+
switch (typeof value) {
|
|
154
|
+
case "boolean": return value ? "true" : "false";
|
|
155
|
+
case "number":
|
|
156
|
+
if (!Number.isFinite(value)) throw new Error(`canonicalJson: non-finite number (${value}) at ${path} — ambiguity is an error, not a coercion`);
|
|
157
|
+
return JSON.stringify(value);
|
|
158
|
+
case "string": return JSON.stringify(value);
|
|
159
|
+
case "undefined":
|
|
160
|
+
case "function":
|
|
161
|
+
case "symbol": throw new Error(`canonicalJson: ${typeof value} at ${path} — ambiguity is an error, not a coercion`);
|
|
162
|
+
case "bigint": throw new Error(`canonicalJson: bigint at ${path} — not representable in JSON`);
|
|
163
|
+
case "object": break;
|
|
164
|
+
}
|
|
165
|
+
const obj = value;
|
|
166
|
+
if (typeof obj.toJSON === "function") return canonicalizeAt(obj.toJSON(), path);
|
|
167
|
+
if (Array.isArray(obj)) return `[${obj.map((item, i) => canonicalizeAt(item, `${path}[${i}]`)).join(",")}]`;
|
|
168
|
+
if (obj instanceof Map || obj instanceof Set) throw new Error(`canonicalJson: ${obj instanceof Map ? "Map" : "Set"} at ${path} — would serialize as '{}'; convert to a plain object/array first`);
|
|
169
|
+
return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${canonicalizeAt(obj[k], `${path}.${k}`)}`).join(",")}}`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Stable JSON stringify: object keys sorted recursively, so two semantically
|
|
173
|
+
* equal values produce byte-identical output regardless of key insertion
|
|
174
|
+
* order. Throws on undefined / function / symbol / NaN / ±Infinity / bigint /
|
|
175
|
+
* Map / Set — anything JSON.stringify would coerce or drop silently.
|
|
176
|
+
*
|
|
177
|
+
* Distinct from `pre-registration.ts`'s `canonicalize`/`hashJson`, which are
|
|
178
|
+
* permissive (coercion allowed) and async (web-crypto). Use THIS pair when a
|
|
179
|
+
* hash collision or silent coercion would corrupt a cache key or attestation.
|
|
180
|
+
*/
|
|
181
|
+
function canonicalJson(value) {
|
|
182
|
+
return canonicalizeAt(value, "$");
|
|
183
|
+
}
|
|
184
|
+
/** Hex sha-256 over `canonicalJson(value)`. The content address used by the
|
|
185
|
+
* verdict cache and report attestation. */
|
|
186
|
+
function contentHash(value) {
|
|
187
|
+
return createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
188
|
+
}
|
|
189
|
+
/** Process-local Map-backed store. */
|
|
190
|
+
function inMemoryVerdictCache() {
|
|
191
|
+
const entries = /* @__PURE__ */ new Map();
|
|
192
|
+
return {
|
|
193
|
+
get: (key) => entries.get(key),
|
|
194
|
+
set: (key, score) => {
|
|
195
|
+
entries.set(key, score);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function parseCacheLine(line, path, lineNo) {
|
|
200
|
+
let parsed;
|
|
201
|
+
try {
|
|
202
|
+
parsed = JSON.parse(line);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
throw new Error(`fileVerdictCache: corrupt JSONL at ${path}:${lineNo} — ${err instanceof Error ? err.message : String(err)}`);
|
|
205
|
+
}
|
|
206
|
+
const rec = parsed;
|
|
207
|
+
if (typeof rec !== "object" || rec === null || typeof rec.key !== "string" || typeof rec.score !== "object" || rec.score === null || typeof rec.score.composite !== "number" || typeof rec.score.dimensions !== "object") throw new Error(`fileVerdictCache: invalid record shape at ${path}:${lineNo} — expected {key, score:{dimensions, composite, notes}}`);
|
|
208
|
+
return rec;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* JSONL-file-backed store: the full file is loaded into an in-memory index at
|
|
212
|
+
* construction; every `set` appends one line synchronously (durable before
|
|
213
|
+
* the verdict is returned). A corrupt or malformed line throws at load with
|
|
214
|
+
* file:line — a skipped line would silently re-judge (cost) or, worse, mask
|
|
215
|
+
* a half-written file that needs operator attention.
|
|
216
|
+
*/
|
|
217
|
+
function fileVerdictCache(path) {
|
|
218
|
+
const entries = /* @__PURE__ */ new Map();
|
|
219
|
+
if (existsSync(path)) {
|
|
220
|
+
const lines = readFileSync(path, "utf8").split("\n");
|
|
221
|
+
for (let i = 0; i < lines.length; i++) {
|
|
222
|
+
const line = lines[i];
|
|
223
|
+
if (line === void 0 || line.trim() === "") continue;
|
|
224
|
+
const rec = parseCacheLine(line, path, i + 1);
|
|
225
|
+
entries.set(rec.key, rec.score);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
get: (key) => entries.get(key),
|
|
230
|
+
set: (key, score) => {
|
|
231
|
+
appendFileSync(path, `${JSON.stringify({
|
|
232
|
+
key,
|
|
233
|
+
score
|
|
234
|
+
})}\n`, "utf8");
|
|
235
|
+
entries.set(key, score);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Wrap a `JudgeConfig` so repeat judgments of the same artifact are served
|
|
241
|
+
* from the store instead of re-invoking `score()`. The wrapper is generic
|
|
242
|
+
* over the judge's own type parameters and preserves `appliesTo` — it is a
|
|
243
|
+
* drop-in replacement anywhere a `JudgeConfig` is accepted.
|
|
244
|
+
*
|
|
245
|
+
* A judge that throws is NOT cached: the error propagates and the next
|
|
246
|
+
* attempt re-judges (caching a failure would pin a transient outage forever).
|
|
247
|
+
*/
|
|
248
|
+
function cachedJudge(judge, store, options) {
|
|
249
|
+
if (typeof options.judgeVersion !== "string" || options.judgeVersion.trim() === "") throw new Error("cachedJudge: judgeVersion is required and must be a non-empty string");
|
|
250
|
+
const stats = {
|
|
251
|
+
hits: 0,
|
|
252
|
+
misses: 0
|
|
253
|
+
};
|
|
254
|
+
const wrapped = {
|
|
255
|
+
name: judge.name,
|
|
256
|
+
dimensions: judge.dimensions,
|
|
257
|
+
judgeVersion: options.judgeVersion,
|
|
258
|
+
async score(input) {
|
|
259
|
+
const key = contentHash({
|
|
260
|
+
artifact: canonicalJson(input.artifact),
|
|
261
|
+
scenarioId: input.scenario.id,
|
|
262
|
+
judgeName: judge.name,
|
|
263
|
+
dimensions: judge.dimensions,
|
|
264
|
+
judgeVersion: options.judgeVersion
|
|
265
|
+
});
|
|
266
|
+
const cached = await store.get(key);
|
|
267
|
+
if (cached !== void 0) {
|
|
268
|
+
stats.hits += 1;
|
|
269
|
+
return cached;
|
|
270
|
+
}
|
|
271
|
+
const score = await judge.score(input);
|
|
272
|
+
await store.set(key, score);
|
|
273
|
+
stats.misses += 1;
|
|
274
|
+
return score;
|
|
275
|
+
},
|
|
276
|
+
stats: () => ({ ...stats })
|
|
277
|
+
};
|
|
278
|
+
if (judge.appliesTo) wrapped.appliesTo = judge.appliesTo;
|
|
279
|
+
return wrapped;
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/campaign/external-optimizer-http.ts
|
|
283
|
+
function listenLocal(server) {
|
|
284
|
+
return new Promise((resolvePromise, reject) => {
|
|
285
|
+
server.once("error", reject);
|
|
286
|
+
server.listen(0, "127.0.0.1", () => {
|
|
287
|
+
server.off("error", reject);
|
|
288
|
+
const address = server.address();
|
|
289
|
+
if (!address || typeof address === "string") {
|
|
290
|
+
reject(/* @__PURE__ */ new Error("external optimizer callback did not bind a TCP port"));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
resolvePromise(address.port);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function closeServer(server) {
|
|
298
|
+
return new Promise((resolvePromise, reject) => {
|
|
299
|
+
server.close((error) => error ? reject(error) : resolvePromise());
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function sendJson(response, status, body) {
|
|
303
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
304
|
+
response.end(JSON.stringify(body));
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/campaign/external-optimizer-model-proxy.ts
|
|
308
|
+
const MODEL_PROXY_PATHS = /* @__PURE__ */ new Set(["/v1/chat/completions", "/v1/responses"]);
|
|
309
|
+
/**
|
|
310
|
+
* Put an OpenAI-compatible optimizer behind the shared cost ledger.
|
|
311
|
+
*
|
|
312
|
+
* The child process receives only a loopback URL and an ephemeral token. The
|
|
313
|
+
* package that owns execution receives a validated immutable request through
|
|
314
|
+
* `call`; Eval receives no provider credential. Every request reserves its
|
|
315
|
+
* conservative byte-count input bound plus the declared output cap before the
|
|
316
|
+
* owner is invoked exactly once.
|
|
317
|
+
*/
|
|
318
|
+
async function startExternalOptimizerModelProxy(args) {
|
|
319
|
+
assertModelProxyConfig(args);
|
|
320
|
+
args.signal?.throwIfAborted();
|
|
321
|
+
const token = randomLocalToken();
|
|
322
|
+
let requestCount = 0;
|
|
323
|
+
let successfulCompletionCount = 0;
|
|
324
|
+
let modelCallInvocations = 0;
|
|
325
|
+
let executionRecordCount = 0;
|
|
326
|
+
let executionSequence = 0;
|
|
327
|
+
const failures = [];
|
|
328
|
+
let totalRequestCount = args.initialUsage?.requests ?? 0;
|
|
329
|
+
let committedForBudget = args.initialUsage?.costUsd ?? 0;
|
|
330
|
+
let reservedForBudget = 0;
|
|
331
|
+
let accepting = true;
|
|
332
|
+
let closePromise;
|
|
333
|
+
const activeControllers = /* @__PURE__ */ new Set();
|
|
334
|
+
const activeHandlers = /* @__PURE__ */ new Set();
|
|
335
|
+
const server = createServer((request, response) => {
|
|
336
|
+
if (!accepting) {
|
|
337
|
+
sendJsonIfOpen$1(response, 503, { error: "external optimizer model proxy is closing" });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const controller = new AbortController();
|
|
341
|
+
const abortRequest = () => {
|
|
342
|
+
request.destroy();
|
|
343
|
+
response.destroy();
|
|
344
|
+
};
|
|
345
|
+
activeControllers.add(controller);
|
|
346
|
+
controller.signal.addEventListener("abort", abortRequest, { once: true });
|
|
347
|
+
let handler;
|
|
348
|
+
handler = handleModelProxyRequest({
|
|
349
|
+
request,
|
|
350
|
+
response,
|
|
351
|
+
controller,
|
|
352
|
+
token,
|
|
353
|
+
args,
|
|
354
|
+
nextReservation: (maximumCostUsd) => {
|
|
355
|
+
if (totalRequestCount >= args.budget.maxRequests) return {
|
|
356
|
+
accepted: false,
|
|
357
|
+
reason: "optimizer model request limit reached"
|
|
358
|
+
};
|
|
359
|
+
if (args.budget.maxCostUsd !== void 0 && maximumCostUsd !== void 0 && committedForBudget + reservedForBudget + maximumCostUsd > args.budget.maxCostUsd + Number.EPSILON) return {
|
|
360
|
+
accepted: false,
|
|
361
|
+
reason: "optimizer model cost limit reached"
|
|
362
|
+
};
|
|
363
|
+
requestCount += 1;
|
|
364
|
+
totalRequestCount += 1;
|
|
365
|
+
reservedForBudget += maximumCostUsd ?? 0;
|
|
366
|
+
return { accepted: true };
|
|
367
|
+
},
|
|
368
|
+
settleReservation: (maximumCostUsd, chargedCostUsd) => {
|
|
369
|
+
reservedForBudget = Math.max(0, reservedForBudget - (maximumCostUsd ?? 0));
|
|
370
|
+
committedForBudget += chargedCostUsd ?? 0;
|
|
371
|
+
},
|
|
372
|
+
recordSuccessfulCompletion: () => {
|
|
373
|
+
successfulCompletionCount += 1;
|
|
374
|
+
},
|
|
375
|
+
recordExecutionReceipt: (observation) => {
|
|
376
|
+
try {
|
|
377
|
+
args.recordExecution({
|
|
378
|
+
...observation,
|
|
379
|
+
sequence: executionSequence + 1
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
throw new ModelExecutionPersistenceError(`optimizer model execution evidence was not persisted: ${toErrorMessage(error)}`);
|
|
383
|
+
}
|
|
384
|
+
executionSequence += 1;
|
|
385
|
+
executionRecordCount += 1;
|
|
386
|
+
},
|
|
387
|
+
recordModelCallInvocation: () => {
|
|
388
|
+
modelCallInvocations += 1;
|
|
389
|
+
},
|
|
390
|
+
recordFailure: (error) => {
|
|
391
|
+
failures.push(error);
|
|
392
|
+
}
|
|
393
|
+
}).finally(() => {
|
|
394
|
+
controller.signal.removeEventListener("abort", abortRequest);
|
|
395
|
+
activeControllers.delete(controller);
|
|
396
|
+
activeHandlers.delete(handler);
|
|
397
|
+
});
|
|
398
|
+
activeHandlers.add(handler);
|
|
399
|
+
handler.catch(() => void 0);
|
|
400
|
+
});
|
|
401
|
+
const port = await listenLocal(server);
|
|
402
|
+
const close = () => {
|
|
403
|
+
closePromise ??= closeModelProxy();
|
|
404
|
+
return closePromise;
|
|
405
|
+
};
|
|
406
|
+
const onAbort = () => {
|
|
407
|
+
for (const controller of activeControllers) controller.abort(args.signal?.reason);
|
|
408
|
+
close().catch(() => void 0);
|
|
409
|
+
};
|
|
410
|
+
args.signal?.addEventListener("abort", onAbort, { once: true });
|
|
411
|
+
if (args.signal?.aborted) onAbort();
|
|
412
|
+
const assertExecutionComplete = () => {
|
|
413
|
+
if (executionRecordCount !== modelCallInvocations) throw new Error(`external optimizer model callback returned ${executionRecordCount} execution records for ${modelCallInvocations} invoked calls`);
|
|
414
|
+
};
|
|
415
|
+
return {
|
|
416
|
+
baseUrl: `http://127.0.0.1:${port}/v1`,
|
|
417
|
+
apiKey: token,
|
|
418
|
+
requestAttempts: () => requestCount,
|
|
419
|
+
successfulCompletions: () => successfulCompletionCount,
|
|
420
|
+
assertExecutionComplete,
|
|
421
|
+
failures: () => [...failures],
|
|
422
|
+
close
|
|
423
|
+
};
|
|
424
|
+
async function closeModelProxy() {
|
|
425
|
+
args.signal?.removeEventListener("abort", onAbort);
|
|
426
|
+
accepting = false;
|
|
427
|
+
const closingServer = closeServer(server);
|
|
428
|
+
server.closeIdleConnections?.();
|
|
429
|
+
const [serverResult] = await Promise.allSettled([closingServer, waitForActiveHandlers$1(activeHandlers)]);
|
|
430
|
+
if (activeControllers.size !== 0 || activeHandlers.size !== 0) throw new Error("external optimizer model proxy closed with active request work");
|
|
431
|
+
let executionError;
|
|
432
|
+
try {
|
|
433
|
+
assertExecutionComplete();
|
|
434
|
+
} catch (error) {
|
|
435
|
+
executionError = error;
|
|
436
|
+
}
|
|
437
|
+
if (serverResult?.status === "rejected" && executionError !== void 0) throw new AggregateError([serverResult.reason, executionError], "external optimizer model proxy close and execution evidence both failed");
|
|
438
|
+
if (serverResult?.status === "rejected") throw serverResult.reason;
|
|
439
|
+
if (executionError !== void 0) throw executionError;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function handleModelProxyRequest(args) {
|
|
443
|
+
const { controller, request, response } = args;
|
|
444
|
+
try {
|
|
445
|
+
const path = request.url ? new URL(request.url, "http://127.0.0.1").pathname : "";
|
|
446
|
+
if (request.method !== "POST" || !MODEL_PROXY_PATHS.has(path)) {
|
|
447
|
+
sendJsonIfOpen$1(response, 404, { error: "not found" });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const modelPath = path;
|
|
451
|
+
if (request.headers.authorization !== `Bearer ${args.token}`) {
|
|
452
|
+
sendJsonIfOpen$1(response, 401, { error: "unauthorized" });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const body = await readBody(request, args.args.budget.maxRequestBytes);
|
|
456
|
+
const parsed = parseModelProxyRequest(body, modelPath, args.args.model, args.args.budget);
|
|
457
|
+
const maximumUsage = conservativeMaximumUsage(body.byteLength, parsed.maxOutputTokens + (args.args.budget.maxReasoningTokensPerRequest ?? 0), args.args.budget.pricing);
|
|
458
|
+
const maximumCostUsd = args.args.budget.pricing ? costForTokenPricing(args.args.budget.pricing, maximumUsage) : void 0;
|
|
459
|
+
const reservation = args.nextReservation(maximumCostUsd);
|
|
460
|
+
if (!reservation.accepted) {
|
|
461
|
+
sendJsonIfOpen$1(response, 429, { error: reservation.reason });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const timeout = setTimeout(() => controller.abort(), args.args.budget.requestTimeoutMs ?? 3e5);
|
|
465
|
+
let chargedForBudget = maximumCostUsd;
|
|
466
|
+
try {
|
|
467
|
+
const paid = await args.args.costLedger.runPaidCall({
|
|
468
|
+
...args.args.callId ? { callId: args.args.callId } : {},
|
|
469
|
+
channel: args.args.channel ?? "optimizer",
|
|
470
|
+
phase: args.args.phase,
|
|
471
|
+
actor: args.args.actor,
|
|
472
|
+
...args.args.tags ? { tags: args.args.tags } : {},
|
|
473
|
+
model: args.args.model,
|
|
474
|
+
...args.args.budget.pricing ? { maximumCharge: {
|
|
475
|
+
customTokenPricing: args.args.budget.pricing,
|
|
476
|
+
...maximumUsage
|
|
477
|
+
} } : {},
|
|
478
|
+
signal: controller.signal,
|
|
479
|
+
execute: async (paidCallSignal, paidCallId) => forwardModelProxyRequest({
|
|
480
|
+
call: args.args.call,
|
|
481
|
+
callId: paidCallId,
|
|
482
|
+
callRef: args.args.callRef,
|
|
483
|
+
recordExecutionReceipt: args.recordExecutionReceipt,
|
|
484
|
+
recordModelCallInvocation: args.recordModelCallInvocation,
|
|
485
|
+
path: modelPath,
|
|
486
|
+
request: parsed.request,
|
|
487
|
+
model: args.args.model,
|
|
488
|
+
maxOutputTokens: parsed.maxOutputTokens,
|
|
489
|
+
...args.args.budget.maxReasoningTokensPerRequest === void 0 ? {} : { maxReasoningTokens: args.args.budget.maxReasoningTokensPerRequest },
|
|
490
|
+
maxResponseBytes: args.args.budget.maxResponseBytes,
|
|
491
|
+
signal: paidCallSignal
|
|
492
|
+
}),
|
|
493
|
+
receipt: (result) => result.receipt,
|
|
494
|
+
receiptFromError: () => ({
|
|
495
|
+
model: args.args.model,
|
|
496
|
+
inputTokens: 0,
|
|
497
|
+
outputTokens: 0,
|
|
498
|
+
costUnknown: true,
|
|
499
|
+
usageUnknown: true
|
|
500
|
+
})
|
|
501
|
+
});
|
|
502
|
+
if (!paid.succeeded) {
|
|
503
|
+
args.recordFailure(paid.error);
|
|
504
|
+
chargedForBudget = paid.receipt ? paid.receipt.usageUnknown || paid.receipt.costUnknown ? maximumCostUsd : paid.receipt.costUsd : void 0;
|
|
505
|
+
sendJsonIfOpen$1(response, isAbortError(paid.error) ? 504 : paid.error instanceof ProviderResponseTooLargeError || paid.error instanceof MissingModelExecutionError || paid.error instanceof ModelExecutionPersistenceError || paid.error instanceof OwnerModelContractError ? 502 : 429, { error: paid.error.message });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (paid.value.modelCallFailed) {
|
|
509
|
+
sendJsonIfOpen$1(response, 502, { error: paid.value.modelCallFailed });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
chargedForBudget = paid.value.usageComplete ? paid.receipt.costUsd : maximumCostUsd;
|
|
513
|
+
if (paid.value.usageRejected) {
|
|
514
|
+
sendJsonIfOpen$1(response, 502, { error: paid.value.usageRejected });
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (paid.value.status >= 200 && paid.value.status < 300) args.recordSuccessfulCompletion();
|
|
518
|
+
if (response.destroyed || response.writableEnded) return;
|
|
519
|
+
response.writeHead(paid.value.status, {
|
|
520
|
+
"content-type": paid.value.contentType,
|
|
521
|
+
"content-length": String(paid.value.body.byteLength)
|
|
522
|
+
});
|
|
523
|
+
response.end(paid.value.body);
|
|
524
|
+
} finally {
|
|
525
|
+
clearTimeout(timeout);
|
|
526
|
+
args.settleReservation(maximumCostUsd, chargedForBudget);
|
|
527
|
+
}
|
|
528
|
+
} catch (error) {
|
|
529
|
+
sendJsonIfOpen$1(response, error instanceof RequestBodyTooLargeError ? 413 : error instanceof Error && isAbortError(error) ? 503 : 400, { error: toErrorMessage(error) });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async function waitForActiveHandlers$1(activeHandlers) {
|
|
533
|
+
while (activeHandlers.size > 0) await Promise.allSettled([...activeHandlers]);
|
|
534
|
+
}
|
|
535
|
+
function sendJsonIfOpen$1(response, status, body) {
|
|
536
|
+
if (response.destroyed || response.writableEnded) return;
|
|
537
|
+
sendJson(response, status, body);
|
|
538
|
+
}
|
|
539
|
+
async function forwardModelProxyRequest(args) {
|
|
540
|
+
args.recordModelCallInvocation();
|
|
541
|
+
let called;
|
|
542
|
+
try {
|
|
543
|
+
called = await args.call({
|
|
544
|
+
callId: args.callId,
|
|
545
|
+
request: freezeJsonSnapshot(args.request, "optimizer model canonical request"),
|
|
546
|
+
endpointFormat: endpointFormat(args.path),
|
|
547
|
+
signal: args.signal
|
|
548
|
+
});
|
|
549
|
+
} catch (error) {
|
|
550
|
+
throw new MissingModelExecutionError(`optimizer model callback rejected without execution evidence: ${toErrorMessage(error)}`);
|
|
551
|
+
}
|
|
552
|
+
if (!called || typeof called !== "object" || typeof called.succeeded !== "boolean") throw new MissingModelExecutionError("optimizer model callback returned no typed success/failure outcome");
|
|
553
|
+
let execution;
|
|
554
|
+
try {
|
|
555
|
+
execution = freezeJsonSnapshot(called.execution, "optimizer model callback execution evidence");
|
|
556
|
+
} catch (error) {
|
|
557
|
+
throw new MissingModelExecutionError(`optimizer model callback returned invalid execution evidence: ${toErrorMessage(error)}`);
|
|
558
|
+
}
|
|
559
|
+
if (called.succeeded) {
|
|
560
|
+
let authoritativeReceipt;
|
|
561
|
+
let canonicalResponse;
|
|
562
|
+
try {
|
|
563
|
+
authoritativeReceipt = snapshotModelReceipt(called.receipt, args.model);
|
|
564
|
+
canonicalResponse = snapshotChatResponse(called.response, args.model);
|
|
565
|
+
assertResponseUsageMatchesReceipt(canonicalResponse, authoritativeReceipt);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
args.recordExecutionReceipt({
|
|
568
|
+
callId: args.callId,
|
|
569
|
+
callRef: args.callRef,
|
|
570
|
+
path: args.path,
|
|
571
|
+
model: args.model,
|
|
572
|
+
succeeded: false,
|
|
573
|
+
error: toErrorMessage(error),
|
|
574
|
+
execution
|
|
575
|
+
});
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
args.recordExecutionReceipt({
|
|
579
|
+
callId: args.callId,
|
|
580
|
+
callRef: args.callRef,
|
|
581
|
+
path: args.path,
|
|
582
|
+
model: args.model,
|
|
583
|
+
succeeded: true,
|
|
584
|
+
responseStatus: 200,
|
|
585
|
+
execution
|
|
586
|
+
});
|
|
587
|
+
const usageKnown = canonicalResponse.usage.captured !== false;
|
|
588
|
+
const reasoningTokens = canonicalResponse.usage.reasoningTokens ?? 0;
|
|
589
|
+
const completionTokens = canonicalResponse.usage.completionTokens - reasoningTokens;
|
|
590
|
+
const usageRejected = usageKnown && completionTokens > args.maxOutputTokens ? `optimizer model execution reported ${completionTokens} completion tokens, exceeding requested limit ${args.maxOutputTokens}` : usageKnown && args.maxReasoningTokens !== void 0 && reasoningTokens > args.maxReasoningTokens ? `optimizer model execution reported ${reasoningTokens} reasoning tokens, exceeding the declared budget ${args.maxReasoningTokens}` : void 0;
|
|
591
|
+
return {
|
|
592
|
+
status: 200,
|
|
593
|
+
contentType: "application/json",
|
|
594
|
+
body: encodeCanonicalModelResponse({
|
|
595
|
+
path: args.path,
|
|
596
|
+
callId: args.callId,
|
|
597
|
+
response: canonicalResponse,
|
|
598
|
+
receipt: authoritativeReceipt,
|
|
599
|
+
maxBytes: args.maxResponseBytes
|
|
600
|
+
}),
|
|
601
|
+
receipt: authoritativeReceipt,
|
|
602
|
+
usageComplete: authoritativeReceipt.usageUnknown !== true,
|
|
603
|
+
...usageRejected ? { usageRejected } : {}
|
|
604
|
+
};
|
|
605
|
+
} else {
|
|
606
|
+
if (typeof called.error !== "string" || !called.error.trim()) throw new MissingModelExecutionError("optimizer model callback failure did not return a public error");
|
|
607
|
+
args.recordExecutionReceipt({
|
|
608
|
+
callId: args.callId,
|
|
609
|
+
callRef: args.callRef,
|
|
610
|
+
path: args.path,
|
|
611
|
+
model: args.model,
|
|
612
|
+
succeeded: false,
|
|
613
|
+
error: called.error,
|
|
614
|
+
execution
|
|
615
|
+
});
|
|
616
|
+
const failedReceipt = snapshotModelReceipt(called.receipt, args.model);
|
|
617
|
+
return {
|
|
618
|
+
status: 502,
|
|
619
|
+
contentType: "application/json",
|
|
620
|
+
body: new TextEncoder().encode(JSON.stringify({ error: called.error })),
|
|
621
|
+
receipt: failedReceipt,
|
|
622
|
+
usageComplete: failedReceipt.usageUnknown !== true,
|
|
623
|
+
modelCallFailed: called.error
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function snapshotModelReceipt(value, expectedModel) {
|
|
628
|
+
let snapshot;
|
|
629
|
+
try {
|
|
630
|
+
assertJsonValue(value, "optimizer model callback receipt");
|
|
631
|
+
snapshot = JSON.parse(canonicalJson(value));
|
|
632
|
+
} catch (error) {
|
|
633
|
+
throw new OwnerModelContractError(`optimizer model callback returned an invalid receipt: ${toErrorMessage(error)}`);
|
|
634
|
+
}
|
|
635
|
+
if (snapshot.model !== expectedModel) throw new OwnerModelContractError(`optimizer model callback receipt used '${snapshot.model}' instead of '${expectedModel}'`);
|
|
636
|
+
return snapshot;
|
|
637
|
+
}
|
|
638
|
+
function freezeJsonSnapshot(value, label) {
|
|
639
|
+
assertJsonValue(value, label);
|
|
640
|
+
return deepFreezeJson(JSON.parse(canonicalJson(value)));
|
|
641
|
+
}
|
|
642
|
+
function deepFreezeJson(value) {
|
|
643
|
+
if (value !== null && typeof value === "object") {
|
|
644
|
+
for (const child of Object.values(value)) deepFreezeJson(child);
|
|
645
|
+
Object.freeze(value);
|
|
646
|
+
}
|
|
647
|
+
return value;
|
|
648
|
+
}
|
|
649
|
+
function assertResponseUsageMatchesReceipt(response, receipt) {
|
|
650
|
+
if (response.usage.captured === false || receipt.usageUnknown === true) {
|
|
651
|
+
if (response.usage.captured !== false || receipt.usageUnknown !== true) throw new OwnerModelContractError("optimizer model response and execution receipt disagree about whether usage was captured");
|
|
652
|
+
} else {
|
|
653
|
+
const cachedTokens = response.usage.cachedPromptTokens ?? 0;
|
|
654
|
+
if (receipt.inputTokens + (receipt.cachedTokens ?? 0) !== response.usage.promptTokens) throw new OwnerModelContractError("optimizer model response promptTokens must equal receipt inputTokens plus cachedTokens");
|
|
655
|
+
if (cachedTokens !== (receipt.cachedTokens ?? 0)) throw new OwnerModelContractError("optimizer model response cachedPromptTokens disagrees with the execution receipt");
|
|
656
|
+
if (response.usage.completionTokens !== receipt.outputTokens) throw new OwnerModelContractError("optimizer model response completionTokens disagrees with the execution receipt");
|
|
657
|
+
if ((response.usage.reasoningTokens ?? 0) !== (receipt.reasoningTokens ?? 0)) throw new OwnerModelContractError("optimizer model response reasoningTokens disagrees with the execution receipt");
|
|
658
|
+
}
|
|
659
|
+
const receiptCostUsd = receipt.actualCostUsd ?? receipt.estimatedCostUsd ?? (receipt.customTokenPricing && receipt.usageUnknown !== true ? costForTokenPricing(receipt.customTokenPricing, receipt) : null);
|
|
660
|
+
if (response.costUsd !== receiptCostUsd) throw new OwnerModelContractError("optimizer model response billed cost disagrees with the execution receipt");
|
|
661
|
+
}
|
|
662
|
+
function parseModelProxyRequest(body, path, expectedModel, budget) {
|
|
663
|
+
let value;
|
|
664
|
+
try {
|
|
665
|
+
value = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
666
|
+
} catch {
|
|
667
|
+
throw new Error("optimizer model request must be valid JSON");
|
|
668
|
+
}
|
|
669
|
+
if (!isRecord(value)) throw new Error("optimizer model request must be a JSON object");
|
|
670
|
+
if (value.model !== expectedModel) throw new Error(`optimizer model request must use configured model '${expectedModel}'`);
|
|
671
|
+
if (value.stream !== void 0 && value.stream !== false) throw new Error("streaming optimizer model requests are not supported");
|
|
672
|
+
for (const field of [
|
|
673
|
+
"n",
|
|
674
|
+
"best_of",
|
|
675
|
+
"candidate_count",
|
|
676
|
+
"num_candidates"
|
|
677
|
+
]) if (value[field] !== void 0 && value[field] !== 1) throw new Error(`optimizer model request ${field} must be 1 when supplied`);
|
|
678
|
+
if (value.reasoning !== void 0 || value.reasoning_effort !== void 0) throw new Error("optimizer model reasoning settings belong to the execution AgentProfile");
|
|
679
|
+
const maxOutputTokens = parseOutputMaximum(value, path === "/v1/responses" ? ["max_output_tokens"] : [
|
|
680
|
+
"max_tokens",
|
|
681
|
+
"max_completion_tokens",
|
|
682
|
+
"max_output_tokens"
|
|
683
|
+
]);
|
|
684
|
+
if (maxOutputTokens > budget.maxOutputTokensPerRequest) throw new Error("optimizer model request exceeds maxOutputTokensPerRequest");
|
|
685
|
+
return {
|
|
686
|
+
maxOutputTokens,
|
|
687
|
+
request: freezeJsonSnapshot(path === "/v1/chat/completions" ? parseChatCompletionsRequest(value, expectedModel, maxOutputTokens) : parseResponsesRequest(value, expectedModel, maxOutputTokens), "optimizer model canonical request")
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function parseChatCompletionsRequest(body, model, maxTokens) {
|
|
691
|
+
assertAllowedKeys(body, [
|
|
692
|
+
"model",
|
|
693
|
+
"messages",
|
|
694
|
+
"max_tokens",
|
|
695
|
+
"max_completion_tokens",
|
|
696
|
+
"max_output_tokens",
|
|
697
|
+
"temperature",
|
|
698
|
+
"thinking",
|
|
699
|
+
"response_format",
|
|
700
|
+
"stream",
|
|
701
|
+
"n",
|
|
702
|
+
"best_of",
|
|
703
|
+
"candidate_count",
|
|
704
|
+
"num_candidates"
|
|
705
|
+
]);
|
|
706
|
+
return {
|
|
707
|
+
model,
|
|
708
|
+
messages: parseChatMessages(body.messages),
|
|
709
|
+
maxTokens,
|
|
710
|
+
...parseTemperature(body.temperature),
|
|
711
|
+
...parseThinking(body.thinking),
|
|
712
|
+
...parseChatResponseFormat(body.response_format)
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function parseResponsesRequest(body, model, maxTokens) {
|
|
716
|
+
assertAllowedKeys(body, [
|
|
717
|
+
"model",
|
|
718
|
+
"input",
|
|
719
|
+
"instructions",
|
|
720
|
+
"max_output_tokens",
|
|
721
|
+
"temperature",
|
|
722
|
+
"thinking",
|
|
723
|
+
"text",
|
|
724
|
+
"stream",
|
|
725
|
+
"n",
|
|
726
|
+
"best_of",
|
|
727
|
+
"candidate_count",
|
|
728
|
+
"num_candidates"
|
|
729
|
+
]);
|
|
730
|
+
const instructions = body.instructions;
|
|
731
|
+
if (instructions !== void 0 && typeof instructions !== "string") throw new Error("optimizer Responses instructions must be a string");
|
|
732
|
+
return {
|
|
733
|
+
model,
|
|
734
|
+
messages: [...instructions === void 0 ? [] : [{
|
|
735
|
+
role: "system",
|
|
736
|
+
content: instructions
|
|
737
|
+
}], ...parseResponsesInput(body.input)],
|
|
738
|
+
maxTokens,
|
|
739
|
+
...parseTemperature(body.temperature),
|
|
740
|
+
...parseThinking(body.thinking),
|
|
741
|
+
...parseResponsesTextFormat(body.text)
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function parseOutputMaximum(body, fields) {
|
|
745
|
+
const supplied = fields.flatMap((field) => body[field] === void 0 ? [] : [{
|
|
746
|
+
field,
|
|
747
|
+
value: body[field]
|
|
748
|
+
}]);
|
|
749
|
+
if (supplied.length === 0 || supplied.some(({ value }) => !Number.isSafeInteger(value) || value <= 0)) throw new Error("optimizer model request requires a positive output-token limit");
|
|
750
|
+
if (new Set(supplied.map(({ value }) => value)).size !== 1) throw new Error("optimizer model request supplied conflicting output-token limits");
|
|
751
|
+
return supplied[0].value;
|
|
752
|
+
}
|
|
753
|
+
function parseTemperature(value) {
|
|
754
|
+
if (value === void 0) return {};
|
|
755
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error("optimizer model request temperature must be finite");
|
|
756
|
+
return { temperature: value };
|
|
757
|
+
}
|
|
758
|
+
function parseThinking(value) {
|
|
759
|
+
if (value === void 0) return {};
|
|
760
|
+
if (!isRecord(value)) throw new Error("optimizer model request thinking must be an object");
|
|
761
|
+
assertAllowedKeys(value, ["type"], "optimizer model request thinking");
|
|
762
|
+
if (value.type !== "enabled" && value.type !== "disabled") throw new Error("optimizer model request thinking.type must be enabled or disabled");
|
|
763
|
+
return { thinking: value.type };
|
|
764
|
+
}
|
|
765
|
+
function parseChatMessages(value) {
|
|
766
|
+
if (!Array.isArray(value)) throw new Error("optimizer chat request messages must be an array");
|
|
767
|
+
return value.map((message, index) => parseMessage(message, `optimizer chat message ${index}`));
|
|
768
|
+
}
|
|
769
|
+
function parseResponsesInput(value) {
|
|
770
|
+
if (typeof value === "string") return [{
|
|
771
|
+
role: "user",
|
|
772
|
+
content: value
|
|
773
|
+
}];
|
|
774
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error("optimizer Responses request requires non-empty input");
|
|
775
|
+
return value.map((message, index) => {
|
|
776
|
+
if (!isRecord(message)) throw new Error(`optimizer Responses input ${index} must be a message object`);
|
|
777
|
+
assertAllowedKeys(message, [
|
|
778
|
+
"type",
|
|
779
|
+
"role",
|
|
780
|
+
"content"
|
|
781
|
+
], `optimizer Responses input ${index}`);
|
|
782
|
+
if (message.type !== void 0 && message.type !== "message") throw new Error(`optimizer Responses input ${index} type must be message`);
|
|
783
|
+
return parseMessage(message, `optimizer Responses input ${index}`, true);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function parseMessage(value, label, responsesContent = false) {
|
|
787
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
788
|
+
assertAllowedKeys(value, responsesContent ? [
|
|
789
|
+
"type",
|
|
790
|
+
"role",
|
|
791
|
+
"content"
|
|
792
|
+
] : ["role", "content"], label);
|
|
793
|
+
if (value.role !== "system" && value.role !== "user" && value.role !== "assistant") throw new Error(`${label} role must be system, user, or assistant`);
|
|
794
|
+
return {
|
|
795
|
+
role: value.role,
|
|
796
|
+
content: parseMessageContent(value.content, label, responsesContent)
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function parseMessageContent(value, label, responsesContent) {
|
|
800
|
+
if (typeof value === "string") return value;
|
|
801
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`${label} content must be text or a non-empty content array`);
|
|
802
|
+
return value.map((part, partIndex) => {
|
|
803
|
+
if (!isRecord(part)) throw new Error(`${label} content ${partIndex} must be an object`);
|
|
804
|
+
const partLabel = `${label} content ${partIndex}`;
|
|
805
|
+
if (part.type === "text" || responsesContent && part.type === "input_text") {
|
|
806
|
+
assertAllowedKeys(part, ["type", "text"], partLabel);
|
|
807
|
+
if (typeof part.text !== "string") throw new Error(`${partLabel} text must be a string`);
|
|
808
|
+
return {
|
|
809
|
+
type: "text",
|
|
810
|
+
text: part.text
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (responsesContent && part.type === "output_text") {
|
|
814
|
+
assertAllowedKeys(part, ["type", "text"], partLabel);
|
|
815
|
+
if (typeof part.text !== "string") throw new Error(`${partLabel} text must be a string`);
|
|
816
|
+
return {
|
|
817
|
+
type: "text",
|
|
818
|
+
text: part.text
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
if (part.type === "image_url") {
|
|
822
|
+
assertAllowedKeys(part, ["type", "image_url"], partLabel);
|
|
823
|
+
if (!isRecord(part.image_url) || typeof part.image_url.url !== "string") throw new Error(`${partLabel} image_url must contain a URL string`);
|
|
824
|
+
assertAllowedKeys(part.image_url, ["url", "detail"], `${partLabel} image_url`);
|
|
825
|
+
const detail = parseImageDetail(part.image_url.detail, partLabel);
|
|
826
|
+
return {
|
|
827
|
+
type: "image_url",
|
|
828
|
+
image_url: {
|
|
829
|
+
url: part.image_url.url,
|
|
830
|
+
...detail
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
if (responsesContent && part.type === "input_image") {
|
|
835
|
+
assertAllowedKeys(part, [
|
|
836
|
+
"type",
|
|
837
|
+
"image_url",
|
|
838
|
+
"detail"
|
|
839
|
+
], partLabel);
|
|
840
|
+
if (typeof part.image_url !== "string") throw new Error(`${partLabel} image_url must be a URL string`);
|
|
841
|
+
const detail = parseImageDetail(part.detail, partLabel);
|
|
842
|
+
return {
|
|
843
|
+
type: "image_url",
|
|
844
|
+
image_url: {
|
|
845
|
+
url: part.image_url,
|
|
846
|
+
...detail
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
throw new Error(`${partLabel} uses an unsupported content type`);
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
function parseImageDetail(value, label) {
|
|
854
|
+
if (value === void 0) return {};
|
|
855
|
+
if (value !== "auto" && value !== "low" && value !== "high") throw new Error(`${label} image detail must be auto, low, or high`);
|
|
856
|
+
return { detail: value };
|
|
857
|
+
}
|
|
858
|
+
function parseChatResponseFormat(value) {
|
|
859
|
+
if (value === void 0) return {};
|
|
860
|
+
if (!isRecord(value) || typeof value.type !== "string") throw new Error("optimizer chat response_format must be an object with a type");
|
|
861
|
+
if (value.type === "text") {
|
|
862
|
+
assertAllowedKeys(value, ["type"], "optimizer chat response_format");
|
|
863
|
+
return {};
|
|
864
|
+
}
|
|
865
|
+
if (value.type === "json_object") {
|
|
866
|
+
assertAllowedKeys(value, ["type"], "optimizer chat response_format");
|
|
867
|
+
return { jsonMode: true };
|
|
868
|
+
}
|
|
869
|
+
if (value.type !== "json_schema") throw new Error("optimizer chat response_format type is unsupported");
|
|
870
|
+
assertAllowedKeys(value, ["type", "json_schema"], "optimizer chat response_format");
|
|
871
|
+
return { jsonSchema: parseJsonSchema(value.json_schema, "optimizer chat response_format") };
|
|
872
|
+
}
|
|
873
|
+
function parseResponsesTextFormat(value) {
|
|
874
|
+
if (value === void 0) return {};
|
|
875
|
+
if (!isRecord(value)) throw new Error("optimizer Responses text must be an object");
|
|
876
|
+
assertAllowedKeys(value, ["format"], "optimizer Responses text");
|
|
877
|
+
if (value.format === void 0) return {};
|
|
878
|
+
if (!isRecord(value.format) || typeof value.format.type !== "string") throw new Error("optimizer Responses text.format must be an object with a type");
|
|
879
|
+
const format = value.format;
|
|
880
|
+
if (format.type === "text") {
|
|
881
|
+
assertAllowedKeys(format, ["type"], "optimizer Responses text.format");
|
|
882
|
+
return {};
|
|
883
|
+
}
|
|
884
|
+
if (format.type === "json_object") {
|
|
885
|
+
assertAllowedKeys(format, ["type"], "optimizer Responses text.format");
|
|
886
|
+
return { jsonMode: true };
|
|
887
|
+
}
|
|
888
|
+
if (format.type !== "json_schema") throw new Error("optimizer Responses text.format type is unsupported");
|
|
889
|
+
return { jsonSchema: parseJsonSchema(format, "optimizer Responses text.format", true) };
|
|
890
|
+
}
|
|
891
|
+
function parseJsonSchema(value, label, flat = false) {
|
|
892
|
+
if (!isRecord(value)) throw new Error(`${label} JSON schema must be an object`);
|
|
893
|
+
assertAllowedKeys(value, [
|
|
894
|
+
"name",
|
|
895
|
+
"schema",
|
|
896
|
+
"strict",
|
|
897
|
+
...flat ? ["type"] : []
|
|
898
|
+
], label);
|
|
899
|
+
if (typeof value.name !== "string" || !value.name.trim()) throw new Error(`${label} JSON schema name must be non-empty`);
|
|
900
|
+
if (!isRecord(value.schema)) throw new Error(`${label} JSON schema body must be an object`);
|
|
901
|
+
if (value.strict !== void 0 && value.strict !== true) throw new Error(`${label} JSON schema strict must be true when supplied`);
|
|
902
|
+
return {
|
|
903
|
+
name: value.name,
|
|
904
|
+
schema: freezeJsonSnapshot(value.schema, `${label} schema`)
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
function assertAllowedKeys(value, allowed, label = "optimizer model request") {
|
|
908
|
+
const allowedSet = new Set(allowed);
|
|
909
|
+
const unknown = Object.keys(value).filter((key) => !allowedSet.has(key));
|
|
910
|
+
if (unknown.length > 0) throw new Error(`${label} contains unsupported fields: ${unknown.join(", ")}`);
|
|
911
|
+
}
|
|
912
|
+
function snapshotChatResponse(value, expectedModel) {
|
|
913
|
+
if (!isRecord(value)) throw new OwnerModelContractError("optimizer model callback response must be an object");
|
|
914
|
+
if (typeof value.content !== "string") throw new OwnerModelContractError("optimizer model callback response content must be a string");
|
|
915
|
+
if (value.model !== expectedModel) throw new OwnerModelContractError(`optimizer model callback response used '${String(value.model)}' instead of '${expectedModel}'`);
|
|
916
|
+
if (!isNonnegativeFiniteNumber(value.durationMs)) throw new OwnerModelContractError("optimizer model callback response durationMs must be finite and non-negative");
|
|
917
|
+
if (value.costUsd !== null && !isNonnegativeFiniteNumber(value.costUsd)) throw new OwnerModelContractError("optimizer model callback response costUsd must be null or finite and non-negative");
|
|
918
|
+
if (value.finishReason !== void 0 && value.finishReason !== null && typeof value.finishReason !== "string") throw new OwnerModelContractError("optimizer model callback response finishReason must be a string or null");
|
|
919
|
+
if (value.contentEmpty !== void 0 && (typeof value.contentEmpty !== "boolean" || value.contentEmpty !== (value.content.trim().length === 0))) throw new OwnerModelContractError("optimizer model callback response contentEmpty must match content");
|
|
920
|
+
if (!isRecord(value.raw)) throw new OwnerModelContractError("optimizer model callback response raw must be an object");
|
|
921
|
+
if (!isRecord(value.usage)) throw new OwnerModelContractError("optimizer model callback response usage must be an object");
|
|
922
|
+
const usage = value.usage;
|
|
923
|
+
for (const field of [
|
|
924
|
+
"promptTokens",
|
|
925
|
+
"completionTokens",
|
|
926
|
+
"totalTokens"
|
|
927
|
+
]) if (!isNonnegativeSafeInteger(usage[field])) throw new OwnerModelContractError(`optimizer model callback response usage.${field} must be a non-negative safe integer`);
|
|
928
|
+
const promptTokens = usage.promptTokens;
|
|
929
|
+
const completionTokens = usage.completionTokens;
|
|
930
|
+
const totalTokens = usage.totalTokens;
|
|
931
|
+
if (totalTokens !== promptTokens + completionTokens) throw new OwnerModelContractError("optimizer model callback response totalTokens must equal promptTokens plus completionTokens");
|
|
932
|
+
if (usage.captured !== void 0 && typeof usage.captured !== "boolean") throw new OwnerModelContractError("optimizer model callback response usage.captured must be a boolean");
|
|
933
|
+
if (usage.cachedPromptTokens !== void 0 && (!isNonnegativeSafeInteger(usage.cachedPromptTokens) || usage.cachedPromptTokens > promptTokens)) throw new OwnerModelContractError("optimizer model callback response cachedPromptTokens must be a subset of promptTokens");
|
|
934
|
+
if (usage.reasoningTokens !== void 0 && (!isNonnegativeSafeInteger(usage.reasoningTokens) || usage.reasoningTokens > completionTokens)) throw new OwnerModelContractError("optimizer model callback response reasoningTokens must be a subset of completionTokens");
|
|
935
|
+
let raw;
|
|
936
|
+
try {
|
|
937
|
+
raw = freezeJsonSnapshot(value.raw, "optimizer model callback canonical response.raw");
|
|
938
|
+
} catch (error) {
|
|
939
|
+
throw new OwnerModelContractError(`optimizer model callback returned invalid raw response evidence: ${toErrorMessage(error)}`);
|
|
940
|
+
}
|
|
941
|
+
return deepFreezeJson({
|
|
942
|
+
content: value.content,
|
|
943
|
+
usage: {
|
|
944
|
+
promptTokens,
|
|
945
|
+
completionTokens,
|
|
946
|
+
totalTokens,
|
|
947
|
+
...usage.captured === void 0 ? {} : { captured: usage.captured },
|
|
948
|
+
...usage.reasoningTokens === void 0 ? {} : { reasoningTokens: usage.reasoningTokens },
|
|
949
|
+
...usage.cachedPromptTokens === void 0 ? {} : { cachedPromptTokens: usage.cachedPromptTokens }
|
|
950
|
+
},
|
|
951
|
+
costUsd: value.costUsd,
|
|
952
|
+
model: value.model,
|
|
953
|
+
durationMs: value.durationMs,
|
|
954
|
+
...value.finishReason === void 0 ? {} : { finishReason: value.finishReason },
|
|
955
|
+
...value.contentEmpty === void 0 ? {} : { contentEmpty: value.contentEmpty },
|
|
956
|
+
raw
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
function encodeCanonicalModelResponse(args) {
|
|
960
|
+
const usage = encodeProtocolUsage(args.path, args.response, args.receipt);
|
|
961
|
+
const finishReason = args.response.finishReason === void 0 ? "stop" : args.response.finishReason;
|
|
962
|
+
const body = args.path === "/v1/chat/completions" ? {
|
|
963
|
+
id: `chatcmpl-${args.callId}`,
|
|
964
|
+
object: "chat.completion",
|
|
965
|
+
created: 0,
|
|
966
|
+
model: args.response.model,
|
|
967
|
+
choices: [{
|
|
968
|
+
index: 0,
|
|
969
|
+
message: {
|
|
970
|
+
role: "assistant",
|
|
971
|
+
content: args.response.content
|
|
972
|
+
},
|
|
973
|
+
finish_reason: finishReason
|
|
974
|
+
}],
|
|
975
|
+
...usage ? { usage } : {}
|
|
976
|
+
} : {
|
|
977
|
+
id: `resp-${args.callId}`,
|
|
978
|
+
object: "response",
|
|
979
|
+
created_at: 0,
|
|
980
|
+
model: args.response.model,
|
|
981
|
+
status: finishReason === "length" ? "incomplete" : "completed",
|
|
982
|
+
error: null,
|
|
983
|
+
incomplete_details: finishReason === "length" ? { reason: "max_output_tokens" } : null,
|
|
984
|
+
output: [{
|
|
985
|
+
id: `msg-${args.callId}`,
|
|
986
|
+
type: "message",
|
|
987
|
+
status: finishReason === "length" ? "incomplete" : "completed",
|
|
988
|
+
role: "assistant",
|
|
989
|
+
content: [{
|
|
990
|
+
type: "output_text",
|
|
991
|
+
text: args.response.content,
|
|
992
|
+
annotations: []
|
|
993
|
+
}]
|
|
994
|
+
}],
|
|
995
|
+
output_text: args.response.content,
|
|
996
|
+
...usage ? { usage } : {}
|
|
997
|
+
};
|
|
998
|
+
const encoded = new TextEncoder().encode(JSON.stringify(body));
|
|
999
|
+
if (encoded.byteLength > args.maxBytes) throw new ProviderResponseTooLargeError();
|
|
1000
|
+
return encoded;
|
|
1001
|
+
}
|
|
1002
|
+
function endpointFormat(path) {
|
|
1003
|
+
return path === "/v1/responses" ? "responses" : "chat-completions";
|
|
1004
|
+
}
|
|
1005
|
+
function encodeProtocolUsage(path, response, receipt) {
|
|
1006
|
+
if (response.usage.captured === false) return void 0;
|
|
1007
|
+
const inputDetails = {
|
|
1008
|
+
...receipt.cachedTokens === void 0 ? {} : { cached_tokens: receipt.cachedTokens },
|
|
1009
|
+
...receipt.cacheWriteTokens === void 0 ? {} : path === "/v1/chat/completions" ? { cache_creation_tokens: receipt.cacheWriteTokens } : { cache_write_tokens: receipt.cacheWriteTokens }
|
|
1010
|
+
};
|
|
1011
|
+
const outputDetails = { ...receipt.reasoningTokens === void 0 ? {} : { reasoning_tokens: receipt.reasoningTokens } };
|
|
1012
|
+
return path === "/v1/chat/completions" ? {
|
|
1013
|
+
prompt_tokens: response.usage.promptTokens,
|
|
1014
|
+
completion_tokens: response.usage.completionTokens,
|
|
1015
|
+
total_tokens: response.usage.totalTokens,
|
|
1016
|
+
...Object.keys(inputDetails).length > 0 ? { prompt_tokens_details: inputDetails } : {},
|
|
1017
|
+
...Object.keys(outputDetails).length > 0 ? { completion_tokens_details: outputDetails } : {},
|
|
1018
|
+
...receipt.actualCostUsd === void 0 ? {} : { cost: receipt.actualCostUsd }
|
|
1019
|
+
} : {
|
|
1020
|
+
input_tokens: response.usage.promptTokens,
|
|
1021
|
+
output_tokens: response.usage.completionTokens,
|
|
1022
|
+
total_tokens: response.usage.totalTokens,
|
|
1023
|
+
...Object.keys(inputDetails).length > 0 ? { input_tokens_details: inputDetails } : {},
|
|
1024
|
+
...Object.keys(outputDetails).length > 0 ? { output_tokens_details: outputDetails } : {},
|
|
1025
|
+
...receipt.actualCostUsd === void 0 ? {} : { cost: receipt.actualCostUsd }
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function isNonnegativeFiniteNumber(value) {
|
|
1029
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
1030
|
+
}
|
|
1031
|
+
function isNonnegativeSafeInteger(value) {
|
|
1032
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
1033
|
+
}
|
|
1034
|
+
function conservativeMaximumUsage(inputTokenUpperBound, outputTokenUpperBound, pricing) {
|
|
1035
|
+
if (pricing === void 0) return {
|
|
1036
|
+
inputTokens: inputTokenUpperBound,
|
|
1037
|
+
outputTokens: outputTokenUpperBound
|
|
1038
|
+
};
|
|
1039
|
+
const inputRates = [pricing.inputUsdPerMillion, pricing.cachedInputUsdPerMillion ?? pricing.inputUsdPerMillion];
|
|
1040
|
+
const mostExpensiveInputClass = inputRates.indexOf(Math.max(...inputRates));
|
|
1041
|
+
return {
|
|
1042
|
+
inputTokens: mostExpensiveInputClass === 0 ? inputTokenUpperBound : 0,
|
|
1043
|
+
...mostExpensiveInputClass === 1 ? { cachedTokens: inputTokenUpperBound } : {},
|
|
1044
|
+
cacheWriteTokens: inputTokenUpperBound,
|
|
1045
|
+
outputTokens: outputTokenUpperBound
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function assertModelProxyConfig(args) {
|
|
1049
|
+
for (const [label, value] of [
|
|
1050
|
+
["model", args.model],
|
|
1051
|
+
["phase", args.phase],
|
|
1052
|
+
["actor", args.actor]
|
|
1053
|
+
]) if (typeof value !== "string" || !value.trim() || value.trim() !== value) throw new Error(`external optimizer model proxy: ${label} must be trimmed and non-empty`);
|
|
1054
|
+
if (typeof args.call !== "function") throw new Error("external optimizer model proxy: call must be a function");
|
|
1055
|
+
if (typeof args.callRef !== "string" || !args.callRef.trim() || args.callRef.trim() !== args.callRef) throw new Error("external optimizer model proxy: callRef must be trimmed and non-empty");
|
|
1056
|
+
if (typeof args.recordExecution !== "function") throw new Error("external optimizer model proxy: recordExecution must be a function");
|
|
1057
|
+
if (args.callId !== void 0) {
|
|
1058
|
+
if (typeof args.callId !== "string" || !args.callId.trim() || args.callId !== args.callId.trim()) throw new Error("external optimizer model proxy: callId must be trimmed and non-empty");
|
|
1059
|
+
if (args.budget.maxRequests !== 1) throw new Error("external optimizer model proxy: callId requires budget.maxRequests to be 1");
|
|
1060
|
+
}
|
|
1061
|
+
assertExternalOptimizerModelBudget(args.budget, "external optimizer model proxy: budget");
|
|
1062
|
+
if (args.tags !== void 0) {
|
|
1063
|
+
for (const [key, value] of Object.entries(args.tags)) if (!key.trim() || key.trim() !== key || !value.trim() || value.trim() !== value) throw new Error("external optimizer model proxy: tags must be trimmed and non-empty");
|
|
1064
|
+
}
|
|
1065
|
+
if (args.initialUsage !== void 0) {
|
|
1066
|
+
if (!Number.isSafeInteger(args.initialUsage.requests) || args.initialUsage.requests < 0 || args.initialUsage.costUsd !== void 0 && (!Number.isFinite(args.initialUsage.costUsd) || args.initialUsage.costUsd < 0)) throw new Error("external optimizer model proxy: initialUsage must contain non-negative requests and cost");
|
|
1067
|
+
if (args.initialUsage.requests > args.budget.maxRequests || args.budget.maxCostUsd !== void 0 && args.initialUsage.costUsd !== void 0 && args.initialUsage.costUsd > args.budget.maxCostUsd + Number.EPSILON) throw new Error("external optimizer model proxy: initialUsage exceeds the configured budget");
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
var RequestBodyTooLargeError = class extends Error {};
|
|
1071
|
+
var ProviderResponseTooLargeError = class extends Error {
|
|
1072
|
+
constructor() {
|
|
1073
|
+
super("optimizer model response exceeds maxResponseBytes");
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
var MissingModelExecutionError = class extends Error {};
|
|
1077
|
+
var ModelExecutionPersistenceError = class extends Error {};
|
|
1078
|
+
var OwnerModelContractError = class extends Error {};
|
|
1079
|
+
function readBody(request, maximumBytes) {
|
|
1080
|
+
return new Promise((resolvePromise, reject) => {
|
|
1081
|
+
let size = 0;
|
|
1082
|
+
let tooLarge = false;
|
|
1083
|
+
const chunks = [];
|
|
1084
|
+
request.on("data", (chunk) => {
|
|
1085
|
+
if (tooLarge) return;
|
|
1086
|
+
size += chunk.byteLength;
|
|
1087
|
+
if (size > maximumBytes) {
|
|
1088
|
+
tooLarge = true;
|
|
1089
|
+
chunks.length = 0;
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
chunks.push(chunk);
|
|
1093
|
+
});
|
|
1094
|
+
request.on("error", reject);
|
|
1095
|
+
request.on("end", () => {
|
|
1096
|
+
if (tooLarge) {
|
|
1097
|
+
reject(new RequestBodyTooLargeError("optimizer model request body too large"));
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
resolvePromise(new Uint8Array(Buffer.concat(chunks)));
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
function randomLocalToken() {
|
|
1105
|
+
return randomBytes(32).toString("hex");
|
|
1106
|
+
}
|
|
1107
|
+
function isAbortError(error) {
|
|
1108
|
+
return error.name === "AbortError" || error.message.toLowerCase().includes("abort");
|
|
1109
|
+
}
|
|
1110
|
+
function toErrorMessage(error) {
|
|
1111
|
+
return error instanceof Error ? error.message : String(error);
|
|
1112
|
+
}
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region src/campaign/external-optimizer-resources.ts
|
|
1115
|
+
async function runWithCleanup(args) {
|
|
1116
|
+
let value;
|
|
1117
|
+
let operationError;
|
|
1118
|
+
try {
|
|
1119
|
+
value = await args.run();
|
|
1120
|
+
} catch (cause) {
|
|
1121
|
+
operationError = toError(cause);
|
|
1122
|
+
}
|
|
1123
|
+
let cleanupError;
|
|
1124
|
+
try {
|
|
1125
|
+
await args.cleanup();
|
|
1126
|
+
} catch (cause) {
|
|
1127
|
+
cleanupError = toError(cause);
|
|
1128
|
+
}
|
|
1129
|
+
if (operationError && cleanupError) throw new AggregateError([operationError, cleanupError], `${operationError.message}; ${args.label} cleanup failed: ${cleanupError.message}`);
|
|
1130
|
+
if (operationError) throw operationError;
|
|
1131
|
+
if (cleanupError) throw new Error(`${args.label} cleanup failed: ${cleanupError.message}`, { cause: cleanupError });
|
|
1132
|
+
return value;
|
|
1133
|
+
}
|
|
1134
|
+
async function closeExternalOptimizerResources(args) {
|
|
1135
|
+
const errors = (await Promise.allSettled([...args.modelProxy ? [args.modelProxy.close()] : [], args.callback.close()])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
1136
|
+
if (errors.length > 0) throw new AggregateError(errors, `${args.label}: failed to close optimizer resources`);
|
|
1137
|
+
}
|
|
1138
|
+
function toError(cause) {
|
|
1139
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
1140
|
+
}
|
|
1141
|
+
//#endregion
|
|
1142
|
+
//#region src/campaign/external-optimizer-subprocess.ts
|
|
1143
|
+
const PROCESS_TERMINATION_GRACE_MS = 5e3;
|
|
1144
|
+
const PROCESS_TERMINATION_POLL_MS = 25;
|
|
1145
|
+
const PROCESS_KILL_WAIT_MS = 1e3;
|
|
1146
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
1147
|
+
async function runExternalOptimizerProcess(args) {
|
|
1148
|
+
if (!Number.isSafeInteger(args.timeoutMs) || args.timeoutMs <= 0 || args.timeoutMs > MAX_TIMER_DELAY_MS) throw new Error(`${args.label} timeoutMs must be between 1 and ${MAX_TIMER_DELAY_MS}`);
|
|
1149
|
+
const limits = resolveExternalOptimizerProcessLimits(args.runner?.limits);
|
|
1150
|
+
if (process.platform === "win32") throw new Error(`${args.label} requires POSIX process-group cleanup; run the optimizer through WSL or Linux`);
|
|
1151
|
+
throwIfAborted(args.signal, args.label);
|
|
1152
|
+
const dir = await mkdtemp(join(tmpdir(), args.tempPrefix));
|
|
1153
|
+
const inputPath = join(dir, "input.json");
|
|
1154
|
+
const outputPath = join(dir, "output.json");
|
|
1155
|
+
return runWithCleanup({
|
|
1156
|
+
label: `${args.label} temporary directory`,
|
|
1157
|
+
run: async () => {
|
|
1158
|
+
const serializedInput = JSON.stringify(args.input);
|
|
1159
|
+
if (serializedInput === void 0) throw new Error(`${args.label} input must be JSON-serializable`);
|
|
1160
|
+
const inputJson = `${serializedInput}\n`;
|
|
1161
|
+
const inputBytes = Buffer.byteLength(inputJson);
|
|
1162
|
+
if (inputBytes > limits.maxInputBytes) throw new Error(`${args.label} input exceeds ${limits.maxInputBytes} bytes (${inputBytes})`);
|
|
1163
|
+
await writeFile(inputPath, inputJson);
|
|
1164
|
+
const command = resolveRunnerCommand(args.runner?.command ?? "python");
|
|
1165
|
+
const commandArgs = [
|
|
1166
|
+
...args.runner?.args ?? ["-m", args.module],
|
|
1167
|
+
...args.additionalArgs ?? [],
|
|
1168
|
+
"--input",
|
|
1169
|
+
inputPath,
|
|
1170
|
+
"--output",
|
|
1171
|
+
outputPath
|
|
1172
|
+
];
|
|
1173
|
+
await runProcess({
|
|
1174
|
+
label: args.label,
|
|
1175
|
+
command,
|
|
1176
|
+
args: commandArgs,
|
|
1177
|
+
cwd: dir,
|
|
1178
|
+
env: args.runner?.env,
|
|
1179
|
+
timeoutMs: args.timeoutMs,
|
|
1180
|
+
maxOutputChars: limits.maxOutputChars,
|
|
1181
|
+
signal: args.signal
|
|
1182
|
+
});
|
|
1183
|
+
throwIfAborted(args.signal, args.label);
|
|
1184
|
+
const raw = JSON.parse(await readBoundedTextFile(outputPath, limits.maxResultBytes, `${args.label} output`));
|
|
1185
|
+
throwIfAborted(args.signal, args.label);
|
|
1186
|
+
if (!isRecord(raw)) throw new Error(`${args.label} output must be a JSON object`);
|
|
1187
|
+
return raw;
|
|
1188
|
+
},
|
|
1189
|
+
cleanup: () => rm(dir, {
|
|
1190
|
+
recursive: true,
|
|
1191
|
+
force: true
|
|
1192
|
+
})
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
function resolveRunnerCommand(command) {
|
|
1196
|
+
if (isAbsolute(command) || !command.includes("/") && !command.includes("\\")) return command;
|
|
1197
|
+
return resolve(command);
|
|
1198
|
+
}
|
|
1199
|
+
async function readBoundedTextFile(path, maxBytes, label) {
|
|
1200
|
+
if (!(await lstat(path)).isFile()) throw new Error(`${label} must be a regular file`);
|
|
1201
|
+
const file = await open(path, "r");
|
|
1202
|
+
try {
|
|
1203
|
+
const buffer = Buffer.allocUnsafe(maxBytes + 1);
|
|
1204
|
+
let bytesRead = 0;
|
|
1205
|
+
while (bytesRead < buffer.byteLength) {
|
|
1206
|
+
const chunk = await file.read(buffer, bytesRead, buffer.byteLength - bytesRead, bytesRead);
|
|
1207
|
+
if (chunk.bytesRead === 0) break;
|
|
1208
|
+
bytesRead += chunk.bytesRead;
|
|
1209
|
+
}
|
|
1210
|
+
if (bytesRead > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes`);
|
|
1211
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
1212
|
+
} finally {
|
|
1213
|
+
await file.close();
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function runProcess(args) {
|
|
1217
|
+
throwIfAborted(args.signal, args.label);
|
|
1218
|
+
return new Promise((resolvePromise, reject) => {
|
|
1219
|
+
const child = spawn(args.command, args.args, {
|
|
1220
|
+
cwd: args.cwd,
|
|
1221
|
+
env: {
|
|
1222
|
+
...safeProcessEnvironment(),
|
|
1223
|
+
...args.env
|
|
1224
|
+
},
|
|
1225
|
+
stdio: [
|
|
1226
|
+
"ignore",
|
|
1227
|
+
"pipe",
|
|
1228
|
+
"pipe"
|
|
1229
|
+
],
|
|
1230
|
+
detached: process.platform !== "win32",
|
|
1231
|
+
windowsHide: true
|
|
1232
|
+
});
|
|
1233
|
+
const stdout = createProcessOutputCapture();
|
|
1234
|
+
const stderr = createProcessOutputCapture();
|
|
1235
|
+
let settled = false;
|
|
1236
|
+
let timeout;
|
|
1237
|
+
const onAbort = () => finish(abortReason(args.signal, args.label));
|
|
1238
|
+
const finish = (error) => {
|
|
1239
|
+
if (settled) return;
|
|
1240
|
+
settled = true;
|
|
1241
|
+
if (timeout) clearTimeout(timeout);
|
|
1242
|
+
args.signal?.removeEventListener("abort", onAbort);
|
|
1243
|
+
terminateProcessTree(child).then(() => {
|
|
1244
|
+
if (error) reject(error);
|
|
1245
|
+
else resolvePromise();
|
|
1246
|
+
}, (cleanupError) => {
|
|
1247
|
+
if (error) {
|
|
1248
|
+
const cleanup = cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError));
|
|
1249
|
+
reject(new AggregateError([error, cleanup], `${error.message}; ${args.label} process cleanup failed: ${cleanup.message}`));
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
reject(/* @__PURE__ */ new Error(`${args.label} process cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`));
|
|
1253
|
+
});
|
|
1254
|
+
};
|
|
1255
|
+
timeout = setTimeout(() => {
|
|
1256
|
+
finish(/* @__PURE__ */ new Error(`${args.label} exceeded ${args.timeoutMs}ms`));
|
|
1257
|
+
}, args.timeoutMs);
|
|
1258
|
+
args.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1259
|
+
if (args.signal?.aborted) onAbort();
|
|
1260
|
+
child.stdout.on("data", (chunk) => {
|
|
1261
|
+
appendProcessOutput(stdout, chunk, args.maxOutputChars);
|
|
1262
|
+
});
|
|
1263
|
+
child.stderr.on("data", (chunk) => {
|
|
1264
|
+
appendProcessOutput(stderr, chunk, args.maxOutputChars);
|
|
1265
|
+
});
|
|
1266
|
+
child.on("error", (error) => {
|
|
1267
|
+
finish(/* @__PURE__ */ new Error(`${args.label} could not start: ${error.message}`));
|
|
1268
|
+
});
|
|
1269
|
+
child.on("close", (code) => {
|
|
1270
|
+
if (code === 0) {
|
|
1271
|
+
finish();
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
finish(/* @__PURE__ */ new Error(`${args.label} exited ${String(code)}. stderr=${summarizeProcessOutput(stderr)} stdout=${summarizeProcessOutput(stdout)}`));
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
function throwIfAborted(signal, label) {
|
|
1279
|
+
if (signal?.aborted) throw abortReason(signal, label);
|
|
1280
|
+
}
|
|
1281
|
+
function abortReason(signal, label) {
|
|
1282
|
+
return signal.reason instanceof Error ? signal.reason : new Error(`${label} aborted`, { cause: signal.reason });
|
|
1283
|
+
}
|
|
1284
|
+
function safeProcessEnvironment() {
|
|
1285
|
+
return Object.fromEntries([
|
|
1286
|
+
"PATH",
|
|
1287
|
+
"HOME",
|
|
1288
|
+
"USER",
|
|
1289
|
+
"LOGNAME",
|
|
1290
|
+
"SHELL",
|
|
1291
|
+
"TMPDIR",
|
|
1292
|
+
"TMP",
|
|
1293
|
+
"TEMP",
|
|
1294
|
+
"LANG",
|
|
1295
|
+
"LANGUAGE",
|
|
1296
|
+
"LC_ALL",
|
|
1297
|
+
"VIRTUAL_ENV",
|
|
1298
|
+
"PYTHONPATH",
|
|
1299
|
+
"SYSTEMROOT",
|
|
1300
|
+
"COMSPEC",
|
|
1301
|
+
"PATHEXT",
|
|
1302
|
+
"WINDIR"
|
|
1303
|
+
].flatMap((name) => {
|
|
1304
|
+
const value = process.env[name];
|
|
1305
|
+
return value === void 0 ? [] : [[name, value]];
|
|
1306
|
+
}));
|
|
1307
|
+
}
|
|
1308
|
+
async function terminateProcessTree(child) {
|
|
1309
|
+
const pid = child.pid;
|
|
1310
|
+
if (!pid) return;
|
|
1311
|
+
if (process.platform === "win32") throw new Error("native Windows process-tree cleanup is unsupported");
|
|
1312
|
+
if (!signalProcessGroup(pid, "SIGTERM")) return;
|
|
1313
|
+
if (await waitForProcessGroupExit(pid, PROCESS_TERMINATION_GRACE_MS)) return;
|
|
1314
|
+
if (!signalProcessGroup(pid, "SIGKILL")) return;
|
|
1315
|
+
if (await waitForProcessGroupExit(pid, PROCESS_KILL_WAIT_MS)) return;
|
|
1316
|
+
throw new Error(`process group ${pid} remained alive after SIGKILL`);
|
|
1317
|
+
}
|
|
1318
|
+
function signalProcessGroup(pid, signal) {
|
|
1319
|
+
try {
|
|
1320
|
+
process.kill(-pid, signal);
|
|
1321
|
+
return true;
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
if (isMissingProcessError(error)) return false;
|
|
1324
|
+
throw error;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async function waitForProcessGroupExit(pid, timeoutMs) {
|
|
1328
|
+
const deadline = Date.now() + timeoutMs;
|
|
1329
|
+
while (processGroupExists(pid)) {
|
|
1330
|
+
const remainingMs = deadline - Date.now();
|
|
1331
|
+
if (remainingMs <= 0) return false;
|
|
1332
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(PROCESS_TERMINATION_POLL_MS, remainingMs)));
|
|
1333
|
+
}
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
function processGroupExists(pid) {
|
|
1337
|
+
try {
|
|
1338
|
+
process.kill(-pid, 0);
|
|
1339
|
+
return true;
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
if (isMissingProcessError(error)) return false;
|
|
1342
|
+
throw error;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
function isMissingProcessError(error) {
|
|
1346
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
|
|
1347
|
+
}
|
|
1348
|
+
function createProcessOutputCapture() {
|
|
1349
|
+
return {
|
|
1350
|
+
full: "",
|
|
1351
|
+
head: "",
|
|
1352
|
+
tail: "",
|
|
1353
|
+
totalChars: 0
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
function appendProcessOutput(capture, chunk, maxOutputChars) {
|
|
1357
|
+
const text = chunk.toString();
|
|
1358
|
+
capture.totalChars += text.length;
|
|
1359
|
+
const headChars = Math.floor(maxOutputChars / 2);
|
|
1360
|
+
const tailChars = maxOutputChars - headChars;
|
|
1361
|
+
if (capture.full !== void 0) {
|
|
1362
|
+
const combined = `${capture.full}${text}`;
|
|
1363
|
+
if (combined.length <= maxOutputChars) {
|
|
1364
|
+
capture.full = combined;
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
capture.head = combined.slice(0, headChars);
|
|
1368
|
+
capture.tail = combined.slice(-tailChars);
|
|
1369
|
+
capture.full = void 0;
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
capture.tail = `${capture.tail}${text}`.slice(-tailChars);
|
|
1373
|
+
}
|
|
1374
|
+
function summarizeProcessOutput(capture, max = 4e3) {
|
|
1375
|
+
const compact = (capture.full ?? `${capture.head}\n...[${capture.totalChars - capture.head.length - capture.tail.length} chars omitted]...\n${capture.tail}`).trim().replace(/\s+/g, " ");
|
|
1376
|
+
if (compact.length <= max) return compact;
|
|
1377
|
+
const headChars = Math.floor(max / 3);
|
|
1378
|
+
const tailChars = max - headChars;
|
|
1379
|
+
return `${compact.slice(0, headChars)}...[${compact.length - max} chars omitted]...${compact.slice(-tailChars)}`;
|
|
1380
|
+
}
|
|
1381
|
+
//#endregion
|
|
1382
|
+
//#region src/campaign/external-optimizer-callback.ts
|
|
1383
|
+
async function startExternalOptimizerCallback(args) {
|
|
1384
|
+
assertCallbackConfig(args);
|
|
1385
|
+
const limits = resolveExternalOptimizerCallbackLimits(args.limits);
|
|
1386
|
+
args.signal?.throwIfAborted();
|
|
1387
|
+
let evaluations = 0;
|
|
1388
|
+
let accepting = true;
|
|
1389
|
+
let closePromise;
|
|
1390
|
+
const activeControllers = /* @__PURE__ */ new Set();
|
|
1391
|
+
const activeHandlers = /* @__PURE__ */ new Set();
|
|
1392
|
+
const proposedCandidateHashes = /* @__PURE__ */ new Set();
|
|
1393
|
+
let observationSequence = 0;
|
|
1394
|
+
const server = createServer((request, response) => {
|
|
1395
|
+
if (!accepting) {
|
|
1396
|
+
sendJsonIfOpen(response, 503, { error: "external optimizer callback is closing" });
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const controller = new AbortController();
|
|
1400
|
+
const abortRequest = () => {
|
|
1401
|
+
request.destroy();
|
|
1402
|
+
response.destroy();
|
|
1403
|
+
};
|
|
1404
|
+
activeControllers.add(controller);
|
|
1405
|
+
controller.signal.addEventListener("abort", abortRequest, { once: true });
|
|
1406
|
+
let handler;
|
|
1407
|
+
handler = handleCallback(request, response, controller.signal, args, limits, () => {
|
|
1408
|
+
const accepted = args.acceptEvaluation ? args.acceptEvaluation() : evaluations + 1;
|
|
1409
|
+
if (accepted === void 0) return void 0;
|
|
1410
|
+
if (!Number.isSafeInteger(accepted) || accepted <= 0) throw new Error("external optimizer callback: invalid accepted evaluation count");
|
|
1411
|
+
if (accepted > args.maxEvaluations) return void 0;
|
|
1412
|
+
evaluations += 1;
|
|
1413
|
+
return accepted;
|
|
1414
|
+
}, (observation) => {
|
|
1415
|
+
if (!args.observe) return;
|
|
1416
|
+
args.observe({
|
|
1417
|
+
...observation,
|
|
1418
|
+
sequence: ++observationSequence
|
|
1419
|
+
});
|
|
1420
|
+
}, proposedCandidateHashes).finally(() => {
|
|
1421
|
+
controller.signal.removeEventListener("abort", abortRequest);
|
|
1422
|
+
activeControllers.delete(controller);
|
|
1423
|
+
activeHandlers.delete(handler);
|
|
1424
|
+
});
|
|
1425
|
+
activeHandlers.add(handler);
|
|
1426
|
+
handler.catch(() => void 0);
|
|
1427
|
+
});
|
|
1428
|
+
const port = await listenLocal(server);
|
|
1429
|
+
const close = () => {
|
|
1430
|
+
closePromise ??= closeCallbackServer();
|
|
1431
|
+
return closePromise;
|
|
1432
|
+
};
|
|
1433
|
+
const onAbort = () => {
|
|
1434
|
+
close().catch(() => void 0);
|
|
1435
|
+
};
|
|
1436
|
+
args.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1437
|
+
if (args.signal?.aborted) onAbort();
|
|
1438
|
+
return {
|
|
1439
|
+
url: `http://127.0.0.1:${port}/evaluate`,
|
|
1440
|
+
token: args.token,
|
|
1441
|
+
evaluations: () => evaluations,
|
|
1442
|
+
close
|
|
1443
|
+
};
|
|
1444
|
+
async function closeCallbackServer() {
|
|
1445
|
+
args.signal?.removeEventListener("abort", onAbort);
|
|
1446
|
+
accepting = false;
|
|
1447
|
+
const closingServer = closeServer(server);
|
|
1448
|
+
server.closeIdleConnections?.();
|
|
1449
|
+
for (const controller of activeControllers) controller.abort();
|
|
1450
|
+
const [serverResult] = await Promise.allSettled([closingServer, waitForActiveHandlers(activeHandlers)]);
|
|
1451
|
+
if (activeControllers.size !== 0 || activeHandlers.size !== 0) throw new Error("external optimizer callback closed with active request work");
|
|
1452
|
+
if (serverResult?.status === "rejected") throw serverResult.reason;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async function handleCallback(request, response, signal, args, limits, nextEvaluation, observe, proposedCandidateHashes) {
|
|
1456
|
+
try {
|
|
1457
|
+
if (request.method !== "POST" || request.url !== "/evaluate") {
|
|
1458
|
+
sendJsonIfOpen(response, 404, { error: "not found" });
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (request.headers.authorization !== `Bearer ${args.token}`) {
|
|
1462
|
+
sendJsonIfOpen(response, 401, { error: "unauthorized" });
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
let body;
|
|
1466
|
+
try {
|
|
1467
|
+
body = await readJson(request, limits.maxRequestBytes);
|
|
1468
|
+
} catch {
|
|
1469
|
+
observe({
|
|
1470
|
+
kind: "refusal",
|
|
1471
|
+
reason: "invalid-request"
|
|
1472
|
+
});
|
|
1473
|
+
sendJsonIfOpen(response, 400, { error: "request body must be bounded valid JSON" });
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (!isRecord(body) || !isExternalTextCandidate(body.candidate) || typeof body.exampleId !== "string") {
|
|
1477
|
+
observe({
|
|
1478
|
+
kind: "refusal",
|
|
1479
|
+
reason: "invalid-request"
|
|
1480
|
+
});
|
|
1481
|
+
sendJsonIfOpen(response, 400, { error: "candidate and exampleId are required strings" });
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const candidate = cloneCandidate(body.candidate);
|
|
1485
|
+
const candidateHash = contentHash({
|
|
1486
|
+
kind: "external-text-candidate",
|
|
1487
|
+
candidate
|
|
1488
|
+
});
|
|
1489
|
+
if (!proposedCandidateHashes.has(candidateHash)) {
|
|
1490
|
+
observe({
|
|
1491
|
+
kind: "proposal",
|
|
1492
|
+
candidate,
|
|
1493
|
+
candidateHash
|
|
1494
|
+
});
|
|
1495
|
+
proposedCandidateHashes.add(candidateHash);
|
|
1496
|
+
}
|
|
1497
|
+
const count = nextEvaluation();
|
|
1498
|
+
if (count === void 0) {
|
|
1499
|
+
observe({
|
|
1500
|
+
kind: "refusal",
|
|
1501
|
+
reason: "evaluation-limit",
|
|
1502
|
+
candidate,
|
|
1503
|
+
candidateHash,
|
|
1504
|
+
exampleId: body.exampleId
|
|
1505
|
+
});
|
|
1506
|
+
sendJsonIfOpen(response, 429, { error: "evaluation limit reached" });
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
let result;
|
|
1510
|
+
try {
|
|
1511
|
+
result = await args.evaluate({
|
|
1512
|
+
candidate,
|
|
1513
|
+
exampleId: body.exampleId
|
|
1514
|
+
}, signal);
|
|
1515
|
+
} catch {
|
|
1516
|
+
observe({
|
|
1517
|
+
kind: "refusal",
|
|
1518
|
+
reason: "evaluation-failed",
|
|
1519
|
+
candidate,
|
|
1520
|
+
candidateHash,
|
|
1521
|
+
exampleId: body.exampleId
|
|
1522
|
+
});
|
|
1523
|
+
sendJsonIfOpen(response, 500, { error: "evaluation failed" });
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
let encoded;
|
|
1527
|
+
try {
|
|
1528
|
+
encoded = encodeJsonResponse(result);
|
|
1529
|
+
} catch {
|
|
1530
|
+
observe({
|
|
1531
|
+
kind: "refusal",
|
|
1532
|
+
reason: "evaluation-failed",
|
|
1533
|
+
candidate,
|
|
1534
|
+
candidateHash,
|
|
1535
|
+
exampleId: body.exampleId
|
|
1536
|
+
});
|
|
1537
|
+
sendJsonIfOpen(response, 500, { error: "evaluation response must be valid JSON" });
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
if (Buffer.byteLength(encoded) > limits.maxResponseBytes) {
|
|
1541
|
+
observe({
|
|
1542
|
+
kind: "refusal",
|
|
1543
|
+
reason: "evaluation-failed",
|
|
1544
|
+
candidate,
|
|
1545
|
+
candidateHash,
|
|
1546
|
+
exampleId: body.exampleId
|
|
1547
|
+
});
|
|
1548
|
+
sendJsonIfOpen(response, 413, { error: "evaluation response too large" });
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
observe({
|
|
1552
|
+
kind: "evaluation",
|
|
1553
|
+
candidate,
|
|
1554
|
+
candidateHash,
|
|
1555
|
+
exampleId: body.exampleId,
|
|
1556
|
+
evaluationNumber: count,
|
|
1557
|
+
response: structuredClone(result)
|
|
1558
|
+
});
|
|
1559
|
+
sendEncodedJsonIfOpen(response, 200, encoded);
|
|
1560
|
+
} catch {
|
|
1561
|
+
sendJsonIfOpen(response, 500, { error: "evaluation failed" });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
async function waitForActiveHandlers(activeHandlers) {
|
|
1565
|
+
while (activeHandlers.size > 0) await Promise.allSettled([...activeHandlers]);
|
|
1566
|
+
}
|
|
1567
|
+
function sendJsonIfOpen(response, status, body) {
|
|
1568
|
+
if (response.destroyed || response.writableEnded) return;
|
|
1569
|
+
sendJson(response, status, body);
|
|
1570
|
+
}
|
|
1571
|
+
function readJson(request, maxRequestBytes) {
|
|
1572
|
+
return new Promise((resolvePromise, reject) => {
|
|
1573
|
+
let size = 0;
|
|
1574
|
+
const chunks = [];
|
|
1575
|
+
request.on("data", (chunk) => {
|
|
1576
|
+
size += chunk.length;
|
|
1577
|
+
if (size > maxRequestBytes) {
|
|
1578
|
+
reject(/* @__PURE__ */ new Error("callback body too large"));
|
|
1579
|
+
request.destroy();
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
chunks.push(chunk);
|
|
1583
|
+
});
|
|
1584
|
+
request.on("error", reject);
|
|
1585
|
+
request.on("end", () => {
|
|
1586
|
+
try {
|
|
1587
|
+
resolvePromise(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
reject(error);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
function encodeJsonResponse(value) {
|
|
1595
|
+
const encoded = JSON.stringify(value);
|
|
1596
|
+
if (encoded === void 0) throw new Error("evaluation response must be JSON-serializable");
|
|
1597
|
+
return encoded;
|
|
1598
|
+
}
|
|
1599
|
+
function sendEncodedJsonIfOpen(response, status, encoded) {
|
|
1600
|
+
if (response.destroyed || response.writableEnded) return;
|
|
1601
|
+
response.writeHead(status, {
|
|
1602
|
+
"content-type": "application/json; charset=utf-8",
|
|
1603
|
+
"content-length": String(Buffer.byteLength(encoded))
|
|
1604
|
+
});
|
|
1605
|
+
response.end(encoded);
|
|
1606
|
+
}
|
|
1607
|
+
function assertCallbackConfig(args) {
|
|
1608
|
+
if (typeof args.token !== "string" || !args.token.trim()) throw new Error("external optimizer callback: token must be non-empty");
|
|
1609
|
+
if (!Number.isSafeInteger(args.maxEvaluations) || args.maxEvaluations <= 0) throw new Error("external optimizer callback: maxEvaluations must be a positive safe integer");
|
|
1610
|
+
if (args.acceptEvaluation !== void 0 && typeof args.acceptEvaluation !== "function") throw new Error("external optimizer callback: acceptEvaluation must be a function");
|
|
1611
|
+
if (typeof args.evaluate !== "function") throw new Error("external optimizer callback: evaluate must be a function");
|
|
1612
|
+
if (args.observe !== void 0 && typeof args.observe !== "function") throw new Error("external optimizer callback: observe must be a function");
|
|
1613
|
+
}
|
|
1614
|
+
function cloneCandidate(candidate) {
|
|
1615
|
+
return typeof candidate === "string" ? candidate : { ...candidate };
|
|
1616
|
+
}
|
|
1617
|
+
//#endregion
|
|
1618
|
+
//#region src/campaign/search-ledger-errors.ts
|
|
1619
|
+
/** Base error for invalid search-ledger input or operations. */
|
|
1620
|
+
var SearchLedgerError = class extends ValidationError {};
|
|
1621
|
+
/** Error raised when durable search-ledger data fails an integrity check. */
|
|
1622
|
+
var SearchLedgerIntegrityError = class extends SearchLedgerError {};
|
|
1623
|
+
/** Error raised when an event identifier is reused with different content. */
|
|
1624
|
+
var SearchLedgerConflictError = class extends SearchLedgerError {};
|
|
1625
|
+
//#endregion
|
|
1626
|
+
//#region src/campaign/search-ledger-file.ts
|
|
1627
|
+
/** Campaign binding of the generic journal file layer to search-ledger errors. */
|
|
1628
|
+
const SEARCH_LEDGER_FILE_CONTEXT = {
|
|
1629
|
+
subject: "search ledger",
|
|
1630
|
+
integrityError: (message, options) => new SearchLedgerIntegrityError(message, options)
|
|
1631
|
+
};
|
|
1632
|
+
function appendSearchLedgerLine(path, line) {
|
|
1633
|
+
appendLedgerLine(path, line, SEARCH_LEDGER_FILE_CONTEXT);
|
|
1634
|
+
}
|
|
1635
|
+
function tryWithSearchLedgerFileLock(ledgerPath, run) {
|
|
1636
|
+
return tryWithLedgerFileLock(ledgerPath, SEARCH_LEDGER_FILE_CONTEXT, run);
|
|
1637
|
+
}
|
|
1638
|
+
//#endregion
|
|
1639
|
+
//#region src/campaign/storage.ts
|
|
1640
|
+
/** Node-filesystem storage — the default. Lazily requires `node:fs` so the
|
|
1641
|
+
* module imports cleanly in non-Node runtimes (where the caller passes
|
|
1642
|
+
* `inMemoryCampaignStorage` instead and never constructs this).
|
|
1643
|
+
*
|
|
1644
|
+
* `createRequire(import.meta.url)` is the ESM-native lazy require — a bare
|
|
1645
|
+
* `require` is a ReferenceError under `"type": "module"`, which is exactly
|
|
1646
|
+
* the shape this package publishes. */
|
|
1647
|
+
function fsCampaignStorage() {
|
|
1648
|
+
const { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } = createRequire(import.meta.url)("node:fs");
|
|
1649
|
+
return {
|
|
1650
|
+
ensureDir(dir) {
|
|
1651
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1652
|
+
},
|
|
1653
|
+
exists(path) {
|
|
1654
|
+
return existsSync(path);
|
|
1655
|
+
},
|
|
1656
|
+
read(path) {
|
|
1657
|
+
try {
|
|
1658
|
+
return readFileSync(path, "utf8");
|
|
1659
|
+
} catch {
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
},
|
|
1663
|
+
write(path, content) {
|
|
1664
|
+
writeFileSync(path, content);
|
|
1665
|
+
},
|
|
1666
|
+
append(path, content, expectedBytes) {
|
|
1667
|
+
const result = tryWithSearchLedgerFileLock(path, () => {
|
|
1668
|
+
let actualBytes = 0;
|
|
1669
|
+
try {
|
|
1670
|
+
actualBytes = statSync(path).size;
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
if (error.code !== "ENOENT") throw error;
|
|
1673
|
+
}
|
|
1674
|
+
if (actualBytes !== expectedBytes) return void 0;
|
|
1675
|
+
appendSearchLedgerLine(path, content);
|
|
1676
|
+
return expectedBytes + Buffer.byteLength(content);
|
|
1677
|
+
});
|
|
1678
|
+
return result.acquired ? result.value : void 0;
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
/** In-memory storage for filesystem-less runtimes. Artifacts + trace spans
|
|
1683
|
+
* live in a `Map` for the duration of the run; the `CampaignResult` is
|
|
1684
|
+
* fully populated, but nothing is persisted to disk. */
|
|
1685
|
+
function inMemoryCampaignStorage() {
|
|
1686
|
+
const files = /* @__PURE__ */ new Map();
|
|
1687
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
1688
|
+
return {
|
|
1689
|
+
ensureDir(dir) {
|
|
1690
|
+
dirs.add(dir);
|
|
1691
|
+
},
|
|
1692
|
+
exists(path) {
|
|
1693
|
+
return files.has(path) || dirs.has(path);
|
|
1694
|
+
},
|
|
1695
|
+
read(path) {
|
|
1696
|
+
const value = files.get(path);
|
|
1697
|
+
if (value === void 0) return void 0;
|
|
1698
|
+
return typeof value === "string" ? value : new TextDecoder().decode(value);
|
|
1699
|
+
},
|
|
1700
|
+
write(path, content) {
|
|
1701
|
+
files.set(path, content);
|
|
1702
|
+
},
|
|
1703
|
+
append(path, content, expectedBytes) {
|
|
1704
|
+
const current = files.get(path);
|
|
1705
|
+
const currentText = current === void 0 ? "" : typeof current === "string" ? current : new TextDecoder().decode(current);
|
|
1706
|
+
const currentBytes = new TextEncoder().encode(currentText).byteLength;
|
|
1707
|
+
if (currentBytes !== expectedBytes) return void 0;
|
|
1708
|
+
files.set(path, `${currentText}${content}`);
|
|
1709
|
+
return currentBytes + new TextEncoder().encode(content).byteLength;
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
/** Open the durable spend account stored beside a logical run. */
|
|
1714
|
+
function createRunCostLedger(input) {
|
|
1715
|
+
const path = join(input.runDir, "cost-ledger.jsonl");
|
|
1716
|
+
if (input.ensureRunDir ?? true) input.storage.ensureDir(input.runDir);
|
|
1717
|
+
return new CostLedger({
|
|
1718
|
+
costCeilingUsd: input.costCeilingUsd,
|
|
1719
|
+
persistence: {
|
|
1720
|
+
read: () => {
|
|
1721
|
+
const stored = input.storage.read(path);
|
|
1722
|
+
if (stored === void 0 && input.storage.exists(path)) throw new Error(`CostLedger: cannot read existing event log '${path}'`);
|
|
1723
|
+
const events = stored ?? "";
|
|
1724
|
+
return {
|
|
1725
|
+
revision: String(new TextEncoder().encode(events).byteLength),
|
|
1726
|
+
events
|
|
1727
|
+
};
|
|
1728
|
+
},
|
|
1729
|
+
append: (expectedRevision, event) => {
|
|
1730
|
+
const expectedBytes = Number(expectedRevision);
|
|
1731
|
+
if (!Number.isSafeInteger(expectedBytes) || expectedBytes < 0) throw new Error(`CostLedger: invalid storage revision '${expectedRevision}'`);
|
|
1732
|
+
const next = input.storage.append(path, event, expectedBytes);
|
|
1733
|
+
return next === void 0 ? void 0 : String(next);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
//#endregion
|
|
1739
|
+
//#region src/campaign/single-run-lock.ts
|
|
1740
|
+
/**
|
|
1741
|
+
* Single-run lock for evaluations that share one mutable environment.
|
|
1742
|
+
*
|
|
1743
|
+
* Two concurrent runs against a shared stateful gym silently corrupt each
|
|
1744
|
+
* other: each resets/mutates environment state mid-cell of the other, and
|
|
1745
|
+
* every score from both becomes garbage that LOOKS like worker variance
|
|
1746
|
+
* (agent-lab R357 burned hours on flip-flopping scores before tracing them
|
|
1747
|
+
* to exactly this). The fix is a pid lockfile: refuse to start while a live
|
|
1748
|
+
* holder exists, reclaim stale locks whose pid is gone, release only if the
|
|
1749
|
+
* lock is still ours.
|
|
1750
|
+
*
|
|
1751
|
+
* `alsoCheck` exists because independent runners can guard the same shared
|
|
1752
|
+
* resource with differently named lockfiles; a runner must respect all of
|
|
1753
|
+
* them even though it writes only its own.
|
|
1754
|
+
*/
|
|
1755
|
+
function assertAvailable(path) {
|
|
1756
|
+
const unavailable = probeAtomicFileLock({ lockPath: path });
|
|
1757
|
+
if (unavailable) throw unavailableError(path, unavailable);
|
|
1758
|
+
}
|
|
1759
|
+
function unavailableError(path, unavailable) {
|
|
1760
|
+
if (unavailable.reason === "recovery") return /* @__PURE__ */ new Error(`single-run lock recovery is already in progress (${path}); refusing a concurrent run`);
|
|
1761
|
+
return /* @__PURE__ */ new Error(`single-run lock held by live pid ${unavailable.holder.pid} (${path}); refusing a concurrent run on the shared resource`);
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Acquire the lock or throw naming the live holder. A stale lock (holder pid
|
|
1765
|
+
* no longer running) is reclaimed by one contender. An interrupted reclaim
|
|
1766
|
+
* leaves a marker that fails closed instead of admitting overlapping runs.
|
|
1767
|
+
*/
|
|
1768
|
+
function acquireSingleRunLock(opts) {
|
|
1769
|
+
const pid = opts.pid ?? process.pid;
|
|
1770
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("single-run lock pid must be a positive integer");
|
|
1771
|
+
for (const path of opts.alsoCheck ?? []) assertAvailable(path);
|
|
1772
|
+
const acquisition = tryAcquireAtomicFileLock({
|
|
1773
|
+
lockPath: opts.lockPath,
|
|
1774
|
+
pid
|
|
1775
|
+
});
|
|
1776
|
+
if (!acquisition.acquired) throw unavailableError(opts.lockPath, acquisition);
|
|
1777
|
+
const releaseOnExit = opts.releaseOnExit ?? true;
|
|
1778
|
+
let released = false;
|
|
1779
|
+
const release = () => {
|
|
1780
|
+
if (released) return;
|
|
1781
|
+
released = true;
|
|
1782
|
+
if (releaseOnExit) process.off("exit", release);
|
|
1783
|
+
try {
|
|
1784
|
+
acquisition.lock.release();
|
|
1785
|
+
} catch {}
|
|
1786
|
+
};
|
|
1787
|
+
try {
|
|
1788
|
+
for (const path of opts.alsoCheck ?? []) assertAvailable(path);
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
release();
|
|
1791
|
+
throw error;
|
|
1792
|
+
}
|
|
1793
|
+
if (releaseOnExit) process.on("exit", release);
|
|
1794
|
+
return { release };
|
|
1795
|
+
}
|
|
1796
|
+
//#endregion
|
|
1797
|
+
export { removeCredentialEnvironment as A, DEFAULT_EXTERNAL_OPTIMIZER_PROCESS_LIMITS as C, isCandidateText as D, assertNoCredentialValues as E, resolveExternalOptimizerProcessLimits as M, safePathComponent as N, isExternalTextCandidate as O, DEFAULT_EXTERNAL_OPTIMIZER_CALLBACK_LIMITS as S, assertJsonValue as T, cachedJudge as _, SEARCH_LEDGER_FILE_CONTEXT as a, fileVerdictCache as b, SearchLedgerIntegrityError as c, closeExternalOptimizerResources as d, runWithCleanup as f, sendJson as g, listenLocal as h, inMemoryCampaignStorage as i, resolveExternalOptimizerCallbackLimits as j, isRecord as k, startExternalOptimizerCallback as l, closeServer as m, createRunCostLedger as n, SearchLedgerConflictError as o, startExternalOptimizerModelProxy as p, fsCampaignStorage as r, SearchLedgerError as s, acquireSingleRunLock as t, runExternalOptimizerProcess as u, canonicalJson as v, assertExternalOptimizerModelBudget as w, inMemoryVerdictCache as x, contentHash as y };
|
|
1798
|
+
|
|
1799
|
+
//# sourceMappingURL=single-run-lock-B-cFnnNn.js.map
|