jev-recipes 0.9.6 → 0.9.9
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/adapters/ai-sdk/index.js +5 -4
- package/dist/evaluation/archive.d.ts +4 -4
- package/dist/evaluation/comparison.d.ts +1 -0
- package/dist/evaluation/comparison.js +19 -0
- package/dist/evaluation/decisions.d.ts +0 -2
- package/dist/evaluation/decisions.js +0 -14
- package/dist/evaluation/engine.js +4 -4
- package/dist/evaluation/report.d.ts +2 -2
- package/dist/evaluation/report.js +36 -28
- package/package.json +1 -1
|
@@ -90,8 +90,9 @@ export function routeModelStep(options) {
|
|
|
90
90
|
if (!candidates.length)
|
|
91
91
|
throw new Error('Supply at least one model candidate.');
|
|
92
92
|
const decisions = new WeakMap();
|
|
93
|
-
return async ({ initialMessages }) => {
|
|
94
|
-
|
|
93
|
+
return async ({ initialMessages, steps }) => {
|
|
94
|
+
// The SDK owns one steps array per generation, even when callers reuse messages.
|
|
95
|
+
if (!decisions.has(steps)) {
|
|
95
96
|
const decision = await modelRoute({
|
|
96
97
|
request: request ?? requireText(firstUserText(initialMessages), 'request'),
|
|
97
98
|
models: candidates.map(({ id, text }) => ({ id, text })),
|
|
@@ -102,9 +103,9 @@ export function routeModelStep(options) {
|
|
|
102
103
|
? candidates.find((candidate) => candidate.id === decision.selection)?.model
|
|
103
104
|
: fallback;
|
|
104
105
|
onDecision?.({ decision, model: selected });
|
|
105
|
-
decisions.set(
|
|
106
|
+
decisions.set(steps, selected);
|
|
106
107
|
}
|
|
107
|
-
const model = decisions.get(
|
|
108
|
+
const model = decisions.get(steps);
|
|
108
109
|
return model === undefined ? {} : { model };
|
|
109
110
|
};
|
|
110
111
|
}
|
|
@@ -142,9 +142,9 @@ export declare function compare(baseline: EvaluationRun, candidate: EvaluationRu
|
|
|
142
142
|
}[];
|
|
143
143
|
thresholdEvaluation: string;
|
|
144
144
|
thresholds: {
|
|
145
|
+
readyAccuracy: number | null;
|
|
145
146
|
minConfidence: number;
|
|
146
147
|
deferRate: number | null;
|
|
147
|
-
readyAccuracy: number | null;
|
|
148
148
|
readyCases: number;
|
|
149
149
|
reviewCases: number;
|
|
150
150
|
failedCases: number;
|
|
@@ -225,9 +225,9 @@ export declare function compare(baseline: EvaluationRun, candidate: EvaluationRu
|
|
|
225
225
|
}[];
|
|
226
226
|
thresholdEvaluation: string;
|
|
227
227
|
thresholds: {
|
|
228
|
+
readyAccuracy: number | null;
|
|
228
229
|
minConfidence: number;
|
|
229
230
|
deferRate: number | null;
|
|
230
|
-
readyAccuracy: number | null;
|
|
231
231
|
readyCases: number;
|
|
232
232
|
reviewCases: number;
|
|
233
233
|
failedCases: number;
|
|
@@ -328,9 +328,9 @@ export declare function compare(baseline: EvaluationRun, candidate: EvaluationRu
|
|
|
328
328
|
}[];
|
|
329
329
|
thresholdEvaluation: string;
|
|
330
330
|
thresholds: {
|
|
331
|
+
readyAccuracy: number | null;
|
|
331
332
|
minConfidence: number;
|
|
332
333
|
deferRate: number | null;
|
|
333
|
-
readyAccuracy: number | null;
|
|
334
334
|
readyCases: number;
|
|
335
335
|
reviewCases: number;
|
|
336
336
|
failedCases: number;
|
|
@@ -411,9 +411,9 @@ export declare function compare(baseline: EvaluationRun, candidate: EvaluationRu
|
|
|
411
411
|
}[];
|
|
412
412
|
thresholdEvaluation: string;
|
|
413
413
|
thresholds: {
|
|
414
|
+
readyAccuracy: number | null;
|
|
414
415
|
minConfidence: number;
|
|
415
416
|
deferRate: number | null;
|
|
416
|
-
readyAccuracy: number | null;
|
|
417
417
|
readyCases: number;
|
|
418
418
|
reviewCases: number;
|
|
419
419
|
failedCases: number;
|
|
@@ -3,6 +3,7 @@ type Schema = Record<string, unknown>;
|
|
|
3
3
|
export declare function resultSchemaFor(recipe: string): Schema;
|
|
4
4
|
export declare function comparableDecision(value: unknown, recipe: string): unknown;
|
|
5
5
|
export declare function matchesExpected(actual: unknown, expected: unknown, recipe: string): boolean;
|
|
6
|
+
export declare function isReady(result: unknown, recipe: string): boolean;
|
|
6
7
|
export declare function isMetadataPath(schema: Schema, segments: string[]): boolean;
|
|
7
8
|
export declare function schemaAtPath(schema: Schema, segments: string[]): Schema | undefined;
|
|
8
9
|
export {};
|
|
@@ -33,6 +33,25 @@ export function comparableDecision(value, recipe) {
|
|
|
33
33
|
export function matchesExpected(actual, expected, recipe) {
|
|
34
34
|
return isDeepStrictEqual(comparableDecision(actual, recipe), comparableDecision(expected, recipe));
|
|
35
35
|
}
|
|
36
|
+
export function isReady(result, recipe) {
|
|
37
|
+
if (!isRecord(result))
|
|
38
|
+
return false;
|
|
39
|
+
const schema = resultSchemaFor(recipe);
|
|
40
|
+
if (isMetadataPath(schema, ['status']) && result.status !== undefined)
|
|
41
|
+
return result.status === 'ready';
|
|
42
|
+
return !hasReview(result, schema);
|
|
43
|
+
}
|
|
44
|
+
function hasReview(value, schema) {
|
|
45
|
+
if (!schema)
|
|
46
|
+
return false;
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return value.some((item, index) => hasReview(item, schemaAtPath(schema, [String(index)])));
|
|
49
|
+
if (!isRecord(value))
|
|
50
|
+
return false;
|
|
51
|
+
return Object.entries(value).some(([name, field]) => name === 'status' && isMetadataPath(schema, [name])
|
|
52
|
+
? field === 'review'
|
|
53
|
+
: hasReview(field, schemaAtPath(schema, [name])));
|
|
54
|
+
}
|
|
36
55
|
function normalize(value, schema) {
|
|
37
56
|
if (!schema)
|
|
38
57
|
return value;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
2
2
|
export declare function valueAtPath(record: unknown, path: string): unknown;
|
|
3
3
|
export declare function pickPaths(record: unknown, paths: string[]): Record<string, unknown>;
|
|
4
|
-
export declare function hasReviewAnywhere(value: unknown): boolean;
|
|
5
|
-
export declare function isReady(result: unknown): boolean;
|
|
6
4
|
export declare function caseConfidence(result: unknown, expectedPaths?: string[]): number | undefined;
|
|
7
5
|
export declare function mapWithConcurrencyLimit<T, R>(items: T[], limit: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
8
6
|
export declare function roundedTo(places: number, value: number): number;
|
|
@@ -11,20 +11,6 @@ export function valueAtPath(record, path) {
|
|
|
11
11
|
export function pickPaths(record, paths) {
|
|
12
12
|
return Object.fromEntries(paths.map((path) => [path, valueAtPath(record, path)]));
|
|
13
13
|
}
|
|
14
|
-
export function hasReviewAnywhere(value) {
|
|
15
|
-
if (Array.isArray(value))
|
|
16
|
-
return value.some(hasReviewAnywhere);
|
|
17
|
-
if (isRecord(value))
|
|
18
|
-
return Object.entries(value).some(([name, field]) => name === 'status' ? field === 'review' : hasReviewAnywhere(field));
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
export function isReady(result) {
|
|
22
|
-
if (!isRecord(result))
|
|
23
|
-
return false;
|
|
24
|
-
if (result.status !== undefined)
|
|
25
|
-
return result.status === 'ready';
|
|
26
|
-
return !hasReviewAnywhere(result);
|
|
27
|
-
}
|
|
28
14
|
export function caseConfidence(result, expectedPaths = []) {
|
|
29
15
|
if (isRecord(result) && typeof result.confidence === 'number')
|
|
30
16
|
return result.confidence;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
2
|
import { createClient } from '../src/client.js';
|
|
3
|
-
import { caseConfidence,
|
|
4
|
-
import { matchesExpected } from './comparison.js';
|
|
3
|
+
import { caseConfidence, mapWithConcurrencyLimit, pickPaths } from './decisions.js';
|
|
4
|
+
import { isReady, matchesExpected } from './comparison.js';
|
|
5
5
|
export const confidenceThresholds = [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95];
|
|
6
6
|
export async function evaluateCases(recipe, cases, options = {}) {
|
|
7
7
|
let requests = 0;
|
|
@@ -52,7 +52,7 @@ async function evaluateCase(recipe, goldenCase, client, options) {
|
|
|
52
52
|
row.result = await recipe.run(goldenCase.input, recipeOptions);
|
|
53
53
|
row.actual = pickPaths(row.result, Object.keys(goldenCase.expected));
|
|
54
54
|
row.correct = matchesExpected(row.actual, row.expected, recipe.id);
|
|
55
|
-
row.ready = isReady(row.result);
|
|
55
|
+
row.ready = isReady(row.result, recipe.id);
|
|
56
56
|
row.confidence = caseConfidence(row.result, Object.keys(goldenCase.expected));
|
|
57
57
|
const model = pickPaths(row.result, ['model']).model;
|
|
58
58
|
if (typeof model === 'string')
|
|
@@ -72,7 +72,7 @@ async function evaluateCase(recipe, goldenCase, client, options) {
|
|
|
72
72
|
playback.assertComplete();
|
|
73
73
|
row.decisions.push({
|
|
74
74
|
minConfidence,
|
|
75
|
-
ready: isReady(result),
|
|
75
|
+
ready: isReady(result, recipe.id),
|
|
76
76
|
correct: matchesExpected(pickPaths(result, Object.keys(row.expected)), row.expected, recipe.id),
|
|
77
77
|
});
|
|
78
78
|
}
|
|
@@ -29,9 +29,9 @@ export declare function buildReport(id: string, rows: EvaluationRow[], confidenc
|
|
|
29
29
|
}[];
|
|
30
30
|
thresholdEvaluation: string;
|
|
31
31
|
thresholds: {
|
|
32
|
+
readyAccuracy: number | null;
|
|
32
33
|
minConfidence: number;
|
|
33
34
|
deferRate: number | null;
|
|
34
|
-
readyAccuracy: number | null;
|
|
35
35
|
readyCases: number;
|
|
36
36
|
reviewCases: number;
|
|
37
37
|
failedCases: number;
|
|
@@ -112,9 +112,9 @@ export declare function buildReport(id: string, rows: EvaluationRow[], confidenc
|
|
|
112
112
|
}[];
|
|
113
113
|
thresholdEvaluation: string;
|
|
114
114
|
thresholds: {
|
|
115
|
+
readyAccuracy: number | null;
|
|
115
116
|
minConfidence: number;
|
|
116
117
|
deferRate: number | null;
|
|
117
|
-
readyAccuracy: number | null;
|
|
118
118
|
readyCases: number;
|
|
119
119
|
reviewCases: number;
|
|
120
120
|
failedCases: number;
|
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
import { isRecord, roundedTo } from './decisions.js';
|
|
2
2
|
import { matchesExpected } from './comparison.js';
|
|
3
|
+
import { resultMetadataSchema } from '../src/schema.js';
|
|
3
4
|
export function buildReport(id, rows, confidenceThresholds, price, evidence) {
|
|
4
5
|
const completed = rows.filter((row) => row.error === undefined);
|
|
5
6
|
const ready = completed.filter((row) => row.ready);
|
|
6
|
-
const
|
|
7
|
+
const thresholdEvaluations = confidenceThresholds.map((minConfidence) => {
|
|
7
8
|
const decisions = completed
|
|
8
9
|
.flatMap((row) => row.decisions)
|
|
9
10
|
.filter((decision) => decision.minConfidence === minConfidence);
|
|
10
11
|
const accepted = decisions.filter((decision) => decision.ready);
|
|
12
|
+
const correctReadyCases = accepted.filter((decision) => decision.correct).length;
|
|
11
13
|
return {
|
|
12
14
|
minConfidence,
|
|
13
15
|
deferRate: completed.length
|
|
14
16
|
? roundedTo(3, (completed.length - accepted.length) / completed.length)
|
|
15
17
|
: null,
|
|
16
|
-
readyAccuracy:
|
|
18
|
+
readyAccuracy: accepted.length ? correctReadyCases / accepted.length : null,
|
|
17
19
|
readyCases: accepted.length,
|
|
18
20
|
reviewCases: completed.length - accepted.length,
|
|
19
21
|
failedCases: rows.length - completed.length,
|
|
20
|
-
readyAccuracyInterval95: wilsonInterval(
|
|
22
|
+
readyAccuracyInterval95: wilsonInterval(correctReadyCases, accepted.length),
|
|
21
23
|
};
|
|
22
24
|
});
|
|
23
|
-
const
|
|
25
|
+
const suggestedMinConfidence = thresholdEvaluations.find((entry) => entry.readyAccuracy !== null && entry.readyAccuracy >= 0.95)?.minConfidence ?? null;
|
|
26
|
+
const thresholds = thresholdEvaluations.map((threshold) => ({
|
|
27
|
+
...threshold,
|
|
28
|
+
readyAccuracy: threshold.readyAccuracy === null ? null : roundedTo(3, threshold.readyAccuracy),
|
|
29
|
+
}));
|
|
30
|
+
const usage = summarizeUsage(rows);
|
|
31
|
+
const cost = estimateKnownCost(usage, price);
|
|
24
32
|
const models = [...new Set(rows.flatMap((row) => (row.model ? [row.model] : [])))].sort();
|
|
25
33
|
return {
|
|
26
34
|
...(evidence ? { evidence } : {}),
|
|
@@ -60,21 +68,10 @@ export function buildReport(id, rows, confidenceThresholds, price, evidence) {
|
|
|
60
68
|
? 'recipe-replay'
|
|
61
69
|
: 'not-applicable',
|
|
62
70
|
thresholds,
|
|
63
|
-
suggestedMinConfidence
|
|
64
|
-
?.minConfidence ?? null,
|
|
71
|
+
suggestedMinConfidence,
|
|
65
72
|
latencyMs: latencyOf(rows.map((row) => row.durationMs)),
|
|
66
73
|
usage,
|
|
67
|
-
cost
|
|
68
|
-
usage.models.every((model) => model === price.model) &&
|
|
69
|
-
usage.unknownResponses === 0 &&
|
|
70
|
-
usage.failedRequests === 0
|
|
71
|
-
? {
|
|
72
|
-
...price,
|
|
73
|
-
estimated: roundedTo(8, (usage.input_tokens * price.inputPerMillion +
|
|
74
|
-
usage.output_tokens * price.outputPerMillion) /
|
|
75
|
-
1_000_000),
|
|
76
|
-
}
|
|
77
|
-
: null,
|
|
74
|
+
cost,
|
|
78
75
|
confusion: confusionOf(rows),
|
|
79
76
|
failures: rows
|
|
80
77
|
.filter((row) => !row.correct)
|
|
@@ -126,7 +123,7 @@ function latencyOf(values) {
|
|
|
126
123
|
totalCaseTime: roundedTo(2, values.reduce((sum, value) => sum + value, 0)),
|
|
127
124
|
};
|
|
128
125
|
}
|
|
129
|
-
function
|
|
126
|
+
function summarizeUsage(rows) {
|
|
130
127
|
let input_tokens = 0, output_tokens = 0, unknownResponses = 0, failedRequests = 0;
|
|
131
128
|
const models = new Set();
|
|
132
129
|
const exchanges = rows.flatMap((row) => row.exchanges);
|
|
@@ -135,18 +132,17 @@ function usageOf(rows) {
|
|
|
135
132
|
failedRequests++;
|
|
136
133
|
continue;
|
|
137
134
|
}
|
|
138
|
-
const response = exchange.response;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!
|
|
142
|
-
!isRecord(response.usage) ||
|
|
143
|
-
typeof response.usage.input_tokens !== 'number' ||
|
|
144
|
-
typeof response.usage.output_tokens !== 'number') {
|
|
135
|
+
const response = isRecord(exchange.response) ? exchange.response : {};
|
|
136
|
+
const usage = resultMetadataSchema.shape.usage.safeParse(response.usage);
|
|
137
|
+
const model = resultMetadataSchema.shape.model.safeParse(response.model);
|
|
138
|
+
if (!usage.success || !model.success)
|
|
145
139
|
unknownResponses++;
|
|
140
|
+
if (model.success)
|
|
141
|
+
models.add(model.data);
|
|
142
|
+
if (!usage.success)
|
|
146
143
|
continue;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
output_tokens += response.usage.output_tokens;
|
|
144
|
+
input_tokens += usage.data.input_tokens;
|
|
145
|
+
output_tokens += usage.data.output_tokens;
|
|
150
146
|
}
|
|
151
147
|
return {
|
|
152
148
|
requests: exchanges.length,
|
|
@@ -157,6 +153,18 @@ function usageOf(rows) {
|
|
|
157
153
|
models: [...models].sort(),
|
|
158
154
|
};
|
|
159
155
|
}
|
|
156
|
+
function estimateKnownCost(usage, price) {
|
|
157
|
+
if (!price ||
|
|
158
|
+
usage.unknownResponses > 0 ||
|
|
159
|
+
usage.failedRequests > 0 ||
|
|
160
|
+
usage.models.some((model) => model !== price.model))
|
|
161
|
+
return null;
|
|
162
|
+
return {
|
|
163
|
+
...price,
|
|
164
|
+
estimated: roundedTo(8, (usage.input_tokens * price.inputPerMillion + usage.output_tokens * price.outputPerMillion) /
|
|
165
|
+
1_000_000),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
160
168
|
function confusionOf(rows) {
|
|
161
169
|
const byExpected = new Map();
|
|
162
170
|
for (const row of rows) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jev-recipes",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"description": "248 small Jev decisions for JavaScript and TypeScript: route, rerank, gate, grade, compare, and label text for agents, RAG, support, code review, and music. Each returns a typed result with explicit uncertainty. Plug into Vercel AI SDK or LangChain agents, import one recipe, or run the CLI with JSON from any language.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|