dsh-model-arena 0.2.1 → 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/lib/arena-view.d.ts +4 -2
- package/lib/arena-view.js +4 -12
- package/lib/arena.d.ts +11 -21
- package/lib/arena.js +66 -10
- package/lib/arena.web.js +644 -56
- package/lib/arena.web.js.map +4 -4
- package/lib/client/locales.d.ts +15 -0
- package/lib/client/locales.js +30 -0
- package/lib/client/ui.d.ts +124 -0
- package/lib/client/ui.js +388 -0
- package/lib/client/view.d.ts +3 -20
- package/lib/client/view.js +51 -51
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/routes.d.ts +2 -2
- package/lib/routes.js +51 -7
- package/lib/scoring.d.ts +6 -1
- package/lib/scoring.js +98 -0
- package/lib/store.d.ts +1 -0
- package/lib/store.js +8 -1
- package/lib/types.d.ts +50 -6
- package/package.json +1 -1
package/lib/routes.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import { ARENA_PANEL_PATH, ARENA_RUN_PATH, ARENA_RATE_PATH } from './arena-view.js';
|
|
2
|
-
export { ARENA_PANEL_PATH, ARENA_RUN_PATH, ARENA_RATE_PATH };
|
|
1
|
+
import { ARENA_PANEL_PATH, ARENA_RUN_PATH, ARENA_RATE_PATH, ARENA_COMPARE_PATH, ARENA_DELETE_PATH } from './arena-view.js';
|
|
2
|
+
export { ARENA_PANEL_PATH, ARENA_RUN_PATH, ARENA_RATE_PATH, ARENA_COMPARE_PATH, ARENA_DELETE_PATH };
|
|
3
3
|
export function registerArenaRoutes(ctx, arena) {
|
|
4
4
|
ctx.inject(['connection'], (connectionCtx) => {
|
|
5
5
|
const connection = connectionCtx.connection;
|
|
6
|
+
// Panel payload (with leaderboard + stats)
|
|
6
7
|
connection.fetch.register({
|
|
7
8
|
path: ARENA_PANEL_PATH,
|
|
8
9
|
methods: ['GET'],
|
|
9
10
|
requestBody: 'buffered',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
})),
|
|
11
|
+
fetch: () => Promise.resolve(Response.json({
|
|
12
|
+
recentRuns: arena.listRuns(),
|
|
13
|
+
leaderboard: arena.leaderboard(),
|
|
14
|
+
stats: arena.stats(),
|
|
15
|
+
}, { headers: { 'cache-control': 'no-store' } })),
|
|
15
16
|
});
|
|
17
|
+
// Run detail
|
|
16
18
|
connection.fetch.register({
|
|
17
19
|
path: ARENA_RUN_PATH,
|
|
18
20
|
methods: ['GET'],
|
|
@@ -28,6 +30,7 @@ export function registerArenaRoutes(ctx, arena) {
|
|
|
28
30
|
return Response.json(run, { headers: { 'cache-control': 'no-store' } });
|
|
29
31
|
},
|
|
30
32
|
});
|
|
33
|
+
// Rate a model in a run
|
|
31
34
|
connection.fetch.register({
|
|
32
35
|
path: ARENA_RATE_PATH,
|
|
33
36
|
methods: ['POST'],
|
|
@@ -49,5 +52,46 @@ export function registerArenaRoutes(ctx, arena) {
|
|
|
49
52
|
return Response.json(run, { headers: { 'cache-control': 'no-store' } });
|
|
50
53
|
},
|
|
51
54
|
});
|
|
55
|
+
// Compare two models in a run
|
|
56
|
+
connection.fetch.register({
|
|
57
|
+
path: ARENA_COMPARE_PATH,
|
|
58
|
+
methods: ['GET'],
|
|
59
|
+
requestBody: 'buffered',
|
|
60
|
+
fetch: async (request) => {
|
|
61
|
+
const url = new URL(request.url);
|
|
62
|
+
const runId = url.searchParams.get('runId');
|
|
63
|
+
const modelA = url.searchParams.get('modelA');
|
|
64
|
+
const modelB = url.searchParams.get('modelB');
|
|
65
|
+
if (!runId || !modelA || !modelB) {
|
|
66
|
+
return Response.json({ error: 'missing runId, modelA, or modelB' }, { status: 400 });
|
|
67
|
+
}
|
|
68
|
+
const result = arena.compare(runId, modelA, modelB);
|
|
69
|
+
if (!result)
|
|
70
|
+
return Response.json({ error: 'not found' }, { status: 404 });
|
|
71
|
+
return Response.json(result, { headers: { 'cache-control': 'no-store' } });
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
// Delete a run
|
|
75
|
+
connection.fetch.register({
|
|
76
|
+
path: ARENA_DELETE_PATH,
|
|
77
|
+
methods: ['POST'],
|
|
78
|
+
requestBody: 'buffered',
|
|
79
|
+
fetch: async (request) => {
|
|
80
|
+
let body;
|
|
81
|
+
try {
|
|
82
|
+
body = await request.json();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return Response.json({ error: 'request body must be JSON' }, { status: 400 });
|
|
86
|
+
}
|
|
87
|
+
if (!body.runId) {
|
|
88
|
+
return Response.json({ error: 'missing runId' }, { status: 400 });
|
|
89
|
+
}
|
|
90
|
+
const ok = arena.deleteRun(body.runId);
|
|
91
|
+
if (!ok)
|
|
92
|
+
return Response.json({ error: 'run not found' }, { status: 404 });
|
|
93
|
+
return Response.json({ ok: true }, { headers: { 'cache-control': 'no-store' } });
|
|
94
|
+
},
|
|
95
|
+
});
|
|
52
96
|
});
|
|
53
97
|
}
|
package/lib/scoring.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type { ModelResult, Score } from './types.js';
|
|
1
|
+
import type { ModelResult, Score, LeaderboardEntry } from './types.js';
|
|
2
2
|
/** Score outputs automatically based on simple metrics. */
|
|
3
3
|
export declare function autoScore(results: ModelResult[], reference?: string): Record<string, Score>;
|
|
4
4
|
/** Simple character-level similarity (Jaccard on character bigrams). */
|
|
5
5
|
export declare function similarity(a: string, b: string): number;
|
|
6
|
+
/** Build a leaderboard from all runs using Elo rating. */
|
|
7
|
+
export declare function buildLeaderboard(runs: {
|
|
8
|
+
results: ModelResult[];
|
|
9
|
+
scores: Record<string, Score>;
|
|
10
|
+
}[]): LeaderboardEntry[];
|
package/lib/scoring.js
CHANGED
|
@@ -33,3 +33,101 @@ export function similarity(a, b) {
|
|
|
33
33
|
intersection++;
|
|
34
34
|
return intersection / (bigramsA.size + bigramsB.size - intersection);
|
|
35
35
|
}
|
|
36
|
+
// ── Elo rating system ────────────────────────────────────────
|
|
37
|
+
const INITIAL_ELO = 1200;
|
|
38
|
+
const K_FACTOR = 32;
|
|
39
|
+
/** Compute expected score (probability of winning). */
|
|
40
|
+
function expectedScore(ratingA, ratingB) {
|
|
41
|
+
return 1 / (1 + Math.pow(10, (ratingB - ratingA) / 400));
|
|
42
|
+
}
|
|
43
|
+
/** Build a leaderboard from all runs using Elo rating. */
|
|
44
|
+
export function buildLeaderboard(runs) {
|
|
45
|
+
const records = new Map();
|
|
46
|
+
const getOrInit = (model) => {
|
|
47
|
+
if (!records.has(model)) {
|
|
48
|
+
records.set(model, { elo: INITIAL_ELO, wins: 0, losses: 0, draws: 0, games: 0, totalLatencyMs: 0, totalTokens: 0, totalRuns: 0, totalRating: 0, ratingCount: 0 });
|
|
49
|
+
}
|
|
50
|
+
return records.get(model);
|
|
51
|
+
};
|
|
52
|
+
for (const run of runs) {
|
|
53
|
+
// accumulate raw stats
|
|
54
|
+
for (const r of run.results) {
|
|
55
|
+
const rec = getOrInit(r.model);
|
|
56
|
+
rec.totalLatencyMs += r.latencyMs;
|
|
57
|
+
rec.totalTokens += r.promptTokens + r.completionTokens;
|
|
58
|
+
rec.totalRuns++;
|
|
59
|
+
if (r.rating !== undefined) {
|
|
60
|
+
rec.totalRating += r.rating;
|
|
61
|
+
rec.ratingCount++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// pairwise Elo updates based on user rating (if any) or composite auto-score
|
|
65
|
+
const models = run.results.map((r) => r.model);
|
|
66
|
+
for (let i = 0; i < models.length; i++) {
|
|
67
|
+
for (let j = i + 1; j < models.length; j++) {
|
|
68
|
+
const a = models[i];
|
|
69
|
+
const b = models[j];
|
|
70
|
+
const recA = getOrInit(a);
|
|
71
|
+
const recB = getOrInit(b);
|
|
72
|
+
const sA = compositeScore(run.scores[a]);
|
|
73
|
+
const sB = compositeScore(run.scores[b]);
|
|
74
|
+
const eA = expectedScore(recA.elo, recB.elo);
|
|
75
|
+
let actualA;
|
|
76
|
+
if (Math.abs(sA - sB) < 0.01) {
|
|
77
|
+
// draw
|
|
78
|
+
actualA = 0.5;
|
|
79
|
+
recA.draws++;
|
|
80
|
+
recB.draws++;
|
|
81
|
+
}
|
|
82
|
+
else if (sA > sB) {
|
|
83
|
+
actualA = 1;
|
|
84
|
+
recA.wins++;
|
|
85
|
+
recB.losses++;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
actualA = 0;
|
|
89
|
+
recA.losses++;
|
|
90
|
+
recB.wins++;
|
|
91
|
+
}
|
|
92
|
+
recA.elo += K_FACTOR * (actualA - eA);
|
|
93
|
+
recB.elo += K_FACTOR * ((1 - actualA) - (1 - eA));
|
|
94
|
+
recA.games++;
|
|
95
|
+
recB.games++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const entries = [];
|
|
100
|
+
for (const [model, rec] of records) {
|
|
101
|
+
entries.push({
|
|
102
|
+
model,
|
|
103
|
+
elo: Math.round(rec.elo),
|
|
104
|
+
wins: rec.wins,
|
|
105
|
+
losses: rec.losses,
|
|
106
|
+
draws: rec.draws,
|
|
107
|
+
games: rec.games,
|
|
108
|
+
totalRuns: rec.totalRuns,
|
|
109
|
+
avgRating: rec.ratingCount > 0 ? Math.round((rec.totalRating / rec.ratingCount) * 10) / 10 : 0,
|
|
110
|
+
avgLatencyMs: rec.games > 0 ? Math.round(rec.totalLatencyMs / rec.games) : 0,
|
|
111
|
+
avgTokens: rec.games > 0 ? Math.round(rec.totalTokens / rec.games) : 0,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
entries.sort((a, b) => b.elo - a.elo);
|
|
115
|
+
return entries;
|
|
116
|
+
}
|
|
117
|
+
/** Combine score dimensions into a single number for ranking. */
|
|
118
|
+
function compositeScore(s) {
|
|
119
|
+
if (!s)
|
|
120
|
+
return 0;
|
|
121
|
+
// User rating dominates if present (1-5 scale → 0-1)
|
|
122
|
+
if (s.userRating !== undefined)
|
|
123
|
+
return s.userRating / 5;
|
|
124
|
+
// Otherwise average available auto-scores
|
|
125
|
+
const parts = [];
|
|
126
|
+
if (s.referenceMatch !== undefined)
|
|
127
|
+
parts.push(s.referenceMatch);
|
|
128
|
+
if (s.lengthScore !== undefined)
|
|
129
|
+
parts.push(s.lengthScore);
|
|
130
|
+
if (s.latencyScore !== undefined)
|
|
131
|
+
parts.push(s.latencyScore);
|
|
132
|
+
return parts.length > 0 ? parts.reduce((a, b) => a + b, 0) / parts.length : 0;
|
|
133
|
+
}
|
package/lib/store.d.ts
CHANGED
package/lib/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
1
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
export class ArenaStore {
|
|
@@ -40,6 +40,13 @@ export class ArenaStore {
|
|
|
40
40
|
return null;
|
|
41
41
|
return JSON.parse(readFileSync(path, 'utf8'));
|
|
42
42
|
}
|
|
43
|
+
deleteRun(id) {
|
|
44
|
+
const path = join(this.arenaDir, `${id}.json`);
|
|
45
|
+
if (!existsSync(path))
|
|
46
|
+
return false;
|
|
47
|
+
unlinkSync(path);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
43
50
|
static generateId(prompt, models) {
|
|
44
51
|
const hash = createHash('sha256');
|
|
45
52
|
hash.update(prompt);
|
package/lib/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface ModelResult {
|
|
|
5
5
|
completionTokens: number;
|
|
6
6
|
latencyMs: number;
|
|
7
7
|
error?: string;
|
|
8
|
+
rating?: number;
|
|
8
9
|
}
|
|
9
10
|
export interface Score {
|
|
10
11
|
userRating?: number;
|
|
@@ -18,6 +19,7 @@ export interface ArenaRun {
|
|
|
18
19
|
timestamp: number;
|
|
19
20
|
results: ModelResult[];
|
|
20
21
|
scores: Record<string, Score>;
|
|
22
|
+
reference?: string | undefined;
|
|
21
23
|
}
|
|
22
24
|
export interface Suite {
|
|
23
25
|
name: string;
|
|
@@ -36,11 +38,53 @@ export interface RegressionReport {
|
|
|
36
38
|
currentOutput: string;
|
|
37
39
|
}[];
|
|
38
40
|
}
|
|
39
|
-
export interface
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
export interface LeaderboardEntry {
|
|
42
|
+
model: string;
|
|
43
|
+
elo: number;
|
|
44
|
+
wins: number;
|
|
45
|
+
losses: number;
|
|
46
|
+
draws: number;
|
|
47
|
+
games: number;
|
|
48
|
+
totalRuns: number;
|
|
49
|
+
avgRating: number;
|
|
50
|
+
avgLatencyMs: number;
|
|
51
|
+
avgTokens: number;
|
|
52
|
+
}
|
|
53
|
+
export interface RunSummary {
|
|
54
|
+
id: string;
|
|
55
|
+
prompt: string;
|
|
56
|
+
timestamp: number;
|
|
57
|
+
modelCount: number;
|
|
58
|
+
winner?: string | undefined;
|
|
59
|
+
}
|
|
60
|
+
export interface ComparePayload {
|
|
61
|
+
runId: string;
|
|
62
|
+
modelA: string;
|
|
63
|
+
modelB: string;
|
|
64
|
+
outputA: string;
|
|
65
|
+
outputB: string;
|
|
66
|
+
diff: {
|
|
67
|
+
type: 'same' | 'added' | 'removed';
|
|
68
|
+
text: string;
|
|
45
69
|
}[];
|
|
70
|
+
summary: {
|
|
71
|
+
added: number;
|
|
72
|
+
removed: number;
|
|
73
|
+
changed: number;
|
|
74
|
+
};
|
|
75
|
+
scoreA: Score;
|
|
76
|
+
scoreB: Score;
|
|
77
|
+
}
|
|
78
|
+
export interface ArenaStats {
|
|
79
|
+
totalRuns: number;
|
|
80
|
+
totalModels: number;
|
|
81
|
+
uniqueModels: number;
|
|
82
|
+
uniquePrompts: number;
|
|
83
|
+
totalRatings: number;
|
|
84
|
+
avgModelsPerRun: number;
|
|
85
|
+
}
|
|
86
|
+
export interface PanelPayload {
|
|
87
|
+
recentRuns: RunSummary[];
|
|
88
|
+
leaderboard: LeaderboardEntry[];
|
|
89
|
+
stats: ArenaStats;
|
|
46
90
|
}
|