@axlsdk/eval 0.25.3 → 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/README.md +5 -0
- package/dist/{chunk-F4MJ26YK.js → chunk-3XLPDV3O.js} +1 -1
- package/dist/chunk-3XLPDV3O.js.map +1 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +421 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -3
- package/dist/index.d.ts +43 -3
- package/dist/index.js +408 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-F4MJ26YK.js.map +0 -1
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-
|
|
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/
|
|
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 ??
|
|
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,
|