@crowandraven/evalkeeper 0.2.0 → 0.4.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/common.js +4 -2
- package/dist/common.js.map +1 -1
- package/dist/importCases.d.ts +94 -0
- package/dist/importCases.js +185 -0
- package/dist/importCases.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/labels.d.ts +9 -3
- package/dist/labels.js +2 -0
- package/dist/labels.js.map +1 -1
- package/dist/memoryStore.js +16 -2
- package/dist/memoryStore.js.map +1 -1
- package/dist/models.d.ts +17 -6
- package/dist/models.js +29 -5
- package/dist/models.js.map +1 -1
- package/dist/runner.d.ts +16 -1
- package/dist/runner.js +68 -18
- package/dist/runner.js.map +1 -1
- package/dist/settings.js +1 -0
- package/dist/settings.js.map +1 -1
- package/dist/stats.d.ts +5 -1
- package/dist/stats.js +21 -6
- package/dist/stats.js.map +1 -1
- package/dist/store.d.ts +24 -2
- package/dist/testing.js +77 -0
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.js.map +1 -1
- package/dist/weekly.js +3 -2
- package/dist/weekly.js.map +1 -1
- package/package.json +1 -1
- package/src/common.ts +5 -2
- package/src/importCases.ts +252 -0
- package/src/index.ts +1 -0
- package/src/labels.ts +9 -4
- package/src/memoryStore.ts +24 -2
- package/src/models.ts +35 -7
- package/src/runner.ts +126 -49
- package/src/settings.ts +1 -0
- package/src/stats.ts +19 -6
- package/src/store.ts +28 -2
- package/src/testing.ts +91 -0
- package/src/types.ts +5 -0
- package/src/weekly.ts +3 -2
- package/templates/app/evals/[suite]/page.tsx +15 -10
- package/templates/app/evals/feedback/page.tsx +11 -6
- package/templates/app/evals/page.tsx +59 -15
- package/templates/components/evalkeeper/AddCase.tsx +2 -1
- package/templates/components/evalkeeper/AnswerKey.tsx +14 -15
- package/templates/components/evalkeeper/ConfirmAnswer.tsx +46 -0
- package/templates/components/evalkeeper/RefreshWhile.tsx +16 -7
- package/templates/components/evalkeeper/RollBack.tsx +62 -0
- package/templates/components/evalkeeper/SettingsHistory.tsx +6 -20
- package/templates/components/evalkeeper/Submit.tsx +13 -4
package/src/models.ts
CHANGED
|
@@ -3,10 +3,27 @@
|
|
|
3
3
|
* pass a catalog in; Evalkeeper never picks a provider itself. Pure.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The reasoning settings providers name, lowest first, so a setting one
|
|
8
|
+
* model lacks can be placed against the ones it has. A model's own
|
|
9
|
+
* `efforts` say which it takes.
|
|
10
|
+
*/
|
|
11
|
+
export const REASONING_EFFORTS = [
|
|
12
|
+
"none",
|
|
13
|
+
"minimal",
|
|
14
|
+
"low",
|
|
15
|
+
"medium",
|
|
16
|
+
"high",
|
|
17
|
+
"xhigh",
|
|
18
|
+
"max",
|
|
19
|
+
] as const;
|
|
20
|
+
/** A setting from REASONING_EFFORTS. */
|
|
21
|
+
export type KnownReasoningEffort = (typeof REASONING_EFFORTS)[number];
|
|
22
|
+
/**
|
|
23
|
+
* A reasoning setting a model may accept: one of REASONING_EFFORTS, or any
|
|
24
|
+
* other name a provider uses. The catalog's `efforts` order them per model.
|
|
25
|
+
*/
|
|
26
|
+
export type ReasoningEffort = KnownReasoningEffort | (string & {});
|
|
10
27
|
|
|
11
28
|
/**
|
|
12
29
|
* What Evalkeeper needs to know about one model: its label, provider, prices, reasoning
|
|
@@ -112,8 +129,10 @@ export function priceCall(
|
|
|
112
129
|
|
|
113
130
|
/**
|
|
114
131
|
* The effort a model actually gets for a requested one: the same if it
|
|
115
|
-
* takes it;
|
|
116
|
-
*
|
|
132
|
+
* takes it; one below the lowest it takes (say "none" on a model that
|
|
133
|
+
* starts at "minimal"), its lowest; one above its highest, its highest;
|
|
134
|
+
* otherwise (a name it doesn't place) its own default. Undefined: send
|
|
135
|
+
* nothing.
|
|
117
136
|
*/
|
|
118
137
|
export function effortFor(
|
|
119
138
|
models: ModelCatalog,
|
|
@@ -123,5 +142,14 @@ export function effortFor(
|
|
|
123
142
|
const efforts = models[model]?.efforts;
|
|
124
143
|
if (!requested || !efforts) return undefined;
|
|
125
144
|
if (efforts.includes(requested)) return requested;
|
|
126
|
-
|
|
145
|
+
const rank = (e: string) =>
|
|
146
|
+
(REASONING_EFFORTS as readonly string[]).indexOf(e);
|
|
147
|
+
const want = rank(requested);
|
|
148
|
+
const lowest = efforts[0];
|
|
149
|
+
const highest = efforts.at(-1);
|
|
150
|
+
if (want < 0 || lowest === undefined || highest === undefined)
|
|
151
|
+
return undefined;
|
|
152
|
+
if (rank(lowest) >= 0 && want < rank(lowest)) return lowest;
|
|
153
|
+
if (rank(highest) >= 0 && want > rank(highest)) return highest;
|
|
154
|
+
return undefined;
|
|
127
155
|
}
|
package/src/runner.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
EvalStore,
|
|
25
25
|
RunMetrics,
|
|
26
26
|
RunRecord,
|
|
27
|
+
OutputRecord,
|
|
27
28
|
} from "./store.js";
|
|
28
29
|
import { withTimeout } from "./timeout.js";
|
|
29
30
|
import { SCHEDULED } from "./weekly.js";
|
|
@@ -105,11 +106,18 @@ export type RunnerOptions = {
|
|
|
105
106
|
models: string[];
|
|
106
107
|
fallbackModels?: string[];
|
|
107
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* Which retiring models (`ModelInfo.retiring`) the board lists.
|
|
111
|
+
* "always" (the default): every candidate, though a retiring one is never
|
|
112
|
+
* suggested. "inUse": only one a feature still runs on, as its live or
|
|
113
|
+
* fallback model, so nobody spends a test on a model that's going away.
|
|
114
|
+
*/
|
|
115
|
+
retiringOnBoard?: "always" | "inUse";
|
|
108
116
|
};
|
|
109
117
|
|
|
110
118
|
/**
|
|
111
119
|
* Which model a feature runs on now, its fallback, its reasoning setting,
|
|
112
|
-
* and its
|
|
120
|
+
* its temperature, and its output cap.
|
|
113
121
|
*/
|
|
114
122
|
export type Route = {
|
|
115
123
|
model: string;
|
|
@@ -117,6 +125,8 @@ export type Route = {
|
|
|
117
125
|
reasoningEffort: ReasoningEffort | null;
|
|
118
126
|
/** Null or unset: no temperature sent. */
|
|
119
127
|
temperature?: number | null;
|
|
128
|
+
/** The output-token cap; null or unset: the call's own default. */
|
|
129
|
+
maxOutputTokens?: number | null;
|
|
120
130
|
};
|
|
121
131
|
|
|
122
132
|
/**
|
|
@@ -149,6 +159,8 @@ export function requestHashFor(r: {
|
|
|
149
159
|
repeat: number;
|
|
150
160
|
/** Left out of the hash when null, so requests without one reuse as before. */
|
|
151
161
|
temperature?: number | null;
|
|
162
|
+
/** Left out of the hash when null, like temperature. */
|
|
163
|
+
maxOutputTokens?: number | null;
|
|
152
164
|
}): string {
|
|
153
165
|
return sha(
|
|
154
166
|
stableJson({
|
|
@@ -159,6 +171,9 @@ export function requestHashFor(r: {
|
|
|
159
171
|
input: r.input,
|
|
160
172
|
repeat: r.repeat,
|
|
161
173
|
...(r.temperature != null ? { temperature: r.temperature } : {}),
|
|
174
|
+
...(r.maxOutputTokens != null
|
|
175
|
+
? { maxOutputTokens: r.maxOutputTokens }
|
|
176
|
+
: {}),
|
|
162
177
|
}),
|
|
163
178
|
64,
|
|
164
179
|
);
|
|
@@ -267,6 +282,12 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
267
282
|
return (await opts.route(operation)).temperature ?? null;
|
|
268
283
|
}
|
|
269
284
|
|
|
285
|
+
/** The operation's live output cap: the route's, else the call's own. */
|
|
286
|
+
async function liveCapFor(operation: string): Promise<number | null> {
|
|
287
|
+
if (!opts.route) return null;
|
|
288
|
+
return (await opts.route(operation)).maxOutputTokens ?? null;
|
|
289
|
+
}
|
|
290
|
+
|
|
270
291
|
/**
|
|
271
292
|
* What a test would cost. Answers an identical request gave in the last
|
|
272
293
|
* 7 days are reused for free, so only the rest are priced: a retest
|
|
@@ -285,6 +306,8 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
285
306
|
reasoningEffort?: ReasoningEffort | null;
|
|
286
307
|
/** The temperature to test; undefined: the operation's live one. */
|
|
287
308
|
temperature?: number | null;
|
|
309
|
+
/** The output cap to test; undefined: the operation's live one. */
|
|
310
|
+
maxOutputTokens?: number | null;
|
|
288
311
|
} = {},
|
|
289
312
|
): Promise<{
|
|
290
313
|
cases: number;
|
|
@@ -295,41 +318,53 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
295
318
|
costUsd: number | null;
|
|
296
319
|
}> {
|
|
297
320
|
const suite = suiteOrThrow(suiteKey);
|
|
298
|
-
const [cases, perCall, prompt, effort, temperature] =
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
321
|
+
const [cases, perCall, prompt, effort, temperature, cap] =
|
|
322
|
+
await Promise.all([
|
|
323
|
+
casesFor(suite.key, set),
|
|
324
|
+
opts.estimateCallCost?.(suite.operation, model) ?? null,
|
|
325
|
+
opts.promptVersion(suite.operation),
|
|
326
|
+
o.reasoningEffort !== undefined
|
|
327
|
+
? o.reasoningEffort
|
|
328
|
+
: liveEffortFor(suite.operation),
|
|
329
|
+
o.temperature !== undefined
|
|
330
|
+
? o.temperature
|
|
331
|
+
: liveTemperatureFor(suite.operation),
|
|
332
|
+
o.maxOutputTokens !== undefined
|
|
333
|
+
? o.maxOutputTokens
|
|
334
|
+
: liveCapFor(suite.operation),
|
|
335
|
+
]);
|
|
309
336
|
const answers = cases.length * repeats;
|
|
310
337
|
const since = new Date(Date.now() - REUSE_DAYS * 86_400_000);
|
|
311
|
-
const
|
|
312
|
-
?
|
|
313
|
-
: (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
338
|
+
const hashes = o.fresh
|
|
339
|
+
? []
|
|
340
|
+
: cases.flatMap((c) =>
|
|
341
|
+
Array.from({ length: repeats }, (_, i) =>
|
|
342
|
+
requestHashFor({
|
|
343
|
+
suite: suite.key,
|
|
344
|
+
model,
|
|
345
|
+
effort,
|
|
346
|
+
prompt,
|
|
347
|
+
input: c.input,
|
|
348
|
+
repeat: i + 1,
|
|
349
|
+
temperature,
|
|
350
|
+
maxOutputTokens: cap,
|
|
351
|
+
}),
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
// One query where the store can: a lookup per answer is slow on a
|
|
355
|
+
// remote database.
|
|
356
|
+
const found = store.findReusableHashes
|
|
357
|
+
? await store.findReusableHashes(hashes, since)
|
|
358
|
+
: new Set(
|
|
359
|
+
(
|
|
360
|
+
await Promise.all(
|
|
361
|
+
hashes.map(async (h) =>
|
|
362
|
+
(await store.findReusableOutput(h, since)) ? h : null,
|
|
329
363
|
),
|
|
330
|
-
)
|
|
331
|
-
)
|
|
332
|
-
)
|
|
364
|
+
)
|
|
365
|
+
).filter((h): h is string => h !== null),
|
|
366
|
+
);
|
|
367
|
+
const reused = hashes.filter((h) => found.has(h)).length;
|
|
333
368
|
return {
|
|
334
369
|
cases: cases.length,
|
|
335
370
|
answers,
|
|
@@ -350,6 +385,8 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
350
385
|
reasoningEffort?: ReasoningEffort | null;
|
|
351
386
|
/** A temperature to test; undefined: the operation's live one. */
|
|
352
387
|
temperature?: number | null;
|
|
388
|
+
/** An output cap to test; undefined: the operation's live one. */
|
|
389
|
+
maxOutputTokens?: number | null;
|
|
353
390
|
/** For the estimate: pass the same `fresh` to runEval. */
|
|
354
391
|
fresh?: boolean;
|
|
355
392
|
}) {
|
|
@@ -364,10 +401,15 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
364
401
|
args.temperature !== undefined
|
|
365
402
|
? args.temperature
|
|
366
403
|
: await liveTemperatureFor(suite.operation);
|
|
404
|
+
const maxOutputTokens =
|
|
405
|
+
args.maxOutputTokens !== undefined
|
|
406
|
+
? args.maxOutputTokens
|
|
407
|
+
: await liveCapFor(suite.operation);
|
|
367
408
|
const [estimate, promptVersion] = await Promise.all([
|
|
368
409
|
estimateRun(suite.key, args.model, set, repeats, {
|
|
369
410
|
reasoningEffort,
|
|
370
411
|
temperature,
|
|
412
|
+
maxOutputTokens,
|
|
371
413
|
fresh: Boolean(args.fresh),
|
|
372
414
|
}),
|
|
373
415
|
opts.promptVersion(suite.operation),
|
|
@@ -380,6 +422,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
380
422
|
reasoningEffort:
|
|
381
423
|
args.reasoningEffort !== undefined ? args.reasoningEffort : liveEffort,
|
|
382
424
|
temperature,
|
|
425
|
+
maxOutputTokens,
|
|
383
426
|
set,
|
|
384
427
|
repeats,
|
|
385
428
|
datasetVersion: null,
|
|
@@ -516,6 +559,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
516
559
|
input: c.input,
|
|
517
560
|
repeat,
|
|
518
561
|
temperature: run.temperature ?? null,
|
|
562
|
+
maxOutputTokens: run.maxOutputTokens ?? null,
|
|
519
563
|
});
|
|
520
564
|
const earlier = o.fresh
|
|
521
565
|
? null
|
|
@@ -543,9 +587,9 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
543
587
|
const output = await withTimeout(
|
|
544
588
|
suite.run(suite.input.parse(c.input), {
|
|
545
589
|
model: run.model,
|
|
546
|
-
reasoningEffort:
|
|
547
|
-
(run.reasoningEffort as ReasoningEffort | null) ?? null,
|
|
590
|
+
reasoningEffort: run.reasoningEffort ?? null,
|
|
548
591
|
temperature: run.temperature ?? null,
|
|
592
|
+
maxOutputTokens: run.maxOutputTokens ?? null,
|
|
549
593
|
onCost: (usd) => (cost += usd),
|
|
550
594
|
}),
|
|
551
595
|
timeout,
|
|
@@ -693,6 +737,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
693
737
|
startedBy: SCHEDULED,
|
|
694
738
|
reasoningEffort: route.reasoningEffort,
|
|
695
739
|
temperature: route.temperature ?? null,
|
|
740
|
+
maxOutputTokens: route.maxOutputTokens ?? null,
|
|
696
741
|
});
|
|
697
742
|
return run.id;
|
|
698
743
|
}
|
|
@@ -708,11 +753,30 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
708
753
|
return id;
|
|
709
754
|
}
|
|
710
755
|
|
|
756
|
+
/**
|
|
757
|
+
* Several tests' outputs by test id: one call where the store can, so a
|
|
758
|
+
* remote database isn't asked once per test.
|
|
759
|
+
*/
|
|
760
|
+
async function outputsByRun(
|
|
761
|
+
runIds: string[],
|
|
762
|
+
): Promise<Map<string, OutputRecord[]>> {
|
|
763
|
+
const byRun = new Map(runIds.map((id) => [id, [] as OutputRecord[]]));
|
|
764
|
+
if (!runIds.length) return byRun;
|
|
765
|
+
const rows = store.listOutputsFor
|
|
766
|
+
? await store.listOutputsFor(runIds)
|
|
767
|
+
: (await Promise.all(runIds.map((id) => store.listOutputs(id)))).flat();
|
|
768
|
+
for (const o of rows) byRun.get(o.runId)?.push(o);
|
|
769
|
+
return byRun;
|
|
770
|
+
}
|
|
771
|
+
|
|
711
772
|
/** Marks tests the server stopped mid-way as failed. */
|
|
712
773
|
async function stopStuckRuns(suiteKey: string, now = Date.now()) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
774
|
+
const old = (await store.listRuns(suiteKey, { status: "RUNNING" })).filter(
|
|
775
|
+
(r) => now - +r.startedAt > STUCK_MS,
|
|
776
|
+
);
|
|
777
|
+
const outputs = await outputsByRun(old.map((r) => r.id));
|
|
778
|
+
for (const r of old) {
|
|
779
|
+
const outs = outputs.get(r.id) ?? [];
|
|
716
780
|
const last = outs.reduce<Date | null>(
|
|
717
781
|
(m, x) => (!m || x.createdAt > m ? x.createdAt : m),
|
|
718
782
|
null,
|
|
@@ -783,12 +847,12 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
783
847
|
if (!opts.route || !opts.candidates)
|
|
784
848
|
throw new Error("The board needs the route and candidates options");
|
|
785
849
|
const suite = suiteOrThrow(suiteKey);
|
|
786
|
-
await
|
|
787
|
-
const [route, cases] = await Promise.all([
|
|
850
|
+
const [route, cases, prompt] = await Promise.all([
|
|
788
851
|
opts.route(suite.operation),
|
|
789
852
|
casesFor(suite.key, "ALL"),
|
|
853
|
+
opts.promptVersion(suite.operation),
|
|
854
|
+
stopStuckRuns(suite.key),
|
|
790
855
|
]);
|
|
791
|
-
const prompt = await opts.promptVersion(suite.operation);
|
|
792
856
|
const current = datasetVersion(cases);
|
|
793
857
|
const { models, fallbackModels = models } = opts.candidates(
|
|
794
858
|
suite.operation,
|
|
@@ -801,7 +865,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
801
865
|
// The setting a model actually gets: models without reasoning settings
|
|
802
866
|
// ignore it, so their rows don't split by it.
|
|
803
867
|
const realEffort = (model: string, effort: string | null) =>
|
|
804
|
-
effortFor(opts.models, model, effort
|
|
868
|
+
effortFor(opts.models, model, effort) ?? null;
|
|
805
869
|
const keyOf = (model: string, effort: string | null) =>
|
|
806
870
|
`${model}|${realEffort(model, effort) ?? ""}`;
|
|
807
871
|
const runKey = (r: RunRecord) => keyOf(r.model, r.reasoningEffort);
|
|
@@ -815,11 +879,14 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
815
879
|
latestDone.set(runKey(r), r);
|
|
816
880
|
}
|
|
817
881
|
const liveTemperature = route.temperature ?? null;
|
|
818
|
-
|
|
882
|
+
const liveCap = route.maxOutputTokens ?? null;
|
|
883
|
+
// A test at another temperature or output cap than the live ones
|
|
884
|
+
// answered differently.
|
|
819
885
|
const isCurrent = (r: RunRecord) =>
|
|
820
886
|
r.datasetVersion === current &&
|
|
821
887
|
r.promptVersion === prompt &&
|
|
822
|
-
(r.temperature ?? null) === liveTemperature
|
|
888
|
+
(r.temperature ?? null) === liveTemperature &&
|
|
889
|
+
(r.maxOutputTokens ?? null) === liveCap;
|
|
823
890
|
|
|
824
891
|
const liveKey = keyOf(route.model, defaultEffort);
|
|
825
892
|
const liveRun = latestDone.get(liveKey);
|
|
@@ -829,12 +896,15 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
829
896
|
liveRun !== undefined &&
|
|
830
897
|
r.datasetVersion === liveRun.datasetVersion &&
|
|
831
898
|
r.promptVersion === liveRun.promptVersion &&
|
|
832
|
-
(r.temperature ?? null) === (liveRun.temperature ?? null)
|
|
899
|
+
(r.temperature ?? null) === (liveRun.temperature ?? null) &&
|
|
900
|
+
(r.maxOutputTokens ?? null) === (liveRun.maxOutputTokens ?? null);
|
|
833
901
|
const outputs = new Map<string, RunOutput[]>();
|
|
834
|
-
for (const
|
|
902
|
+
for (const [id, outs] of await outputsByRun(
|
|
903
|
+
[...latestDone.values()].map((r) => r.id),
|
|
904
|
+
))
|
|
835
905
|
outputs.set(
|
|
836
|
-
|
|
837
|
-
|
|
906
|
+
id,
|
|
907
|
+
outs.map((x) => ({
|
|
838
908
|
caseId: x.caseId,
|
|
839
909
|
checks: x.error ? null : (x.scores ?? []),
|
|
840
910
|
costUsd: x.costUsd,
|
|
@@ -846,7 +916,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
846
916
|
const checkKeys = suite.checks.map((c) => c.key);
|
|
847
917
|
|
|
848
918
|
const seen = new Set<string>();
|
|
849
|
-
|
|
919
|
+
let keys = [
|
|
850
920
|
...models.map((m) => ({
|
|
851
921
|
model: m,
|
|
852
922
|
effort: defaultEffort,
|
|
@@ -865,6 +935,13 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
865
935
|
seen.add(k);
|
|
866
936
|
return true;
|
|
867
937
|
});
|
|
938
|
+
// A retiring model stays on the board only while a feature runs on it.
|
|
939
|
+
const inUse = (model: string) =>
|
|
940
|
+
model === route.model || model === route.fallbackModel;
|
|
941
|
+
if (opts.retiringOnBoard === "inUse")
|
|
942
|
+
keys = keys.filter(
|
|
943
|
+
({ model }) => !opts.models[model]?.retiring || inUse(model),
|
|
944
|
+
);
|
|
868
945
|
const complete = (r: RunRecord | undefined) => !(r?.metrics?.failed ?? 0);
|
|
869
946
|
const estimates = new Map(
|
|
870
947
|
await Promise.all(
|
package/src/settings.ts
CHANGED
package/src/stats.ts
CHANGED
|
@@ -92,21 +92,34 @@ export function allPassed(checks: CheckResult[] | null): boolean {
|
|
|
92
92
|
return checks !== null && checks.every((c) => c.pass !== false);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Sums each answer's partial credit; null when the suite gives none. A
|
|
97
|
+
* failed answer (no checks) earns nothing and costs what a graded answer
|
|
98
|
+
* is worth on average, so a model that fails answers can't score 100%.
|
|
99
|
+
*/
|
|
96
100
|
export function sumUnits(
|
|
97
101
|
outputs: { checks: CheckResult[] | null }[],
|
|
98
102
|
): { right: number; total: number } | null {
|
|
99
103
|
let right = 0;
|
|
100
104
|
let total = 0;
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
let graded = 0;
|
|
106
|
+
let failed = 0;
|
|
107
|
+
for (const o of outputs) {
|
|
108
|
+
if (!o.checks) {
|
|
109
|
+
failed++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
let counted = false;
|
|
113
|
+
for (const c of o.checks)
|
|
104
114
|
if (c.units) {
|
|
105
|
-
|
|
115
|
+
counted = true;
|
|
106
116
|
right += c.units.right;
|
|
107
117
|
total += c.units.total;
|
|
108
118
|
}
|
|
109
|
-
|
|
119
|
+
if (counted) graded++;
|
|
120
|
+
}
|
|
121
|
+
if (!graded) return null;
|
|
122
|
+
return { right, total: total + failed * (total / graded) };
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
/** One answer's score from 0 to 1: its partial credit, else all-or-nothing. */
|
package/src/store.ts
CHANGED
|
@@ -82,6 +82,11 @@ export type RunRecord = {
|
|
|
82
82
|
* test at another temperature than the live one shows as out of date.
|
|
83
83
|
*/
|
|
84
84
|
temperature?: number | null;
|
|
85
|
+
/**
|
|
86
|
+
* The output-token cap tested; null or unset: the call's own default.
|
|
87
|
+
* Kept like temperature: a test at another cap is out of date.
|
|
88
|
+
*/
|
|
89
|
+
maxOutputTokens?: number | null;
|
|
85
90
|
set: EvalSet;
|
|
86
91
|
repeats: number;
|
|
87
92
|
/** Changes when the cases or their answers do, so tests compare. */
|
|
@@ -139,8 +144,15 @@ export type NewCase = Omit<
|
|
|
139
144
|
* database, and run describeStoreContract against it.
|
|
140
145
|
*/
|
|
141
146
|
export interface EvalStore {
|
|
142
|
-
/**
|
|
143
|
-
|
|
147
|
+
/**
|
|
148
|
+
* A suite's cases that aren't retired, with their answers, oldest first.
|
|
149
|
+
* With `includeRetired`, retired ones too (an import checks them, so a
|
|
150
|
+
* case someone retired isn't added again).
|
|
151
|
+
*/
|
|
152
|
+
listCases(
|
|
153
|
+
suite: string,
|
|
154
|
+
opts?: { includeRetired?: boolean },
|
|
155
|
+
): Promise<CaseRecord[]>;
|
|
144
156
|
createCase(data: NewCase): Promise<CaseRecord>;
|
|
145
157
|
/** Adds an answer to a case (answers are never overwritten). */
|
|
146
158
|
addLabel(caseId: string, label: NewLabel): Promise<LabelRecord>;
|
|
@@ -167,6 +179,20 @@ export interface EvalStore {
|
|
|
167
179
|
requestHash: string,
|
|
168
180
|
since: Date,
|
|
169
181
|
): Promise<OutputRecord | null>;
|
|
182
|
+
/**
|
|
183
|
+
* The outputs of several tests in one call, for the board on a remote
|
|
184
|
+
* database. Optional: without it the runner calls listOutputs per test.
|
|
185
|
+
*/
|
|
186
|
+
listOutputsFor?(runIds: string[]): Promise<OutputRecord[]>;
|
|
187
|
+
/**
|
|
188
|
+
* Which of these request hashes have an output findReusableOutput would
|
|
189
|
+
* return, in one call, for estimating a test's cost. Optional: without it
|
|
190
|
+
* the runner looks each hash up.
|
|
191
|
+
*/
|
|
192
|
+
findReusableHashes?(
|
|
193
|
+
requestHashes: string[],
|
|
194
|
+
since: Date,
|
|
195
|
+
): Promise<Set<string>>;
|
|
170
196
|
createOutput(data: NewOutput): Promise<OutputRecord>;
|
|
171
197
|
setOutputScores(id: string, scores: CheckResult[]): Promise<void>;
|
|
172
198
|
}
|
package/src/testing.ts
CHANGED
|
@@ -84,12 +84,19 @@ export function describeStoreContract(
|
|
|
84
84
|
it("lists a suite's cases oldest first, leaving out retired ones", async () => {
|
|
85
85
|
const store = await make();
|
|
86
86
|
const first = await store.createCase(newCase("s"));
|
|
87
|
+
// Apart in time: two cases made in the same millisecond have no
|
|
88
|
+
// oldest one.
|
|
89
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
87
90
|
const second = await store.createCase(newCase("s"));
|
|
88
91
|
await store.createCase(newCase("other"));
|
|
89
92
|
await store.setCaseStatus(first.id, "RETIRED");
|
|
90
93
|
expect((await store.listCases("s")).map((c) => c.id)).toEqual([
|
|
91
94
|
second.id,
|
|
92
95
|
]);
|
|
96
|
+
// On request, retired ones too, still oldest first.
|
|
97
|
+
expect(
|
|
98
|
+
(await store.listCases("s", { includeRetired: true })).map((c) => c.id),
|
|
99
|
+
).toEqual([first.id, second.id]);
|
|
93
100
|
});
|
|
94
101
|
|
|
95
102
|
it("adds answers without overwriting, and changes their status", async () => {
|
|
@@ -135,6 +142,20 @@ export function describeStoreContract(
|
|
|
135
142
|
expect((await store.getRun(none.id))?.temperature ?? null).toBeNull();
|
|
136
143
|
});
|
|
137
144
|
|
|
145
|
+
it("keeps the output cap a run was tested at", async () => {
|
|
146
|
+
const store = await make();
|
|
147
|
+
const capped = await store.createRun({
|
|
148
|
+
...newRun("s"),
|
|
149
|
+
maxOutputTokens: 4000,
|
|
150
|
+
});
|
|
151
|
+
const none = await store.createRun({
|
|
152
|
+
...newRun("s"),
|
|
153
|
+
maxOutputTokens: null,
|
|
154
|
+
});
|
|
155
|
+
expect((await store.getRun(capped.id))?.maxOutputTokens).toBe(4000);
|
|
156
|
+
expect((await store.getRun(none.id))?.maxOutputTokens ?? null).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
138
159
|
it("updates a run only while it has the status it's guarded by", async () => {
|
|
139
160
|
const store = await make();
|
|
140
161
|
const r = await store.createRun(newRun("s"));
|
|
@@ -180,6 +201,61 @@ export function describeStoreContract(
|
|
|
180
201
|
).toBeNull();
|
|
181
202
|
});
|
|
182
203
|
|
|
204
|
+
it("lists several tests' outputs and reusable hashes in one call, where it can", async () => {
|
|
205
|
+
const store = await make();
|
|
206
|
+
const c = await store.createCase(newCase("s"));
|
|
207
|
+
const [r1, r2, r3] = await Promise.all([
|
|
208
|
+
store.createRun(newRun("s")),
|
|
209
|
+
store.createRun(newRun("s")),
|
|
210
|
+
store.createRun(newRun("s")),
|
|
211
|
+
]);
|
|
212
|
+
const base = {
|
|
213
|
+
caseId: c.id,
|
|
214
|
+
repeat: 1,
|
|
215
|
+
output: { text: "HELLO" },
|
|
216
|
+
error: null,
|
|
217
|
+
scores: null,
|
|
218
|
+
costUsd: 0.01,
|
|
219
|
+
ms: 12,
|
|
220
|
+
reused: false,
|
|
221
|
+
};
|
|
222
|
+
await store.createOutput({ ...base, runId: r1.id, requestHash: "a" });
|
|
223
|
+
await store.createOutput({ ...base, runId: r2.id, requestHash: "b" });
|
|
224
|
+
await store.createOutput({
|
|
225
|
+
...base,
|
|
226
|
+
runId: r2.id,
|
|
227
|
+
requestHash: "c",
|
|
228
|
+
reused: true,
|
|
229
|
+
});
|
|
230
|
+
await store.createOutput({
|
|
231
|
+
...base,
|
|
232
|
+
runId: r3.id,
|
|
233
|
+
requestHash: "d",
|
|
234
|
+
error: "boom",
|
|
235
|
+
output: null,
|
|
236
|
+
});
|
|
237
|
+
if (store.listOutputsFor) {
|
|
238
|
+
const outs = await store.listOutputsFor([r1.id, r2.id]);
|
|
239
|
+
expect(outs.map((o) => o.requestHash).sort()).toEqual(["a", "b", "c"]);
|
|
240
|
+
expect(await store.listOutputsFor([])).toEqual([]);
|
|
241
|
+
}
|
|
242
|
+
if (store.findReusableHashes) {
|
|
243
|
+
expect(
|
|
244
|
+
[
|
|
245
|
+
...(await store.findReusableHashes(
|
|
246
|
+
["a", "b", "c", "d", "e"],
|
|
247
|
+
new Date(0),
|
|
248
|
+
)),
|
|
249
|
+
].sort(),
|
|
250
|
+
).toEqual(["a", "b"]);
|
|
251
|
+
expect(
|
|
252
|
+
(await store.findReusableHashes(["a"], new Date(Date.now() + 60_000)))
|
|
253
|
+
.size,
|
|
254
|
+
).toBe(0);
|
|
255
|
+
expect((await store.findReusableHashes([], new Date(0))).size).toBe(0);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
183
259
|
it("runs a whole test through the runner", async () => {
|
|
184
260
|
const store = await make();
|
|
185
261
|
const echo: Suite<{ text: string }, { text: string }, { want: string }> =
|
|
@@ -389,6 +465,21 @@ export function describeSettingsStoreContract(
|
|
|
389
465
|
expect(await store.listVersions("triage")).toHaveLength(2);
|
|
390
466
|
});
|
|
391
467
|
|
|
468
|
+
it("keeps a version's reasoning setting as named, even a newer one", async () => {
|
|
469
|
+
const store = await make();
|
|
470
|
+
const v1 = await store.switchTo("triage", {
|
|
471
|
+
...create("a", null),
|
|
472
|
+
create: {
|
|
473
|
+
settings: { ...settings("a"), reasoningEffort: "xhigh" },
|
|
474
|
+
promptVersion: "p1",
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
expect(v1.reasoningEffort).toBe("xhigh");
|
|
478
|
+
expect((await store.liveVersion("triage"))?.reasoningEffort).toBe(
|
|
479
|
+
"xhigh",
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
|
|
392
483
|
it("keeps a version's temperature", async () => {
|
|
393
484
|
const store = await make();
|
|
394
485
|
const v1 = await store.switchTo("triage", {
|
package/src/types.ts
CHANGED
|
@@ -114,6 +114,11 @@ export type Suite<Input, Output, Label> = {
|
|
|
114
114
|
* temperature was versioned.
|
|
115
115
|
*/
|
|
116
116
|
temperature?: number | null;
|
|
117
|
+
/**
|
|
118
|
+
* The output-token cap the test runs at, like temperature. Null: send
|
|
119
|
+
* none (the call's own default).
|
|
120
|
+
*/
|
|
121
|
+
maxOutputTokens?: number | null;
|
|
117
122
|
onCost: (usd: number) => void;
|
|
118
123
|
},
|
|
119
124
|
): Promise<Output>;
|
package/src/weekly.ts
CHANGED
|
@@ -104,8 +104,8 @@ export function weeklySummary(args: {
|
|
|
104
104
|
continue;
|
|
105
105
|
}
|
|
106
106
|
if (r.status !== "SUCCEEDED") continue;
|
|
107
|
-
// The last check of the same model, temperature,
|
|
108
|
-
// grading, a day or more earlier: anything else changed for a known
|
|
107
|
+
// The last check of the same model, temperature, output cap,
|
|
108
|
+
// instructions, and grading, a day or more earlier: anything else changed for a known
|
|
109
109
|
// reason.
|
|
110
110
|
const prior = scheduled.find(
|
|
111
111
|
(p) =>
|
|
@@ -113,6 +113,7 @@ export function weeklySummary(args: {
|
|
|
113
113
|
p.status === "SUCCEEDED" &&
|
|
114
114
|
p.model === r.model &&
|
|
115
115
|
(p.temperature ?? null) === (r.temperature ?? null) &&
|
|
116
|
+
(p.maxOutputTokens ?? null) === (r.maxOutputTokens ?? null) &&
|
|
116
117
|
p.promptVersion === r.promptVersion &&
|
|
117
118
|
p.graderVersion === r.graderVersion &&
|
|
118
119
|
+r.startedAt - +p.startedAt >= DAY,
|