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.
@@ -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
- if (!decisions.has(initialMessages)) {
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(initialMessages, selected);
106
+ decisions.set(steps, selected);
106
107
  }
107
- const model = decisions.get(initialMessages);
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, isReady, mapWithConcurrencyLimit, pickPaths } from './decisions.js';
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 thresholds = confidenceThresholds.map((minConfidence) => {
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: accuracyOf(accepted),
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(accepted.filter((entry) => entry.correct).length, accepted.length),
22
+ readyAccuracyInterval95: wilsonInterval(correctReadyCases, accepted.length),
21
23
  };
22
24
  });
23
- const usage = usageOf(rows);
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: thresholds.find((entry) => entry.readyAccuracy !== null && entry.readyAccuracy >= 0.95)
64
- ?.minConfidence ?? null,
71
+ suggestedMinConfidence,
65
72
  latencyMs: latencyOf(rows.map((row) => row.durationMs)),
66
73
  usage,
67
- cost: price &&
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 usageOf(rows) {
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
- if (isRecord(response) && typeof response.model === 'string')
140
- models.add(response.model);
141
- if (!isRecord(response) ||
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
- input_tokens += response.usage.input_tokens;
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.6",
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",