@crowandraven/evalkeeper 0.1.1 → 0.3.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 +2 -2
- package/dist/attention.js +2 -1
- package/dist/attention.js.map +1 -1
- package/dist/board.d.ts +2 -0
- package/dist/glossary.d.ts +1 -1
- package/dist/glossary.js +1 -1
- package/dist/glossary.js.map +1 -1
- package/dist/health.js +1 -1
- package/dist/health.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 +3 -2
- package/dist/memoryStore.js.map +1 -1
- package/dist/models.d.ts +44 -7
- package/dist/models.js +48 -7
- package/dist/models.js.map +1 -1
- package/dist/runner.d.ts +25 -1
- package/dist/runner.js +64 -7
- package/dist/runner.js.map +1 -1
- package/dist/settings.d.ts +42 -3
- package/dist/settings.js +101 -11
- package/dist/settings.js.map +1 -1
- package/dist/store.d.ts +18 -2
- package/dist/suggest.js +4 -2
- package/dist/suggest.js.map +1 -1
- package/dist/testing.js +48 -0
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.js.map +1 -1
- package/dist/weekly.js +5 -2
- package/dist/weekly.js.map +1 -1
- package/package.json +2 -2
- package/src/attention.ts +2 -1
- package/src/board.ts +2 -0
- package/src/glossary.ts +1 -1
- package/src/health.ts +3 -1
- package/src/importCases.ts +252 -0
- package/src/index.ts +1 -0
- package/src/labels.ts +9 -4
- package/src/memoryStore.ts +6 -2
- package/src/models.ts +83 -10
- package/src/runner.ts +102 -15
- package/src/settings.ts +125 -14
- package/src/store.ts +19 -2
- package/src/suggest.ts +3 -1
- package/src/testing.ts +56 -0
- package/src/types.ts +11 -0
- package/src/weekly.ts +5 -2
- package/templates/app/evals/[suite]/page.tsx +1 -2
- package/templates/app/evals/feedback/page.tsx +11 -6
- package/templates/app/evals/page.tsx +40 -0
package/src/runner.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { compareRuns, type RunOutput } from "./compare.js";
|
|
|
9
9
|
import { isStuck, shouldContinue } from "./legs.js";
|
|
10
10
|
import {
|
|
11
11
|
effortFor,
|
|
12
|
+
isBaseline,
|
|
12
13
|
type ModelCatalog,
|
|
13
14
|
type ReasoningEffort,
|
|
14
15
|
} from "./models.js";
|
|
@@ -104,15 +105,27 @@ export type RunnerOptions = {
|
|
|
104
105
|
models: string[];
|
|
105
106
|
fallbackModels?: string[];
|
|
106
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* Which retiring models (`ModelInfo.retiring`) the board lists.
|
|
110
|
+
* "always" (the default): every candidate, though a retiring one is never
|
|
111
|
+
* suggested. "inUse": only one a feature still runs on, as its live or
|
|
112
|
+
* fallback model, so nobody spends a test on a model that's going away.
|
|
113
|
+
*/
|
|
114
|
+
retiringOnBoard?: "always" | "inUse";
|
|
107
115
|
};
|
|
108
116
|
|
|
109
117
|
/**
|
|
110
|
-
* Which model a feature runs on now, its fallback,
|
|
118
|
+
* Which model a feature runs on now, its fallback, its reasoning setting,
|
|
119
|
+
* its temperature, and its output cap.
|
|
111
120
|
*/
|
|
112
121
|
export type Route = {
|
|
113
122
|
model: string;
|
|
114
123
|
fallbackModel: string | null;
|
|
115
124
|
reasoningEffort: ReasoningEffort | null;
|
|
125
|
+
/** Null or unset: no temperature sent. */
|
|
126
|
+
temperature?: number | null;
|
|
127
|
+
/** The output-token cap; null or unset: the call's own default. */
|
|
128
|
+
maxOutputTokens?: number | null;
|
|
116
129
|
};
|
|
117
130
|
|
|
118
131
|
/**
|
|
@@ -143,6 +156,10 @@ export function requestHashFor(r: {
|
|
|
143
156
|
prompt: string;
|
|
144
157
|
input: unknown;
|
|
145
158
|
repeat: number;
|
|
159
|
+
/** Left out of the hash when null, so requests without one reuse as before. */
|
|
160
|
+
temperature?: number | null;
|
|
161
|
+
/** Left out of the hash when null, like temperature. */
|
|
162
|
+
maxOutputTokens?: number | null;
|
|
146
163
|
}): string {
|
|
147
164
|
return sha(
|
|
148
165
|
stableJson({
|
|
@@ -152,6 +169,10 @@ export function requestHashFor(r: {
|
|
|
152
169
|
prompt: r.prompt,
|
|
153
170
|
input: r.input,
|
|
154
171
|
repeat: r.repeat,
|
|
172
|
+
...(r.temperature != null ? { temperature: r.temperature } : {}),
|
|
173
|
+
...(r.maxOutputTokens != null
|
|
174
|
+
? { maxOutputTokens: r.maxOutputTokens }
|
|
175
|
+
: {}),
|
|
155
176
|
}),
|
|
156
177
|
64,
|
|
157
178
|
);
|
|
@@ -254,6 +275,18 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
254
275
|
return null;
|
|
255
276
|
}
|
|
256
277
|
|
|
278
|
+
/** The operation's live temperature: the route's, else none. */
|
|
279
|
+
async function liveTemperatureFor(operation: string): Promise<number | null> {
|
|
280
|
+
if (!opts.route) return null;
|
|
281
|
+
return (await opts.route(operation)).temperature ?? null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The operation's live output cap: the route's, else the call's own. */
|
|
285
|
+
async function liveCapFor(operation: string): Promise<number | null> {
|
|
286
|
+
if (!opts.route) return null;
|
|
287
|
+
return (await opts.route(operation)).maxOutputTokens ?? null;
|
|
288
|
+
}
|
|
289
|
+
|
|
257
290
|
/**
|
|
258
291
|
* What a test would cost. Answers an identical request gave in the last
|
|
259
292
|
* 7 days are reused for free, so only the rest are priced: a retest
|
|
@@ -270,6 +303,10 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
270
303
|
fresh?: boolean;
|
|
271
304
|
/** The setting to test; undefined: the operation's live one. */
|
|
272
305
|
reasoningEffort?: ReasoningEffort | null;
|
|
306
|
+
/** The temperature to test; undefined: the operation's live one. */
|
|
307
|
+
temperature?: number | null;
|
|
308
|
+
/** The output cap to test; undefined: the operation's live one. */
|
|
309
|
+
maxOutputTokens?: number | null;
|
|
273
310
|
} = {},
|
|
274
311
|
): Promise<{
|
|
275
312
|
cases: number;
|
|
@@ -280,14 +317,21 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
280
317
|
costUsd: number | null;
|
|
281
318
|
}> {
|
|
282
319
|
const suite = suiteOrThrow(suiteKey);
|
|
283
|
-
const [cases, perCall, prompt, effort] =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
320
|
+
const [cases, perCall, prompt, effort, temperature, cap] =
|
|
321
|
+
await Promise.all([
|
|
322
|
+
casesFor(suite.key, set),
|
|
323
|
+
opts.estimateCallCost?.(suite.operation, model) ?? null,
|
|
324
|
+
opts.promptVersion(suite.operation),
|
|
325
|
+
o.reasoningEffort !== undefined
|
|
326
|
+
? o.reasoningEffort
|
|
327
|
+
: liveEffortFor(suite.operation),
|
|
328
|
+
o.temperature !== undefined
|
|
329
|
+
? o.temperature
|
|
330
|
+
: liveTemperatureFor(suite.operation),
|
|
331
|
+
o.maxOutputTokens !== undefined
|
|
332
|
+
? o.maxOutputTokens
|
|
333
|
+
: liveCapFor(suite.operation),
|
|
334
|
+
]);
|
|
291
335
|
const answers = cases.length * repeats;
|
|
292
336
|
const since = new Date(Date.now() - REUSE_DAYS * 86_400_000);
|
|
293
337
|
const reused = o.fresh
|
|
@@ -304,6 +348,8 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
304
348
|
prompt,
|
|
305
349
|
input: c.input,
|
|
306
350
|
repeat: i + 1,
|
|
351
|
+
temperature,
|
|
352
|
+
maxOutputTokens: cap,
|
|
307
353
|
}),
|
|
308
354
|
since,
|
|
309
355
|
),
|
|
@@ -329,6 +375,10 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
329
375
|
startedBy?: string | null;
|
|
330
376
|
/** A reasoning setting to test; undefined: the operation's own. */
|
|
331
377
|
reasoningEffort?: ReasoningEffort | null;
|
|
378
|
+
/** A temperature to test; undefined: the operation's live one. */
|
|
379
|
+
temperature?: number | null;
|
|
380
|
+
/** An output cap to test; undefined: the operation's live one. */
|
|
381
|
+
maxOutputTokens?: number | null;
|
|
332
382
|
/** For the estimate: pass the same `fresh` to runEval. */
|
|
333
383
|
fresh?: boolean;
|
|
334
384
|
}) {
|
|
@@ -339,9 +389,19 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
339
389
|
args.reasoningEffort !== undefined
|
|
340
390
|
? args.reasoningEffort
|
|
341
391
|
: await liveEffortFor(suite.operation);
|
|
392
|
+
const temperature =
|
|
393
|
+
args.temperature !== undefined
|
|
394
|
+
? args.temperature
|
|
395
|
+
: await liveTemperatureFor(suite.operation);
|
|
396
|
+
const maxOutputTokens =
|
|
397
|
+
args.maxOutputTokens !== undefined
|
|
398
|
+
? args.maxOutputTokens
|
|
399
|
+
: await liveCapFor(suite.operation);
|
|
342
400
|
const [estimate, promptVersion] = await Promise.all([
|
|
343
401
|
estimateRun(suite.key, args.model, set, repeats, {
|
|
344
402
|
reasoningEffort,
|
|
403
|
+
temperature,
|
|
404
|
+
maxOutputTokens,
|
|
345
405
|
fresh: Boolean(args.fresh),
|
|
346
406
|
}),
|
|
347
407
|
opts.promptVersion(suite.operation),
|
|
@@ -353,6 +413,8 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
353
413
|
promptVersion,
|
|
354
414
|
reasoningEffort:
|
|
355
415
|
args.reasoningEffort !== undefined ? args.reasoningEffort : liveEffort,
|
|
416
|
+
temperature,
|
|
417
|
+
maxOutputTokens,
|
|
356
418
|
set,
|
|
357
419
|
repeats,
|
|
358
420
|
datasetVersion: null,
|
|
@@ -488,6 +550,8 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
488
550
|
prompt: run.promptVersion,
|
|
489
551
|
input: c.input,
|
|
490
552
|
repeat,
|
|
553
|
+
temperature: run.temperature ?? null,
|
|
554
|
+
maxOutputTokens: run.maxOutputTokens ?? null,
|
|
491
555
|
});
|
|
492
556
|
const earlier = o.fresh
|
|
493
557
|
? null
|
|
@@ -515,8 +579,9 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
515
579
|
const output = await withTimeout(
|
|
516
580
|
suite.run(suite.input.parse(c.input), {
|
|
517
581
|
model: run.model,
|
|
518
|
-
reasoningEffort:
|
|
519
|
-
|
|
582
|
+
reasoningEffort: run.reasoningEffort ?? null,
|
|
583
|
+
temperature: run.temperature ?? null,
|
|
584
|
+
maxOutputTokens: run.maxOutputTokens ?? null,
|
|
520
585
|
onCost: (usd) => (cost += usd),
|
|
521
586
|
}),
|
|
522
587
|
timeout,
|
|
@@ -663,6 +728,8 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
663
728
|
set: "SMOKE",
|
|
664
729
|
startedBy: SCHEDULED,
|
|
665
730
|
reasoningEffort: route.reasoningEffort,
|
|
731
|
+
temperature: route.temperature ?? null,
|
|
732
|
+
maxOutputTokens: route.maxOutputTokens ?? null,
|
|
666
733
|
});
|
|
667
734
|
return run.id;
|
|
668
735
|
}
|
|
@@ -771,7 +838,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
771
838
|
// The setting a model actually gets: models without reasoning settings
|
|
772
839
|
// ignore it, so their rows don't split by it.
|
|
773
840
|
const realEffort = (model: string, effort: string | null) =>
|
|
774
|
-
effortFor(opts.models, model, effort
|
|
841
|
+
effortFor(opts.models, model, effort) ?? null;
|
|
775
842
|
const keyOf = (model: string, effort: string | null) =>
|
|
776
843
|
`${model}|${realEffort(model, effort) ?? ""}`;
|
|
777
844
|
const runKey = (r: RunRecord) => keyOf(r.model, r.reasoningEffort);
|
|
@@ -784,8 +851,15 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
784
851
|
if (r.status === "SUCCEEDED" && !latestDone.has(runKey(r)))
|
|
785
852
|
latestDone.set(runKey(r), r);
|
|
786
853
|
}
|
|
854
|
+
const liveTemperature = route.temperature ?? null;
|
|
855
|
+
const liveCap = route.maxOutputTokens ?? null;
|
|
856
|
+
// A test at another temperature or output cap than the live ones
|
|
857
|
+
// answered differently.
|
|
787
858
|
const isCurrent = (r: RunRecord) =>
|
|
788
|
-
r.datasetVersion === current &&
|
|
859
|
+
r.datasetVersion === current &&
|
|
860
|
+
r.promptVersion === prompt &&
|
|
861
|
+
(r.temperature ?? null) === liveTemperature &&
|
|
862
|
+
(r.maxOutputTokens ?? null) === liveCap;
|
|
789
863
|
|
|
790
864
|
const liveKey = keyOf(route.model, defaultEffort);
|
|
791
865
|
const liveRun = latestDone.get(liveKey);
|
|
@@ -794,7 +868,9 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
794
868
|
const sameAsLive = (r: RunRecord) =>
|
|
795
869
|
liveRun !== undefined &&
|
|
796
870
|
r.datasetVersion === liveRun.datasetVersion &&
|
|
797
|
-
r.promptVersion === liveRun.promptVersion
|
|
871
|
+
r.promptVersion === liveRun.promptVersion &&
|
|
872
|
+
(r.temperature ?? null) === (liveRun.temperature ?? null) &&
|
|
873
|
+
(r.maxOutputTokens ?? null) === (liveRun.maxOutputTokens ?? null);
|
|
798
874
|
const outputs = new Map<string, RunOutput[]>();
|
|
799
875
|
for (const r of latestDone.values())
|
|
800
876
|
outputs.set(
|
|
@@ -811,7 +887,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
811
887
|
const checkKeys = suite.checks.map((c) => c.key);
|
|
812
888
|
|
|
813
889
|
const seen = new Set<string>();
|
|
814
|
-
|
|
890
|
+
let keys = [
|
|
815
891
|
...models.map((m) => ({
|
|
816
892
|
model: m,
|
|
817
893
|
effort: defaultEffort,
|
|
@@ -830,6 +906,13 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
830
906
|
seen.add(k);
|
|
831
907
|
return true;
|
|
832
908
|
});
|
|
909
|
+
// A retiring model stays on the board only while a feature runs on it.
|
|
910
|
+
const inUse = (model: string) =>
|
|
911
|
+
model === route.model || model === route.fallbackModel;
|
|
912
|
+
if (opts.retiringOnBoard === "inUse")
|
|
913
|
+
keys = keys.filter(
|
|
914
|
+
({ model }) => !opts.models[model]?.retiring || inUse(model),
|
|
915
|
+
);
|
|
833
916
|
const complete = (r: RunRecord | undefined) => !(r?.metrics?.failed ?? 0);
|
|
834
917
|
const estimates = new Map(
|
|
835
918
|
await Promise.all(
|
|
@@ -863,6 +946,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
863
946
|
.verdict
|
|
864
947
|
: null;
|
|
865
948
|
const label = opts.models[model]?.label ?? model;
|
|
949
|
+
const baseline = isBaseline(opts.models, model);
|
|
866
950
|
return {
|
|
867
951
|
key,
|
|
868
952
|
model,
|
|
@@ -873,6 +957,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
873
957
|
: label,
|
|
874
958
|
live: key === liveKey,
|
|
875
959
|
fallback: !variant && model === route.fallbackModel,
|
|
960
|
+
baseline,
|
|
876
961
|
testing: testing.has(key),
|
|
877
962
|
failed:
|
|
878
963
|
lastRun?.status === "FAILED"
|
|
@@ -900,6 +985,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
900
985
|
vsLive,
|
|
901
986
|
// Only on up-to-date tests: never promote on an old answer key.
|
|
902
987
|
canMakeLive:
|
|
988
|
+
!baseline &&
|
|
903
989
|
models.includes(model) &&
|
|
904
990
|
fresh &&
|
|
905
991
|
complete(r) &&
|
|
@@ -907,6 +993,7 @@ export function createRunner(opts: RunnerOptions) {
|
|
|
907
993
|
liveOnly: !fallbackModels.includes(model),
|
|
908
994
|
fallbackOnly: !models.includes(model),
|
|
909
995
|
canMakeFallback:
|
|
996
|
+
!baseline &&
|
|
910
997
|
fallbackModels.includes(model) &&
|
|
911
998
|
!variant &&
|
|
912
999
|
model !== route.model &&
|
package/src/settings.ts
CHANGED
|
@@ -12,7 +12,8 @@ import type { Route } from "./runner.js";
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* A feature's model settings: live model, fallback, reasoning setting,
|
|
15
|
+
* A feature's model settings: live model, fallback, reasoning setting,
|
|
16
|
+
* output limit, and temperature.
|
|
16
17
|
*/
|
|
17
18
|
export type Settings = {
|
|
18
19
|
model: string;
|
|
@@ -21,6 +22,12 @@ export type Settings = {
|
|
|
21
22
|
reasoningEffort: ReasoningEffort | null;
|
|
22
23
|
/** Null: the call's own default. */
|
|
23
24
|
maxOutputTokens: number | null;
|
|
25
|
+
/**
|
|
26
|
+
* The sampling temperature the feature sends. Null or unset: none sent,
|
|
27
|
+
* so the model's own default. Tests record it, and a test at another
|
|
28
|
+
* temperature than the live one is out of date.
|
|
29
|
+
*/
|
|
30
|
+
temperature?: number | null;
|
|
24
31
|
};
|
|
25
32
|
|
|
26
33
|
/**
|
|
@@ -106,13 +113,31 @@ export class SettingsConflict extends Error {
|
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
/**
|
|
117
|
+
* A feature has no settings: no version yet, and no default in code. Apps
|
|
118
|
+
* whose models are chosen in an admin page, not in code, see this until
|
|
119
|
+
* the feature's first version is recorded (`seed` or `change`).
|
|
120
|
+
*/
|
|
121
|
+
export class NoSettingsError extends Error {
|
|
122
|
+
/** The feature with no settings. */
|
|
123
|
+
readonly operation: string;
|
|
124
|
+
constructor(operation: string) {
|
|
125
|
+
super(
|
|
126
|
+
`${operation} has no settings yet: no version, and no default in code.`,
|
|
127
|
+
);
|
|
128
|
+
this.name = "NoSettingsError";
|
|
129
|
+
this.operation = operation;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
109
133
|
/** Whether two settings are the same in every field. */
|
|
110
134
|
export function sameSettings(a: Settings, b: Settings): boolean {
|
|
111
135
|
return (
|
|
112
136
|
a.model === b.model &&
|
|
113
137
|
a.fallbackModel === b.fallbackModel &&
|
|
114
138
|
a.reasoningEffort === b.reasoningEffort &&
|
|
115
|
-
a.maxOutputTokens === b.maxOutputTokens
|
|
139
|
+
a.maxOutputTokens === b.maxOutputTokens &&
|
|
140
|
+
(a.temperature ?? null) === (b.temperature ?? null)
|
|
116
141
|
);
|
|
117
142
|
}
|
|
118
143
|
|
|
@@ -123,6 +148,7 @@ export function settingsOf(v: Settings): Settings {
|
|
|
123
148
|
fallbackModel: v.fallbackModel,
|
|
124
149
|
reasoningEffort: v.reasoningEffort,
|
|
125
150
|
maxOutputTokens: v.maxOutputTokens,
|
|
151
|
+
temperature: v.temperature ?? null,
|
|
126
152
|
};
|
|
127
153
|
}
|
|
128
154
|
|
|
@@ -148,6 +174,10 @@ export function describeChange(
|
|
|
148
174
|
out.push(
|
|
149
175
|
`Output limit: ${from.maxOutputTokens ?? "default"} → ${to.maxOutputTokens ?? "default"}`,
|
|
150
176
|
);
|
|
177
|
+
if ((from.temperature ?? null) !== (to.temperature ?? null))
|
|
178
|
+
out.push(
|
|
179
|
+
`Temperature: ${from.temperature ?? "default"} → ${to.temperature ?? "default"}`,
|
|
180
|
+
);
|
|
151
181
|
return out;
|
|
152
182
|
}
|
|
153
183
|
|
|
@@ -175,11 +205,28 @@ export type HistoryEntry = SettingsChange & {
|
|
|
175
205
|
* });
|
|
176
206
|
* await settings.rollBack("ticket_triage", versionId, { by: admin.name, reason: "Worse in production" });
|
|
177
207
|
* ```
|
|
208
|
+
*
|
|
209
|
+
* When models are chosen in an admin page rather than in code, return null
|
|
210
|
+
* from `defaults` and record each feature's existing config as version 1
|
|
211
|
+
* with `seed`:
|
|
212
|
+
*
|
|
213
|
+
* ```ts
|
|
214
|
+
* const settings = createSettings({ store, defaults: () => null });
|
|
215
|
+
* for (const row of await db.aiConfig.findMany())
|
|
216
|
+
* await settings.seed(row.operation, settingsFromRow(row), {
|
|
217
|
+
* by: null,
|
|
218
|
+
* reason: "Imported from the admin config",
|
|
219
|
+
* });
|
|
220
|
+
* ```
|
|
178
221
|
*/
|
|
179
222
|
export function createSettings(opts: {
|
|
180
223
|
store: SettingsStore;
|
|
181
|
-
/**
|
|
182
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Each feature's settings in code, used until it's first changed. Null:
|
|
226
|
+
* the feature has no default, and reading its settings throws
|
|
227
|
+
* `NoSettingsError` until a version is recorded.
|
|
228
|
+
*/
|
|
229
|
+
defaults: (operation: string) => Settings | null;
|
|
183
230
|
/** The instructions' current hash, so a version records what it ran. */
|
|
184
231
|
promptVersion?: (operation: string) => string | Promise<string>;
|
|
185
232
|
}) {
|
|
@@ -187,32 +234,38 @@ export function createSettings(opts: {
|
|
|
187
234
|
const prompt = async (op: string) =>
|
|
188
235
|
opts.promptVersion ? await opts.promptVersion(op) : null;
|
|
189
236
|
|
|
190
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* The live settings: the live version's, else the default from code.
|
|
239
|
+
* Throws `NoSettingsError` when the feature has neither.
|
|
240
|
+
*/
|
|
191
241
|
async function current(
|
|
192
242
|
operation: string,
|
|
193
243
|
): Promise<{ settings: Settings; version: SettingsVersion | null }> {
|
|
194
244
|
const version = await store.liveVersion(operation);
|
|
195
|
-
return {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
};
|
|
245
|
+
if (version) return { settings: settingsOf(version), version };
|
|
246
|
+
const fallback = opts.defaults(operation);
|
|
247
|
+
if (!fallback) throw new NoSettingsError(operation);
|
|
248
|
+
return { settings: fallback, version };
|
|
199
249
|
}
|
|
200
250
|
|
|
201
|
-
/** The runner's route for a feature, from its live settings. */
|
|
251
|
+
/** The runner's route for a feature, from its live settings; throws `NoSettingsError` when it has none. */
|
|
202
252
|
async function route(operation: string): Promise<Route> {
|
|
203
253
|
const { settings } = await current(operation);
|
|
204
254
|
return {
|
|
205
255
|
model: settings.model,
|
|
206
256
|
fallbackModel: settings.fallbackModel,
|
|
207
257
|
reasoningEffort: settings.reasoningEffort,
|
|
258
|
+
temperature: settings.temperature ?? null,
|
|
259
|
+
maxOutputTokens: settings.maxOutputTokens,
|
|
208
260
|
};
|
|
209
261
|
}
|
|
210
262
|
|
|
211
263
|
/**
|
|
212
264
|
* Makes new settings live as a new version. The first change to a
|
|
213
265
|
* feature first records its default as version 1, so there's always
|
|
214
|
-
* something to roll back to
|
|
215
|
-
*
|
|
266
|
+
* something to roll back to; a feature with no default gets `next` as
|
|
267
|
+
* version 1. With `evidence`, it's a promotion. Returns null when
|
|
268
|
+
* nothing changed.
|
|
216
269
|
*/
|
|
217
270
|
async function change(
|
|
218
271
|
operation: string,
|
|
@@ -223,6 +276,15 @@ export function createSettings(opts: {
|
|
|
223
276
|
let live = await store.liveVersion(operation);
|
|
224
277
|
if (!live) {
|
|
225
278
|
const initial = opts.defaults(operation);
|
|
279
|
+
if (!initial)
|
|
280
|
+
return store.switchTo(operation, {
|
|
281
|
+
create: { settings: settingsOf(next), promptVersion },
|
|
282
|
+
kind: by.evidence === undefined ? "CHANGE" : "PROMOTE",
|
|
283
|
+
reason: by.reason,
|
|
284
|
+
by: by.by,
|
|
285
|
+
evidence: by.evidence,
|
|
286
|
+
expectLive: null,
|
|
287
|
+
});
|
|
226
288
|
if (sameSettings(initial, next)) return null;
|
|
227
289
|
live = await store.switchTo(operation, {
|
|
228
290
|
create: { settings: initial, promptVersion },
|
|
@@ -242,7 +304,11 @@ export function createSettings(opts: {
|
|
|
242
304
|
});
|
|
243
305
|
}
|
|
244
306
|
|
|
245
|
-
/**
|
|
307
|
+
/**
|
|
308
|
+
* Changes only some settings (e.g. the fallback), keeping the rest.
|
|
309
|
+
* Throws `NoSettingsError` for a feature with no settings to keep: use
|
|
310
|
+
* `change` or `seed` with full settings first.
|
|
311
|
+
*/
|
|
246
312
|
async function update(
|
|
247
313
|
operation: string,
|
|
248
314
|
patch: Partial<Settings>,
|
|
@@ -301,6 +367,42 @@ export function createSettings(opts: {
|
|
|
301
367
|
}
|
|
302
368
|
}
|
|
303
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Records existing settings as a feature's version 1, for an app moving
|
|
372
|
+
* its model choices into versioned settings (an import from its own
|
|
373
|
+
* config table). Only when the feature has no version yet: otherwise it
|
|
374
|
+
* changes nothing and returns null, so an import can run again safely.
|
|
375
|
+
* `promptVersion` defaults to the instructions' current hash.
|
|
376
|
+
*/
|
|
377
|
+
async function seed(
|
|
378
|
+
operation: string,
|
|
379
|
+
initial: Settings,
|
|
380
|
+
by: {
|
|
381
|
+
by: string | null;
|
|
382
|
+
reason: string | null;
|
|
383
|
+
promptVersion?: string | null;
|
|
384
|
+
},
|
|
385
|
+
): Promise<SettingsVersion | null> {
|
|
386
|
+
if (await store.liveVersion(operation)) return null;
|
|
387
|
+
const promptVersion =
|
|
388
|
+
by.promptVersion !== undefined
|
|
389
|
+
? by.promptVersion
|
|
390
|
+
: await prompt(operation);
|
|
391
|
+
try {
|
|
392
|
+
return await store.switchTo(operation, {
|
|
393
|
+
create: { settings: settingsOf(initial), promptVersion },
|
|
394
|
+
kind: "CHANGE",
|
|
395
|
+
reason: by.reason,
|
|
396
|
+
by: by.by,
|
|
397
|
+
expectLive: null,
|
|
398
|
+
});
|
|
399
|
+
} catch (e) {
|
|
400
|
+
// Another request seeded it first: that version stands.
|
|
401
|
+
if (e instanceof SettingsConflict) return null;
|
|
402
|
+
throw e;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
304
406
|
/** Every switch, newest first, with the versions it went from and to. */
|
|
305
407
|
async function history(
|
|
306
408
|
operation: string,
|
|
@@ -324,7 +426,16 @@ export function createSettings(opts: {
|
|
|
324
426
|
});
|
|
325
427
|
}
|
|
326
428
|
|
|
327
|
-
return {
|
|
429
|
+
return {
|
|
430
|
+
current,
|
|
431
|
+
route,
|
|
432
|
+
change,
|
|
433
|
+
update,
|
|
434
|
+
rollBack,
|
|
435
|
+
syncPrompt,
|
|
436
|
+
seed,
|
|
437
|
+
history,
|
|
438
|
+
};
|
|
328
439
|
}
|
|
329
440
|
|
|
330
441
|
/** What createSettings returns. */
|
package/src/store.ts
CHANGED
|
@@ -77,6 +77,16 @@ export type RunRecord = {
|
|
|
77
77
|
promptVersion: string;
|
|
78
78
|
/** The reasoning setting tested; null: the model's own default. */
|
|
79
79
|
reasoningEffort: string | null;
|
|
80
|
+
/**
|
|
81
|
+
* The temperature tested; null or unset: none sent. Stores keep it, so a
|
|
82
|
+
* test at another temperature than the live one shows as out of date.
|
|
83
|
+
*/
|
|
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;
|
|
80
90
|
set: EvalSet;
|
|
81
91
|
repeats: number;
|
|
82
92
|
/** Changes when the cases or their answers do, so tests compare. */
|
|
@@ -134,8 +144,15 @@ export type NewCase = Omit<
|
|
|
134
144
|
* database, and run describeStoreContract against it.
|
|
135
145
|
*/
|
|
136
146
|
export interface EvalStore {
|
|
137
|
-
/**
|
|
138
|
-
|
|
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[]>;
|
|
139
156
|
createCase(data: NewCase): Promise<CaseRecord>;
|
|
140
157
|
/** Adds an answer to a case (answers are never overwritten). */
|
|
141
158
|
addLabel(caseId: string, label: NewLabel): Promise<LabelRecord>;
|
package/src/suggest.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { retiresLabel, type ModelCatalog } from "./models.js";
|
|
1
|
+
import { isBaseline, retiresLabel, type ModelCatalog } from "./models.js";
|
|
2
2
|
import type { Rate } from "./stats.js";
|
|
3
3
|
import type { CheckDef, SuitePriority } from "./types.js";
|
|
4
4
|
import type { Verdict } from "./compare.js";
|
|
@@ -109,8 +109,10 @@ export function suggest<R extends SuggestRow>(
|
|
|
109
109
|
const retiring = (r: SuggestRow) => retiresLabel(models, r.model);
|
|
110
110
|
const limitMs = priority.maxSeconds ? priority.maxSeconds * 1000 : Infinity;
|
|
111
111
|
const tooSlow = (r: SuggestRow) => ms(r) !== Infinity && ms(r) > limitMs;
|
|
112
|
+
// Baselines are code, compared on the board but never suggested.
|
|
112
113
|
const tested = rows.filter(
|
|
113
114
|
(r) =>
|
|
115
|
+
!isBaseline(models, r.model) &&
|
|
114
116
|
r.test &&
|
|
115
117
|
!r.test.stale &&
|
|
116
118
|
!r.test.failedAnswers &&
|
package/src/testing.ts
CHANGED
|
@@ -90,6 +90,10 @@ export function describeStoreContract(
|
|
|
90
90
|
expect((await store.listCases("s")).map((c) => c.id)).toEqual([
|
|
91
91
|
second.id,
|
|
92
92
|
]);
|
|
93
|
+
// On request, retired ones too, still oldest first.
|
|
94
|
+
expect(
|
|
95
|
+
(await store.listCases("s", { includeRetired: true })).map((c) => c.id),
|
|
96
|
+
).toEqual([first.id, second.id]);
|
|
93
97
|
});
|
|
94
98
|
|
|
95
99
|
it("adds answers without overwriting, and changes their status", async () => {
|
|
@@ -127,6 +131,28 @@ export function describeStoreContract(
|
|
|
127
131
|
expect(await store.getRun("missing")).toBeNull();
|
|
128
132
|
});
|
|
129
133
|
|
|
134
|
+
it("keeps the temperature a run was tested at", async () => {
|
|
135
|
+
const store = await make();
|
|
136
|
+
const warm = await store.createRun({ ...newRun("s"), temperature: 0.7 });
|
|
137
|
+
const none = await store.createRun({ ...newRun("s"), temperature: null });
|
|
138
|
+
expect((await store.getRun(warm.id))?.temperature).toBe(0.7);
|
|
139
|
+
expect((await store.getRun(none.id))?.temperature ?? null).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("keeps the output cap a run was tested at", async () => {
|
|
143
|
+
const store = await make();
|
|
144
|
+
const capped = await store.createRun({
|
|
145
|
+
...newRun("s"),
|
|
146
|
+
maxOutputTokens: 4000,
|
|
147
|
+
});
|
|
148
|
+
const none = await store.createRun({
|
|
149
|
+
...newRun("s"),
|
|
150
|
+
maxOutputTokens: null,
|
|
151
|
+
});
|
|
152
|
+
expect((await store.getRun(capped.id))?.maxOutputTokens).toBe(4000);
|
|
153
|
+
expect((await store.getRun(none.id))?.maxOutputTokens ?? null).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
130
156
|
it("updates a run only while it has the status it's guarded by", async () => {
|
|
131
157
|
const store = await make();
|
|
132
158
|
const r = await store.createRun(newRun("s"));
|
|
@@ -381,6 +407,36 @@ export function describeSettingsStoreContract(
|
|
|
381
407
|
expect(await store.listVersions("triage")).toHaveLength(2);
|
|
382
408
|
});
|
|
383
409
|
|
|
410
|
+
it("keeps a version's reasoning setting as named, even a newer one", async () => {
|
|
411
|
+
const store = await make();
|
|
412
|
+
const v1 = await store.switchTo("triage", {
|
|
413
|
+
...create("a", null),
|
|
414
|
+
create: {
|
|
415
|
+
settings: { ...settings("a"), reasoningEffort: "xhigh" },
|
|
416
|
+
promptVersion: "p1",
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
expect(v1.reasoningEffort).toBe("xhigh");
|
|
420
|
+
expect((await store.liveVersion("triage"))?.reasoningEffort).toBe(
|
|
421
|
+
"xhigh",
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("keeps a version's temperature", async () => {
|
|
426
|
+
const store = await make();
|
|
427
|
+
const v1 = await store.switchTo("triage", {
|
|
428
|
+
...create("a", null),
|
|
429
|
+
create: {
|
|
430
|
+
settings: { ...settings("a"), temperature: 0.4 },
|
|
431
|
+
promptVersion: "p1",
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
expect(v1.temperature).toBe(0.4);
|
|
435
|
+
expect((await store.liveVersion("triage"))?.temperature).toBe(0.4);
|
|
436
|
+
const v2 = await store.switchTo("triage", create("b", v1.id));
|
|
437
|
+
expect(v2.temperature ?? null).toBeNull();
|
|
438
|
+
});
|
|
439
|
+
|
|
384
440
|
it("keeps each feature's versions and live one apart", async () => {
|
|
385
441
|
const store = await make();
|
|
386
442
|
await store.switchTo("triage", create("a", null));
|
package/src/types.ts
CHANGED
|
@@ -108,6 +108,17 @@ export type Suite<Input, Output, Label> = {
|
|
|
108
108
|
model: string;
|
|
109
109
|
/** A reasoning setting to test; undefined: the operation's own. */
|
|
110
110
|
reasoningEffort?: ReasoningEffort | null;
|
|
111
|
+
/**
|
|
112
|
+
* The temperature the test runs at: the operation's live one when the
|
|
113
|
+
* test was started. Null: send none. Undefined from runners before
|
|
114
|
+
* temperature was versioned.
|
|
115
|
+
*/
|
|
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;
|
|
111
122
|
onCost: (usd: number) => void;
|
|
112
123
|
},
|
|
113
124
|
): Promise<Output>;
|
package/src/weekly.ts
CHANGED
|
@@ -104,13 +104,16 @@ export function weeklySummary(args: {
|
|
|
104
104
|
continue;
|
|
105
105
|
}
|
|
106
106
|
if (r.status !== "SUCCEEDED") continue;
|
|
107
|
-
// The last check of the same model,
|
|
108
|
-
// 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
|
+
// reason.
|
|
109
110
|
const prior = scheduled.find(
|
|
110
111
|
(p) =>
|
|
111
112
|
p.suite === r.suite &&
|
|
112
113
|
p.status === "SUCCEEDED" &&
|
|
113
114
|
p.model === r.model &&
|
|
115
|
+
(p.temperature ?? null) === (r.temperature ?? null) &&
|
|
116
|
+
(p.maxOutputTokens ?? null) === (r.maxOutputTokens ?? null) &&
|
|
114
117
|
p.promptVersion === r.promptVersion &&
|
|
115
118
|
p.graderVersion === r.graderVersion &&
|
|
116
119
|
+r.startedAt - +p.startedAt >= DAY,
|