mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
3f5f211a22
Record finalContextTokens per attempt: the input-token total of the last model request (input + cache-creation + cache-read), i.e. how full the context window ended up. Complements the cumulative tokenUsage.prompt, which conflates context size with loop-iteration count. Captured generically in the shared frontend runEval via the chat loop's lastIterationUsage, so it covers all frontend modes (global/flow/script/ app), plus CLI mode via the last assistant turn's usage. Aggregated as average and max over passed attempts and printed in the run summary. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
308 lines
9.2 KiB
TypeScript
308 lines
9.2 KiB
TypeScript
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
appendHistoryRecord,
|
|
buildRunResult,
|
|
formatRunSummary,
|
|
} from "./results";
|
|
import type { BenchmarkCaseResult } from "./types";
|
|
|
|
function caseResult(
|
|
attempts: BenchmarkCaseResult["attempts"],
|
|
): BenchmarkCaseResult {
|
|
return {
|
|
id: "case-1",
|
|
prompt: "Do the thing",
|
|
attempts,
|
|
};
|
|
}
|
|
|
|
describe("benchmark results", () => {
|
|
it("keeps success cost metrics separate from failed attempts", () => {
|
|
const result = buildRunResult({
|
|
mode: "global",
|
|
runs: 1,
|
|
runModel: "model-under-test",
|
|
judgeModel: "judge-model",
|
|
caseResults: [
|
|
caseResult([
|
|
{
|
|
attempt: 1,
|
|
passed: true,
|
|
durationMs: 1000,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 1,
|
|
toolsUsed: ["edit_script"],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: true }],
|
|
judgeScore: 100,
|
|
judgeSummary: "ok",
|
|
error: null,
|
|
tokenUsage: { prompt: 100, completion: 20, total: 120 },
|
|
},
|
|
{
|
|
attempt: 2,
|
|
passed: false,
|
|
durationMs: 100,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 0,
|
|
toolsUsed: [],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: false }],
|
|
judgeScore: 10,
|
|
judgeSummary: "missed",
|
|
error: "failed",
|
|
tokenUsage: { prompt: 10, completion: 5, total: 15 },
|
|
},
|
|
]),
|
|
],
|
|
});
|
|
|
|
expect(result.attemptCount).toBe(2);
|
|
expect(result.passedAttempts).toBe(1);
|
|
expect(result.passRate).toBe(0.5);
|
|
expect(result.averageDurationMs).toBe(550);
|
|
expect(result.averagePassedDurationMs).toBe(1000);
|
|
expect(result.totalTokenUsage).toEqual({
|
|
prompt: 110,
|
|
completion: 25,
|
|
total: 135,
|
|
});
|
|
expect(result.totalPassedTokenUsage).toEqual({
|
|
prompt: 100,
|
|
completion: 20,
|
|
total: 120,
|
|
});
|
|
expect(result.averageTokenUsagePerAttempt).toEqual({
|
|
prompt: 55,
|
|
completion: 12.5,
|
|
total: 67.5,
|
|
});
|
|
expect(result.averageTokenUsagePerPassedAttempt).toEqual({
|
|
prompt: 100,
|
|
completion: 20,
|
|
total: 120,
|
|
});
|
|
|
|
const summary = formatRunSummary(result);
|
|
expect(summary).toContain("Average duration (passed): 1000ms");
|
|
expect(summary).toContain("Average tokens (passed): 120 total");
|
|
expect(summary).toContain("Average duration (all attempts): 550ms");
|
|
});
|
|
|
|
it("aggregates final context size over passed attempts only", () => {
|
|
const result = buildRunResult({
|
|
mode: "global",
|
|
runs: 1,
|
|
runModel: "model-under-test",
|
|
judgeModel: "judge-model",
|
|
caseResults: [
|
|
caseResult([
|
|
{
|
|
attempt: 1,
|
|
passed: true,
|
|
durationMs: 1000,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 1,
|
|
toolsUsed: ["edit_script"],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: true }],
|
|
judgeScore: 100,
|
|
judgeSummary: "ok",
|
|
error: null,
|
|
tokenUsage: { prompt: 12000, completion: 200, total: 12200 },
|
|
finalContextTokens: 5000,
|
|
},
|
|
{
|
|
attempt: 2,
|
|
passed: true,
|
|
durationMs: 1100,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 1,
|
|
toolsUsed: ["edit_script"],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: true }],
|
|
judgeScore: 100,
|
|
judgeSummary: "ok",
|
|
error: null,
|
|
tokenUsage: { prompt: 18000, completion: 300, total: 18300 },
|
|
finalContextTokens: 7000,
|
|
},
|
|
{
|
|
attempt: 3,
|
|
passed: false,
|
|
durationMs: 100,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 0,
|
|
toolsUsed: [],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: false }],
|
|
judgeScore: 10,
|
|
judgeSummary: "missed",
|
|
error: "failed",
|
|
tokenUsage: { prompt: 20000, completion: 100, total: 20100 },
|
|
finalContextTokens: 9000,
|
|
},
|
|
]),
|
|
],
|
|
});
|
|
|
|
// Final context size stays below cumulative prompt and ignores the failed attempt.
|
|
expect(result.averageFinalContextTokensPassed).toBe(6000);
|
|
expect(result.maxFinalContextTokensPassed).toBe(7000);
|
|
expect(formatRunSummary(result)).toContain(
|
|
"Final context size (passed): 6000 tokens (max 7000)",
|
|
);
|
|
});
|
|
|
|
it("reports passed averages as unavailable when no attempt passes", () => {
|
|
const result = buildRunResult({
|
|
mode: "global",
|
|
runs: 1,
|
|
runModel: "model-under-test",
|
|
judgeModel: "judge-model",
|
|
caseResults: [
|
|
caseResult([
|
|
{
|
|
attempt: 1,
|
|
passed: false,
|
|
durationMs: 100,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 0,
|
|
toolsUsed: [],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: false }],
|
|
judgeScore: 10,
|
|
judgeSummary: "missed",
|
|
error: "failed",
|
|
tokenUsage: { prompt: 10, completion: 5, total: 15 },
|
|
},
|
|
]),
|
|
],
|
|
});
|
|
|
|
expect(result.averagePassedDurationMs).toBeNull();
|
|
expect(result.totalPassedTokenUsage).toBeNull();
|
|
expect(result.averageTokenUsagePerPassedAttempt).toBeNull();
|
|
expect(formatRunSummary(result)).toContain(
|
|
"Average duration (passed): n/a",
|
|
);
|
|
});
|
|
|
|
it("normalizes passed token averages by passed attempts", () => {
|
|
const result = buildRunResult({
|
|
mode: "global",
|
|
runs: 1,
|
|
runModel: "model-under-test",
|
|
judgeModel: "judge-model",
|
|
caseResults: [
|
|
caseResult([
|
|
{
|
|
attempt: 1,
|
|
passed: true,
|
|
durationMs: 1000,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 1,
|
|
toolsUsed: ["edit_script"],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: true }],
|
|
judgeScore: 100,
|
|
judgeSummary: "ok",
|
|
error: null,
|
|
tokenUsage: { prompt: 100, completion: 20, total: 120 },
|
|
},
|
|
{
|
|
attempt: 2,
|
|
passed: true,
|
|
durationMs: 1200,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 1,
|
|
toolsUsed: ["edit_script"],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: true }],
|
|
judgeScore: 100,
|
|
judgeSummary: "ok",
|
|
error: null,
|
|
tokenUsage: null,
|
|
},
|
|
]),
|
|
],
|
|
});
|
|
|
|
expect(result.passedAttempts).toBe(2);
|
|
expect(result.totalPassedTokenUsage).toEqual({
|
|
prompt: 100,
|
|
completion: 20,
|
|
total: 120,
|
|
});
|
|
expect(result.averageTokenUsagePerPassedAttempt).toEqual({
|
|
prompt: 50,
|
|
completion: 10,
|
|
total: 60,
|
|
});
|
|
});
|
|
|
|
it("records passed-attempt metrics in history", async () => {
|
|
const tempDir = await mkdtemp(join(tmpdir(), "windmill-ai-evals-"));
|
|
try {
|
|
const historyPath = join(tempDir, "history.jsonl");
|
|
const result = buildRunResult({
|
|
mode: "global",
|
|
runs: 1,
|
|
runModel: "model-under-test",
|
|
judgeModel: "judge-model",
|
|
caseResults: [
|
|
caseResult([
|
|
{
|
|
attempt: 1,
|
|
passed: true,
|
|
durationMs: 1000,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 1,
|
|
toolsUsed: ["edit_script"],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: true }],
|
|
judgeScore: 100,
|
|
judgeSummary: "ok",
|
|
error: null,
|
|
tokenUsage: { prompt: 100, completion: 20, total: 120 },
|
|
},
|
|
{
|
|
attempt: 2,
|
|
passed: false,
|
|
durationMs: 100,
|
|
assistantMessageCount: 1,
|
|
toolCallCount: 0,
|
|
toolsUsed: [],
|
|
skillsInvoked: [],
|
|
checks: [{ name: "edited", passed: false }],
|
|
judgeScore: 10,
|
|
judgeSummary: "missed",
|
|
error: "failed",
|
|
tokenUsage: { prompt: 10, completion: 5, total: 15 },
|
|
},
|
|
]),
|
|
],
|
|
});
|
|
|
|
await appendHistoryRecord(result, historyPath);
|
|
const record = JSON.parse(await readFile(historyPath, "utf8"));
|
|
|
|
expect(record.averageDurationMs).toBe(550);
|
|
expect(record.averagePassedDurationMs).toBe(1000);
|
|
expect(record.averageTokenUsagePerAttempt.total).toBe(67.5);
|
|
expect(record.averageTokenUsagePerPassedAttempt.total).toBe(120);
|
|
expect(record.cases[0].averageDurationMs).toBe(550);
|
|
expect(record.cases[0].averagePassedDurationMs).toBe(1000);
|
|
expect(record.cases[0].averageTokenUsagePerAttempt.total).toBe(67.5);
|
|
expect(record.cases[0].averageTokenUsagePerPassedAttempt.total).toBe(
|
|
120,
|
|
);
|
|
} finally {
|
|
await rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|