@axlsdk/eval 0.25.2 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { Provider, Effort, ArtifactManifest, CapturedRequestRecord, Accounting, AxlEvent, AxlRuntime } from '@axlsdk/axl';
2
+ import { FoldedOperationRecord, ModelCallRecord, ToolCallMessage, FinishReason, EvalExecuteWorkflow, Provider, Effort, ArtifactManifest, CapturedRequestRecord, Accounting, AxlEvent, AxlRuntime } from '@axlsdk/axl';
3
3
 
4
4
  type DatasetItem<TInput, TAnnotations = undefined> = {
5
5
  input: TInput;
@@ -60,6 +60,46 @@ type Dataset<TInput = unknown, TAnnotations = unknown> = {
60
60
  };
61
61
  declare function dataset<TInput extends z.ZodType, TAnnotations extends z.ZodType>(config: DatasetConfig<TInput, TAnnotations>): Dataset<z.infer<TInput>, z.infer<TAnnotations>>;
62
62
 
63
+ type ModelCallInput = {
64
+ request?: ModelCallRecord['request'];
65
+ ids: ModelCallRecord['ids'];
66
+ attributes: ModelCallRecord['attributes'];
67
+ recorded: ModelCallRecord;
68
+ };
69
+ type ModelCallDatasetOptions<TAnnotations> = {
70
+ /** Runs after validation, including on external operations. */
71
+ filter?: (record: FoldedOperationRecord) => boolean;
72
+ /** Attach ground truth for scorers without changing their signature. */
73
+ annotate?: (record: ModelCallRecord, index: number) => TAnnotations;
74
+ };
75
+ type ReplayModelCallOptions = {
76
+ model?: string;
77
+ };
78
+ /**
79
+ * The `output` both executors return: one model response, field-named like a
80
+ * recorded `ok` outcome's `response` so a scorer can compare a replay with
81
+ * `input.recorded` directly. Tool calls are part of the answer, not dropped.
82
+ */
83
+ type ModelCallOutput = {
84
+ content: string;
85
+ /** Parsed `content`, present when the request asked for JSON and the model answered in text. */
86
+ json?: unknown;
87
+ thinking?: string;
88
+ tool_calls?: ToolCallMessage[];
89
+ finishReason?: FinishReason;
90
+ rawFinishReason?: string;
91
+ responseId?: string;
92
+ responseModel?: string;
93
+ };
94
+ /** Read v2 records or v1 phase journals without dropping external operations. */
95
+ declare function loadModelCalls(source: string | readonly unknown[]): Promise<FoldedOperationRecord[]>;
96
+ /** Create an ordinary Dataset for runEval/defineEval; external operations require an explicit filter. */
97
+ declare function modelCallDataset<TAnnotations = undefined>(records: readonly FoldedOperationRecord[], options?: ModelCallDatasetOptions<TAnnotations>): Dataset<ModelCallInput, TAnnotations>;
98
+ /** Rescore captured output (a {@link ModelCallOutput}) without calling a provider or returning historical cost. */
99
+ declare function recordedModelCall(): EvalExecuteWorkflow;
100
+ /** Replay one captured provider call into a {@link ModelCallOutput}; returned tool calls are not executed. */
101
+ declare function replayModelCall(options?: ReplayModelCallOptions): EvalExecuteWorkflow;
102
+
63
103
  /** Result from a scorer that includes metadata beyond the numeric score. */
64
104
  type ScorerResult = {
65
105
  score: number;
@@ -203,7 +243,7 @@ type RequestRecord = CapturedRequestRecord;
203
243
  */
204
244
  type OperationRef = {
205
245
  operationId: string;
206
- kind: 'chat' | 'stream';
246
+ kind: 'chat' | 'stream' | 'external';
207
247
  turn?: number;
208
248
  attempt?: number;
209
249
  status: 'recorded' | 'start_only' | 'truncated' | 'omitted';
@@ -1288,4 +1328,4 @@ declare function isBudgetStopped(summary: {
1288
1328
  */
1289
1329
  declare function aggregateAccounting(inputs: readonly Accounting[]): Accounting;
1290
1330
 
1291
- export { type BootstrapCIResult, type CaptureRequestsOption, DEFAULT_COPY_MAX_BYTES, DEFAULT_SIDECAR_MAX_BYTES, type Dataset, type DatasetConfig, type DatasetItem, type DegradedScorer, type DiagnosticManifest, type EvalAccounting, type EvalBudgetStatus, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalCoverage, type EvalImprovement, type EvalItem, type EvalItemFailure, type EvalItemOutcome, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type ExtraKeyPolicy, type ItemErrorRate, type ItemModelTiming, type LlmScorerConfig, type ModelTimingStats, type MultiRunSummary, type OperationRef, type RequestRecord, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerApplies, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerOutcome, type ScorerResult, type ScorerToleranceVerdict, type SidecarValidation, aggregateAccounting, aggregateRuns, dataset, defineEval, evalCompare, evaluateItemErrorRateGate, evaluateScorerErrorRateGate, evaluateScorerTolerance, isBudgetStopped, llmScorer, normalizeScorerResult, pairedBootstrapCI, parseRequestRecords, readAccounting, refusedWork, rescore, resolveCaptureLimits, runEval, scorer, serializeRequestRecords, toDiagnosticManifest, unavailableManifest, validateRequestSidecar };
1331
+ export { type BootstrapCIResult, type CaptureRequestsOption, DEFAULT_COPY_MAX_BYTES, DEFAULT_SIDECAR_MAX_BYTES, type Dataset, type DatasetConfig, type DatasetItem, type DegradedScorer, type DiagnosticManifest, type EvalAccounting, type EvalBudgetStatus, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalCoverage, type EvalImprovement, type EvalItem, type EvalItemFailure, type EvalItemOutcome, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type ExtraKeyPolicy, type ItemErrorRate, type ItemModelTiming, type LlmScorerConfig, type ModelCallDatasetOptions, type ModelCallInput, type ModelCallOutput, type ModelTimingStats, type MultiRunSummary, type OperationRef, type ReplayModelCallOptions, type RequestRecord, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerApplies, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerOutcome, type ScorerResult, type ScorerToleranceVerdict, type SidecarValidation, aggregateAccounting, aggregateRuns, dataset, defineEval, evalCompare, evaluateItemErrorRateGate, evaluateScorerErrorRateGate, evaluateScorerTolerance, isBudgetStopped, llmScorer, loadModelCalls, modelCallDataset, normalizeScorerResult, pairedBootstrapCI, parseRequestRecords, readAccounting, recordedModelCall, refusedWork, replayModelCall, rescore, resolveCaptureLimits, runEval, scorer, serializeRequestRecords, toDiagnosticManifest, unavailableManifest, validateRequestSidecar };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { Provider, Effort, ArtifactManifest, CapturedRequestRecord, Accounting, AxlEvent, AxlRuntime } from '@axlsdk/axl';
2
+ import { FoldedOperationRecord, ModelCallRecord, ToolCallMessage, FinishReason, EvalExecuteWorkflow, Provider, Effort, ArtifactManifest, CapturedRequestRecord, Accounting, AxlEvent, AxlRuntime } from '@axlsdk/axl';
3
3
 
4
4
  type DatasetItem<TInput, TAnnotations = undefined> = {
5
5
  input: TInput;
@@ -60,6 +60,46 @@ type Dataset<TInput = unknown, TAnnotations = unknown> = {
60
60
  };
61
61
  declare function dataset<TInput extends z.ZodType, TAnnotations extends z.ZodType>(config: DatasetConfig<TInput, TAnnotations>): Dataset<z.infer<TInput>, z.infer<TAnnotations>>;
62
62
 
63
+ type ModelCallInput = {
64
+ request?: ModelCallRecord['request'];
65
+ ids: ModelCallRecord['ids'];
66
+ attributes: ModelCallRecord['attributes'];
67
+ recorded: ModelCallRecord;
68
+ };
69
+ type ModelCallDatasetOptions<TAnnotations> = {
70
+ /** Runs after validation, including on external operations. */
71
+ filter?: (record: FoldedOperationRecord) => boolean;
72
+ /** Attach ground truth for scorers without changing their signature. */
73
+ annotate?: (record: ModelCallRecord, index: number) => TAnnotations;
74
+ };
75
+ type ReplayModelCallOptions = {
76
+ model?: string;
77
+ };
78
+ /**
79
+ * The `output` both executors return: one model response, field-named like a
80
+ * recorded `ok` outcome's `response` so a scorer can compare a replay with
81
+ * `input.recorded` directly. Tool calls are part of the answer, not dropped.
82
+ */
83
+ type ModelCallOutput = {
84
+ content: string;
85
+ /** Parsed `content`, present when the request asked for JSON and the model answered in text. */
86
+ json?: unknown;
87
+ thinking?: string;
88
+ tool_calls?: ToolCallMessage[];
89
+ finishReason?: FinishReason;
90
+ rawFinishReason?: string;
91
+ responseId?: string;
92
+ responseModel?: string;
93
+ };
94
+ /** Read v2 records or v1 phase journals without dropping external operations. */
95
+ declare function loadModelCalls(source: string | readonly unknown[]): Promise<FoldedOperationRecord[]>;
96
+ /** Create an ordinary Dataset for runEval/defineEval; external operations require an explicit filter. */
97
+ declare function modelCallDataset<TAnnotations = undefined>(records: readonly FoldedOperationRecord[], options?: ModelCallDatasetOptions<TAnnotations>): Dataset<ModelCallInput, TAnnotations>;
98
+ /** Rescore captured output (a {@link ModelCallOutput}) without calling a provider or returning historical cost. */
99
+ declare function recordedModelCall(): EvalExecuteWorkflow;
100
+ /** Replay one captured provider call into a {@link ModelCallOutput}; returned tool calls are not executed. */
101
+ declare function replayModelCall(options?: ReplayModelCallOptions): EvalExecuteWorkflow;
102
+
63
103
  /** Result from a scorer that includes metadata beyond the numeric score. */
64
104
  type ScorerResult = {
65
105
  score: number;
@@ -203,7 +243,7 @@ type RequestRecord = CapturedRequestRecord;
203
243
  */
204
244
  type OperationRef = {
205
245
  operationId: string;
206
- kind: 'chat' | 'stream';
246
+ kind: 'chat' | 'stream' | 'external';
207
247
  turn?: number;
208
248
  attempt?: number;
209
249
  status: 'recorded' | 'start_only' | 'truncated' | 'omitted';
@@ -1288,4 +1328,4 @@ declare function isBudgetStopped(summary: {
1288
1328
  */
1289
1329
  declare function aggregateAccounting(inputs: readonly Accounting[]): Accounting;
1290
1330
 
1291
- export { type BootstrapCIResult, type CaptureRequestsOption, DEFAULT_COPY_MAX_BYTES, DEFAULT_SIDECAR_MAX_BYTES, type Dataset, type DatasetConfig, type DatasetItem, type DegradedScorer, type DiagnosticManifest, type EvalAccounting, type EvalBudgetStatus, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalCoverage, type EvalImprovement, type EvalItem, type EvalItemFailure, type EvalItemOutcome, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type ExtraKeyPolicy, type ItemErrorRate, type ItemModelTiming, type LlmScorerConfig, type ModelTimingStats, type MultiRunSummary, type OperationRef, type RequestRecord, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerApplies, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerOutcome, type ScorerResult, type ScorerToleranceVerdict, type SidecarValidation, aggregateAccounting, aggregateRuns, dataset, defineEval, evalCompare, evaluateItemErrorRateGate, evaluateScorerErrorRateGate, evaluateScorerTolerance, isBudgetStopped, llmScorer, normalizeScorerResult, pairedBootstrapCI, parseRequestRecords, readAccounting, refusedWork, rescore, resolveCaptureLimits, runEval, scorer, serializeRequestRecords, toDiagnosticManifest, unavailableManifest, validateRequestSidecar };
1331
+ export { type BootstrapCIResult, type CaptureRequestsOption, DEFAULT_COPY_MAX_BYTES, DEFAULT_SIDECAR_MAX_BYTES, type Dataset, type DatasetConfig, type DatasetItem, type DegradedScorer, type DiagnosticManifest, type EvalAccounting, type EvalBudgetStatus, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalCoverage, type EvalImprovement, type EvalItem, type EvalItemFailure, type EvalItemOutcome, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type ExtraKeyPolicy, type ItemErrorRate, type ItemModelTiming, type LlmScorerConfig, type ModelCallDatasetOptions, type ModelCallInput, type ModelCallOutput, type ModelTimingStats, type MultiRunSummary, type OperationRef, type ReplayModelCallOptions, type RequestRecord, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerApplies, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerOutcome, type ScorerResult, type ScorerToleranceVerdict, type SidecarValidation, aggregateAccounting, aggregateRuns, dataset, defineEval, evalCompare, evaluateItemErrorRateGate, evaluateScorerErrorRateGate, evaluateScorerTolerance, isBudgetStopped, llmScorer, loadModelCalls, modelCallDataset, normalizeScorerResult, pairedBootstrapCI, parseRequestRecords, readAccounting, recordedModelCall, refusedWork, replayModelCall, rescore, resolveCaptureLimits, runEval, scorer, serializeRequestRecords, toDiagnosticManifest, unavailableManifest, validateRequestSidecar };
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  toDiagnosticManifest,
23
23
  unavailableManifest,
24
24
  validateRequestSidecar
25
- } from "./chunk-F4MJ26YK.js";
25
+ } from "./chunk-3XLPDV3O.js";
26
26
 
27
27
  // src/dataset.ts
28
28
  import { readFile } from "fs/promises";
@@ -116,8 +116,409 @@ function dataset(config) {
116
116
  };
117
117
  }
118
118
 
119
- // src/llm-scorer.ts
119
+ // src/model-calls.ts
120
+ import { readFile as readFile2 } from "fs/promises";
120
121
  import { z } from "zod";
122
+ import {
123
+ AxlError,
124
+ capturedJournalLineSchema,
125
+ foldJournal,
126
+ foldedOperationRecordSchema,
127
+ modelCallRecordSchema
128
+ } from "@axlsdk/axl";
129
+ function invalid(location, message) {
130
+ throw new AxlError("INVALID_MODEL_CALL_SOURCE", `${location}: ${message}`);
131
+ }
132
+ function versionOf(value) {
133
+ return value !== null && typeof value === "object" ? Object.getOwnPropertyDescriptor(value, "v")?.value : void 0;
134
+ }
135
+ function parseJson(value, location) {
136
+ try {
137
+ return JSON.parse(value);
138
+ } catch {
139
+ return invalid(location, "invalid JSON");
140
+ }
141
+ }
142
+ function arrayLength(source, location) {
143
+ let array;
144
+ let descriptor;
145
+ try {
146
+ array = Array.isArray(source);
147
+ if (array) descriptor = Object.getOwnPropertyDescriptor(source, "length");
148
+ } catch {
149
+ return invalid(location, "array could not be inspected");
150
+ }
151
+ if (!array || !descriptor || !("value" in descriptor)) invalid(location, "expected an array");
152
+ const length = descriptor.value;
153
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0)
154
+ invalid(location, "invalid array length");
155
+ return length;
156
+ }
157
+ function arraySlot(source, index, location) {
158
+ let descriptor;
159
+ try {
160
+ descriptor = Object.getOwnPropertyDescriptor(source, String(index));
161
+ } catch {
162
+ return invalid(location, "array entry could not be inspected");
163
+ }
164
+ if (!descriptor || !("value" in descriptor))
165
+ invalid(location, "array entry is missing or is an accessor");
166
+ return descriptor.value;
167
+ }
168
+ function* locateArray(source, at) {
169
+ const length = arrayLength(source, "source");
170
+ for (let index = 0; index < length; index++) {
171
+ const location = at(index);
172
+ yield { value: arraySlot(source, index, location), position: index, location };
173
+ }
174
+ }
175
+ async function loadModelCalls(source) {
176
+ let located;
177
+ if (typeof source === "string") {
178
+ const content = (await readFile2(source, "utf8")).replace(/^\uFEFF/, "");
179
+ if (content.trimStart().startsWith("[")) {
180
+ const parsed = parseJson(content, `file ${source}`);
181
+ if (!Array.isArray(parsed)) invalid(`file ${source}`, "expected a JSON array");
182
+ located = locateArray(parsed, (index) => `entry ${index + 1} of ${source}`);
183
+ } else {
184
+ const rows = content.split(/\r?\n/);
185
+ if (content.endsWith("\n")) rows.pop();
186
+ located = rows.flatMap((row, index) => {
187
+ const location = `line ${index + 1} of ${source}`;
188
+ if (row.trim() === "") return [];
189
+ return [{ value: parseJson(row, location), position: index, location }];
190
+ });
191
+ }
192
+ } else {
193
+ located = locateArray(source, (index) => `item ${index + 1}`);
194
+ }
195
+ const journal = [];
196
+ const folded = [];
197
+ const firstJournal = /* @__PURE__ */ new Map();
198
+ let format;
199
+ for (const entry of located) {
200
+ let version;
201
+ try {
202
+ version = versionOf(entry.value);
203
+ } catch {
204
+ invalid(entry.location, "record version could not be inspected");
205
+ }
206
+ if (version === 1 || version === 2) {
207
+ if (format !== void 0 && format !== version)
208
+ invalid(entry.location, "one source cannot mix v1 journal lines with v2 records");
209
+ format = version;
210
+ }
211
+ if (version === 1) {
212
+ const parsed = capturedJournalLineSchema.safeParse(entry.value);
213
+ if (!parsed.success) invalid(entry.location, parsed.error.message);
214
+ journal.push({ ...entry, value: parsed.data, operationId: parsed.data.operationId });
215
+ if (!firstJournal.has(parsed.data.operationId))
216
+ firstJournal.set(parsed.data.operationId, entry);
217
+ } else if (version === 2) {
218
+ const parsed = foldedOperationRecordSchema.safeParse(entry.value);
219
+ if (!parsed.success) invalid(entry.location, parsed.error.message);
220
+ folded.push({ record: parsed.data, location: entry.location, position: entry.position });
221
+ } else {
222
+ invalid(entry.location, "unsupported capture version");
223
+ }
224
+ }
225
+ if (journal.length) {
226
+ let records;
227
+ try {
228
+ records = foldJournal(journal.map((entry) => entry.value));
229
+ } catch (error) {
230
+ const message = error instanceof Error ? error.message : String(error);
231
+ const line = /\bat line (\d+)\b/.exec(message) ?? /^Invalid capture journal line (\d+):/.exec(message);
232
+ const indexed = line ? journal[Number(line[1]) - 1] : void 0;
233
+ const matched = indexed ?? [...journal].reverse().find((entry) => {
234
+ const marker = `capture operation ${entry.operationId}`;
235
+ const at = message.indexOf(marker);
236
+ if (at < 0) return false;
237
+ const suffix = message.slice(at + marker.length);
238
+ return suffix === "" || suffix.startsWith(":") || suffix.startsWith(" at line");
239
+ });
240
+ invalid(matched?.location ?? journal[0].location, message);
241
+ }
242
+ for (const record of records) {
243
+ const first = firstJournal.get(record.id);
244
+ folded.push({ record, location: first.location, position: first.position });
245
+ }
246
+ }
247
+ folded.sort((a, b) => a.position - b.position);
248
+ const seen = /* @__PURE__ */ new Map();
249
+ for (const item of folded) {
250
+ const previous = seen.get(item.record.id);
251
+ if (previous)
252
+ invalid(item.location, `duplicate operation id ${item.record.id} (first at ${previous})`);
253
+ seen.set(item.record.id, item.location);
254
+ }
255
+ return folded.map((item) => item.record);
256
+ }
257
+ var modelCallInputSchema = z.object({ recorded: modelCallRecordSchema }).transform(
258
+ ({ recorded }) => ({
259
+ ...recorded.request ? { request: recorded.request } : {},
260
+ ids: recorded.ids,
261
+ attributes: recorded.attributes,
262
+ recorded
263
+ })
264
+ );
265
+ function modelCallDataset(records, options) {
266
+ const items = [];
267
+ const length = arrayLength(records, "records");
268
+ for (let index = 0; index < length; index++) {
269
+ const parsed = foldedOperationRecordSchema.safeParse(
270
+ arraySlot(records, index, `record ${index + 1}`)
271
+ );
272
+ if (!parsed.success) invalid(`record ${index + 1}`, parsed.error.message);
273
+ const record = parsed.data;
274
+ if (options?.filter && !options.filter(record)) continue;
275
+ if (record.kind === "external")
276
+ throw new AxlError(
277
+ "MODEL_CALL_EXPECTED",
278
+ `record ${index + 1} (${record.id}) is external; supply a filter to exclude it`
279
+ );
280
+ const input = {
281
+ ...record.request ? { request: record.request } : {},
282
+ ids: record.ids,
283
+ attributes: record.attributes,
284
+ recorded: record
285
+ };
286
+ items.push({
287
+ input,
288
+ ...options?.annotate ? { annotations: options.annotate(record, index) } : {}
289
+ });
290
+ }
291
+ return dataset({
292
+ name: "model-calls",
293
+ schema: modelCallInputSchema,
294
+ annotations: z.custom(),
295
+ items
296
+ });
297
+ }
298
+ function readInput(input) {
299
+ const candidate = input !== null && typeof input === "object" ? Object.getOwnPropertyDescriptor(input, "recorded")?.value : void 0;
300
+ if (candidate !== null && typeof candidate === "object" && Object.getOwnPropertyDescriptor(candidate, "kind")?.value === "external")
301
+ throw new AxlError(
302
+ "MODEL_CALL_EXPECTED",
303
+ "external operations cannot be scored or replayed as model calls"
304
+ );
305
+ const parsed = modelCallRecordSchema.safeParse(candidate);
306
+ if (!parsed.success)
307
+ throw new AxlError(
308
+ "INVALID_MODEL_CALL_INPUT",
309
+ `expected a validated model-call input: ${parsed.error.message}`
310
+ );
311
+ return parsed.data;
312
+ }
313
+ function modelCallOutput(response, record) {
314
+ const output = {
315
+ content: response.content,
316
+ ...response.thinking ? { thinking: response.thinking } : {},
317
+ ...response.tool_calls?.length ? { tool_calls: response.tool_calls } : {},
318
+ ...response.finishReason !== void 0 ? { finishReason: response.finishReason } : {},
319
+ ...response.rawFinishReason !== void 0 ? { rawFinishReason: response.rawFinishReason } : {},
320
+ ...response.responseId !== void 0 ? { responseId: response.responseId } : {},
321
+ ...response.responseModel !== void 0 ? { responseModel: response.responseModel } : {}
322
+ };
323
+ const format = record.request?.responseFormat?.type;
324
+ if (format !== "json_object" && format !== "json_schema") return output;
325
+ if (output.content === "" && output.tool_calls) return output;
326
+ try {
327
+ return { ...output, json: JSON.parse(output.content) };
328
+ } catch {
329
+ throw new AxlError(
330
+ "INVALID_MODEL_CALL_JSON",
331
+ `model call ${record.id} did not return valid JSON`
332
+ );
333
+ }
334
+ }
335
+ function providerOutput(response, record) {
336
+ return modelCallOutput(
337
+ {
338
+ content: response.content,
339
+ ...response.thinking_content !== void 0 ? { thinking: response.thinking_content } : {},
340
+ ...response.tool_calls !== void 0 ? { tool_calls: response.tool_calls } : {},
341
+ ...response.finishReason !== void 0 ? { finishReason: response.finishReason } : {},
342
+ ...response.rawFinishReason !== void 0 ? { rawFinishReason: response.rawFinishReason } : {},
343
+ ...response.responseId !== void 0 ? { responseId: response.responseId } : {},
344
+ ...response.responseModel !== void 0 ? { responseModel: response.responseModel } : {}
345
+ },
346
+ record
347
+ );
348
+ }
349
+ function recordedModelCall() {
350
+ return async (input) => {
351
+ const record = readInput(input);
352
+ if (record.captured.redacted)
353
+ throw new AxlError("RECORDED_OUTPUT_UNAVAILABLE", `model call ${record.id} was redacted`);
354
+ if (record.outcome.status !== "ok" || !("response" in record.outcome))
355
+ throw new AxlError(
356
+ "RECORDED_OUTPUT_UNAVAILABLE",
357
+ `model call ${record.id} has no complete successful output (${record.outcome.status})`
358
+ );
359
+ return { output: modelCallOutput(record.outcome.response, record) };
360
+ };
361
+ }
362
+ function replayRequest(record) {
363
+ const request = record.request;
364
+ if (!request || !record.captured.replayable || record.captured.continuityCaptured !== true || record.captured.redacted || record.captured.truncated || record.captured.omitted.length > 0 || (request.providerOptionKeys?.length ?? 0) > 0)
365
+ throw new AxlError(
366
+ "MODEL_CALL_NOT_REPLAYABLE",
367
+ `model call ${record.id} does not contain an exact replayable request`
368
+ );
369
+ return request;
370
+ }
371
+ function chatOptions(request, model) {
372
+ if (!request.options.model)
373
+ throw new AxlError("INVALID_MODEL_CALL_INPUT", "captured request model is empty");
374
+ const effort = request.options.effort;
375
+ if (effort !== void 0 && !["none", "low", "medium", "high", "xhigh", "max"].includes(effort))
376
+ throw new AxlError("INVALID_MODEL_CALL_INPUT", `unsupported captured effort ${effort}`);
377
+ const toolChoice = request.options.toolChoice;
378
+ if (toolChoice !== void 0 && toolChoice !== "auto" && toolChoice !== "none" && toolChoice !== "required" && !(toolChoice !== null && typeof toolChoice === "object" && !Array.isArray(toolChoice) && toolChoice.type === "function" && Object.keys(toolChoice).every((key) => key === "type" || key === "function") && toolChoice.function !== null && typeof toolChoice.function === "object" && !Array.isArray(toolChoice.function) && typeof toolChoice.function.name === "string" && Object.keys(toolChoice.function).every((key) => key === "name")))
379
+ throw new AxlError("INVALID_MODEL_CALL_INPUT", "unsupported captured tool choice");
380
+ return {
381
+ model,
382
+ ...request.options.temperature !== void 0 ? { temperature: request.options.temperature } : {},
383
+ ...request.options.maxTokens !== void 0 ? { maxTokens: request.options.maxTokens } : {},
384
+ ...request.options.thinkingBudget !== void 0 ? { thinkingBudget: request.options.thinkingBudget } : {},
385
+ ...request.options.includeThoughts !== void 0 ? { includeThoughts: request.options.includeThoughts } : {},
386
+ ...request.options.stop !== void 0 ? { stop: [...request.options.stop] } : {},
387
+ ...request.options.promptCache !== void 0 ? { promptCache: request.options.promptCache } : {},
388
+ ...effort !== void 0 ? { effort } : {},
389
+ ...toolChoice !== void 0 ? { toolChoice } : {},
390
+ ...request.tools ? {
391
+ tools: request.tools.map((tool) => ({
392
+ type: "function",
393
+ function: {
394
+ name: tool.name,
395
+ description: tool.description ?? "",
396
+ parameters: tool.parameters,
397
+ ...tool.strict !== void 0 ? { strict: tool.strict } : {}
398
+ }
399
+ }))
400
+ } : {},
401
+ ...request.responseFormat ? { responseFormat: request.responseFormat } : {}
402
+ };
403
+ }
404
+ function chatMessages(request) {
405
+ return request.messages.map((message) => {
406
+ if (!["system", "user", "assistant", "tool"].includes(message.role))
407
+ throw new AxlError("MODEL_CALL_NOT_REPLAYABLE", "captured message cannot be reconstructed");
408
+ let content;
409
+ if (message.input) {
410
+ const text = message.content ?? "";
411
+ let offset = 0;
412
+ const parts = [];
413
+ for (const part of message.input.parts) {
414
+ if (part.type !== "text" || offset + part.characters > text.length)
415
+ throw new AxlError(
416
+ "MODEL_CALL_NOT_REPLAYABLE",
417
+ "captured content parts cannot be reconstructed"
418
+ );
419
+ parts.push({ type: "text", text: text.slice(offset, offset + part.characters) });
420
+ offset += part.characters;
421
+ }
422
+ if (offset !== text.length)
423
+ throw new AxlError(
424
+ "MODEL_CALL_NOT_REPLAYABLE",
425
+ "captured text lengths do not match content"
426
+ );
427
+ content = parts;
428
+ } else if (message.content !== null) {
429
+ content = message.content;
430
+ } else {
431
+ throw new AxlError("MODEL_CALL_NOT_REPLAYABLE", "captured message content is absent");
432
+ }
433
+ return {
434
+ role: message.role,
435
+ content,
436
+ ...message.name !== void 0 ? { name: message.name } : {},
437
+ ...message.agent !== void 0 ? { agent: message.agent } : {},
438
+ ...message.origin !== void 0 ? { origin: message.origin } : {},
439
+ ...message.tool_calls !== void 0 ? { tool_calls: message.tool_calls } : {},
440
+ ...message.tool_call_id !== void 0 ? { tool_call_id: message.tool_call_id } : {},
441
+ ...message.providerMetadata !== void 0 ? { providerMetadata: message.providerMetadata } : {}
442
+ };
443
+ });
444
+ }
445
+ function replayModelCall(options = {}) {
446
+ return async (input, runtime) => {
447
+ const record = readInput(input);
448
+ const request = replayRequest(record);
449
+ const messages = chatMessages(request);
450
+ const capturedOptions = chatOptions(request, request.options.model);
451
+ if (!runtime)
452
+ throw new AxlError("MODEL_CALL_RUNTIME_REQUIRED", "replay requires an AxlRuntime");
453
+ const uri = options.model ?? `${record.provider}:${request.options.model}`;
454
+ if (!uri.includes(":"))
455
+ throw new AxlError("MODEL_CALL_MODEL_UNRESOLVED", "model override must use provider:model");
456
+ let resolved;
457
+ try {
458
+ resolved = runtime.resolveProvider(uri);
459
+ } catch {
460
+ throw new AxlError(
461
+ "MODEL_CALL_MODEL_UNRESOLVED",
462
+ `cannot resolve ${uri}; supply an explicit provider:model override`
463
+ );
464
+ }
465
+ if (!options.model && resolved.provider.name !== record.provider)
466
+ throw new AxlError(
467
+ "MODEL_CALL_MODEL_UNRESOLVED",
468
+ `resolved ${uri} to a different provider; supply an explicit override`
469
+ );
470
+ const callOptions = {
471
+ ...capturedOptions,
472
+ model: options.model ? resolved.model : request.options.model
473
+ };
474
+ if (record.kind === "chat") {
475
+ const response = await resolved.provider.chat(messages, callOptions);
476
+ return { output: providerOutput(response, record) };
477
+ }
478
+ let content = "";
479
+ let thinking = "";
480
+ const calls = /* @__PURE__ */ new Map();
481
+ let done;
482
+ for await (const chunk of resolved.provider.stream(messages, callOptions)) {
483
+ if (chunk.type === "text_delta") content += chunk.content;
484
+ else if (chunk.type === "thinking_delta") thinking += chunk.content;
485
+ else if (chunk.type === "tool_call_delta") {
486
+ const call = calls.get(chunk.id) ?? { id: chunk.id, name: "", arguments: "" };
487
+ if (chunk.name) call.name = chunk.name;
488
+ if (chunk.arguments) call.arguments += chunk.arguments;
489
+ calls.set(chunk.id, call);
490
+ } else if (chunk.type === "done") {
491
+ done = chunk;
492
+ break;
493
+ }
494
+ }
495
+ if (!done)
496
+ throw new AxlError("MODEL_CALL_STREAM_INCOMPLETE", "replayed stream ended without done");
497
+ return {
498
+ output: providerOutput(
499
+ {
500
+ content,
501
+ ...thinking ? { thinking_content: thinking } : {},
502
+ ...calls.size ? {
503
+ tool_calls: [...calls.values()].map((call) => ({
504
+ id: call.id,
505
+ type: "function",
506
+ function: { name: call.name, arguments: call.arguments }
507
+ }))
508
+ } : {},
509
+ ...done.finishReason !== void 0 ? { finishReason: done.finishReason } : {},
510
+ ...done.rawFinishReason !== void 0 ? { rawFinishReason: done.rawFinishReason } : {},
511
+ ...done.responseId !== void 0 ? { responseId: done.responseId } : {},
512
+ ...done.responseModel !== void 0 ? { responseModel: done.responseModel } : {}
513
+ },
514
+ record
515
+ )
516
+ };
517
+ };
518
+ }
519
+
520
+ // src/llm-scorer.ts
521
+ import { z as z2 } from "zod";
121
522
  import { extractJson, zodToJsonSchema } from "@axlsdk/axl";
122
523
  function isZodIssueLike(value) {
123
524
  if (!value || typeof value !== "object") return false;
@@ -125,7 +526,7 @@ function isZodIssueLike(value) {
125
526
  return Array.isArray(issue.path) && issue.path.every((segment) => typeof segment === "string" || typeof segment === "number") && typeof issue.message === "string";
126
527
  }
127
528
  function llmScorer(config) {
128
- const schema = config.schema ?? z.object({ score: z.number().min(0).max(1), reasoning: z.string() });
529
+ const schema = config.schema ?? z2.object({ score: z2.number().min(0).max(1), reasoning: z2.string() });
129
530
  const schemaJson = JSON.stringify(zodToJsonSchema(schema), null, 2);
130
531
  return {
131
532
  name: config.name,
@@ -217,11 +618,15 @@ export {
217
618
  evaluateScorerTolerance,
218
619
  isBudgetStopped,
219
620
  llmScorer,
621
+ loadModelCalls,
622
+ modelCallDataset,
220
623
  normalizeScorerResult,
221
624
  pairedBootstrapCI,
222
625
  parseRequestRecords,
223
626
  readAccounting,
627
+ recordedModelCall,
224
628
  refusedWork,
629
+ replayModelCall,
225
630
  rescore,
226
631
  resolveCaptureLimits,
227
632
  runEval,