@unotest/judge 0.24.0 → 0.25.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.
@@ -1,715 +0,0 @@
1
- // src/errors.ts
2
- var JudgeError = class extends Error {
3
- constructor(message, context = {}) {
4
- super(message);
5
- this.context = context;
6
- this.name = new.target.name;
7
- }
8
- context;
9
- };
10
- var JudgeConfigError = class extends JudgeError {
11
- };
12
- var JudgeProviderError = class extends JudgeError {
13
- };
14
-
15
- // src/providers/fake.ts
16
- var FAKE_MODEL_ID = "fake";
17
- var CONSTRAINT_RE = /^must(?<not>\s+not)?\s+contain:\s*(?<needle>.+)$/i;
18
- function parseFakeRubric(rubric) {
19
- const constraints = [];
20
- for (const line of rubric.split(/\r?\n/)) {
21
- const m = CONSTRAINT_RE.exec(line.trim());
22
- if (!m?.groups?.needle) continue;
23
- constraints.push({
24
- kind: m.groups.not ? "not-contains" : "contains",
25
- needle: m.groups.needle.trim()
26
- });
27
- }
28
- return constraints;
29
- }
30
- var FakeJudgeProvider = class {
31
- async judgeOnce(request) {
32
- const constraints = parseFakeRubric(request.rubric);
33
- if (constraints.length === 0) {
34
- return {
35
- pass: false,
36
- reasoning: 'fake provider: rubric has no parseable constraints \u2014 use lines like "must contain: <substring>" / "must not contain: <substring>", or switch to a real provider (UNOTEST_JUDGE_PROVIDER=vertex|claude|gemini|openai|anthropic)',
37
- model: FAKE_MODEL_ID
38
- };
39
- }
40
- const text = request.text.toLowerCase();
41
- const violations = [];
42
- for (const c of constraints) {
43
- const hit = text.includes(c.needle.toLowerCase());
44
- if (c.kind === "contains" && !hit) violations.push(`missing required "${c.needle}"`);
45
- if (c.kind === "not-contains" && hit) violations.push(`contains forbidden "${c.needle}"`);
46
- }
47
- if (violations.length > 0) {
48
- return { pass: false, reasoning: violations.join("; "), model: FAKE_MODEL_ID };
49
- }
50
- return {
51
- pass: true,
52
- reasoning: `all ${constraints.length} constraint(s) satisfied`,
53
- model: FAKE_MODEL_ID
54
- };
55
- }
56
- };
57
-
58
- // src/verdict.ts
59
- var VERDICT_PROMPT = (rubric, text) => `You are a strict test judge. Evaluate the TEXT against the RUBRIC.
60
- Reply with ONLY a raw JSON object, no markdown code fences: {"verdict": "pass" | "fail", "reasoning": "<one short sentence>"}.
61
- RUBRIC:
62
- ${rubric}
63
-
64
- TEXT:
65
- ${text}`;
66
- var FENCED_JSON = /^```(?:json)?\s*\n([\s\S]*?)\n\s*```$/;
67
- function parseVerdictReply(text, model) {
68
- const trimmed = text.trim();
69
- const raw = FENCED_JSON.exec(trimmed)?.[1] ?? trimmed;
70
- let parsed;
71
- try {
72
- parsed = JSON.parse(raw);
73
- } catch {
74
- throw new JudgeProviderError(
75
- `model "${model}" did not reply with the requested JSON verdict: ${text.slice(0, 200)}`,
76
- { model }
77
- );
78
- }
79
- const { verdict, reasoning } = parsed;
80
- if (verdict !== "pass" && verdict !== "fail") {
81
- throw new JudgeProviderError(
82
- `model "${model}" replied with an unknown verdict ${JSON.stringify(verdict)} (expected "pass"/"fail")`,
83
- { model }
84
- );
85
- }
86
- return {
87
- pass: verdict === "pass",
88
- reasoning: typeof reasoning === "string" && reasoning.length > 0 ? reasoning : "(no reasoning)"
89
- };
90
- }
91
-
92
- // src/providers/adc-auth.ts
93
- async function buildAdcTokenSource() {
94
- const specifier = "google-auth-library";
95
- let mod;
96
- try {
97
- mod = await import(specifier);
98
- } catch (e) {
99
- throw new JudgeConfigError(
100
- `Vertex provider could not load "google-auth-library" (a dependency of this package \u2014 a broken install?): ${e instanceof Error ? e.message : String(e)}. Workaround: pass a token via UNOTEST_JUDGE_ACCESS_TOKEN (\`gcloud auth print-access-token\`)`
101
- );
102
- }
103
- const auth = new mod.GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] });
104
- return async () => {
105
- let token;
106
- try {
107
- token = await auth.getAccessToken();
108
- } catch (e) {
109
- throw describeAdcFailure(e);
110
- }
111
- if (!token) {
112
- throw new JudgeConfigError(
113
- "ADC produced no access token \u2014 run `gcloud auth application-default login` or set GOOGLE_APPLICATION_CREDENTIALS"
114
- );
115
- }
116
- return token;
117
- };
118
- }
119
- var REAUTH_CODES = /* @__PURE__ */ new Set(["invalid_grant", "invalid_rapt"]);
120
- function describeAdcFailure(e) {
121
- const body = oauthErrorBody(e);
122
- if (body && (REAUTH_CODES.has(body.error) || REAUTH_CODES.has(body.error_subtype ?? ""))) {
123
- return new JudgeConfigError(
124
- `Application Default Credentials need re-authentication (${body.error}${body.error_subtype ? `/${body.error_subtype}` : ""}) \u2014 run \`gcloud auth application-default login\``,
125
- { oauthError: body.error }
126
- );
127
- }
128
- const detail = body?.error_description ?? body?.error ?? (e instanceof Error ? e.message : String(e));
129
- return new JudgeConfigError(
130
- `Application Default Credentials failed: ${detail} \u2014 check GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION and \`gcloud auth application-default login\``
131
- );
132
- }
133
- function oauthErrorBody(e) {
134
- if (typeof e !== "object" || e === null) return void 0;
135
- const data = e.response?.data;
136
- if (typeof data !== "object" || data === null) return void 0;
137
- const body = data;
138
- return typeof body.error === "string" ? body : void 0;
139
- }
140
-
141
- // src/providers/generate-content.ts
142
- function generateContentBody(request) {
143
- return {
144
- contents: [
145
- { role: "user", parts: [{ text: VERDICT_PROMPT(request.rubric, request.text) }] }
146
- ],
147
- generationConfig: {
148
- temperature: 0,
149
- responseMimeType: "application/json"
150
- }
151
- };
152
- }
153
- function extractCandidateText(body, model, backend) {
154
- let parsed;
155
- try {
156
- parsed = JSON.parse(body);
157
- } catch {
158
- throw new JudgeProviderError(`${backend} reply is not JSON: ${body.slice(0, 200)}`, { model });
159
- }
160
- const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
161
- if (typeof text !== "string" || text.length === 0) {
162
- throw new JudgeProviderError(
163
- `${backend} reply carries no candidate text (blocked or empty): ${body.slice(0, 300)}`,
164
- { model }
165
- );
166
- }
167
- return text;
168
- }
169
-
170
- // src/providers/http-json.ts
171
- async function postJson(call) {
172
- const { url, headers, body, timeoutMs, backend, model, fetchImpl } = call;
173
- let res;
174
- try {
175
- res = await fetchImpl(url, {
176
- method: "POST",
177
- headers: { "content-type": "application/json", ...headers },
178
- body: JSON.stringify(body),
179
- signal: AbortSignal.timeout(timeoutMs)
180
- });
181
- } catch (e) {
182
- throw new JudgeProviderError(
183
- `cannot reach ${backend} at ${url} (${e instanceof Error ? e.message : String(e)})`,
184
- { url }
185
- );
186
- }
187
- const text = await res.text();
188
- if (!res.ok) {
189
- throw new JudgeProviderError(
190
- `${backend} replied ${res.status} for model "${model}": ${text.slice(0, 500)}`,
191
- { status: res.status, model }
192
- );
193
- }
194
- return text;
195
- }
196
-
197
- // src/providers/vertex.ts
198
- var VertexJudgeProvider = class {
199
- constructor(opts) {
200
- this.opts = opts;
201
- this.fetchImpl = opts.fetchImpl ?? fetch;
202
- }
203
- opts;
204
- fetchImpl;
205
- tokenSource;
206
- async judgeOnce(request) {
207
- const { project, location, model, timeoutMs } = this.opts;
208
- const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${model}:generateContent`;
209
- const token = await this.accessToken();
210
- const body = await postJson({
211
- url,
212
- headers: { authorization: `Bearer ${token}` },
213
- body: generateContentBody(request),
214
- timeoutMs,
215
- backend: "Vertex AI",
216
- model,
217
- fetchImpl: this.fetchImpl
218
- });
219
- return { ...parseVerdictReply(extractCandidateText(body, model, "Vertex AI"), model), model };
220
- }
221
- async accessToken() {
222
- if (this.opts.accessToken) return this.opts.accessToken;
223
- this.tokenSource ??= await buildAdcTokenSource();
224
- return this.tokenSource();
225
- }
226
- };
227
-
228
- // src/providers/gemini.ts
229
- var GeminiJudgeProvider = class {
230
- constructor(opts) {
231
- this.opts = opts;
232
- this.fetchImpl = opts.fetchImpl ?? fetch;
233
- }
234
- opts;
235
- fetchImpl;
236
- async judgeOnce(request) {
237
- const { apiKey, model, timeoutMs } = this.opts;
238
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
239
- const body = await postJson({
240
- url,
241
- headers: { "x-goog-api-key": apiKey },
242
- body: generateContentBody(request),
243
- timeoutMs,
244
- backend: "Gemini API",
245
- model,
246
- fetchImpl: this.fetchImpl
247
- });
248
- return { ...parseVerdictReply(extractCandidateText(body, model, "Gemini API"), model), model };
249
- }
250
- };
251
-
252
- // src/providers/openai.ts
253
- var OpenAiJudgeProvider = class {
254
- constructor(opts) {
255
- this.opts = opts;
256
- this.fetchImpl = opts.fetchImpl ?? fetch;
257
- }
258
- opts;
259
- fetchImpl;
260
- async judgeOnce(request) {
261
- const { apiKey, model, timeoutMs } = this.opts;
262
- const body = await postJson({
263
- url: "https://api.openai.com/v1/chat/completions",
264
- headers: { authorization: `Bearer ${apiKey}` },
265
- body: {
266
- model,
267
- messages: [{ role: "user", content: VERDICT_PROMPT(request.rubric, request.text) }],
268
- response_format: { type: "json_object" }
269
- },
270
- timeoutMs,
271
- backend: "OpenAI",
272
- model,
273
- fetchImpl: this.fetchImpl
274
- });
275
- return { ...parseVerdictReply(extractMessageContent(body, model), model), model };
276
- }
277
- };
278
- function extractMessageContent(body, model) {
279
- let parsed;
280
- try {
281
- parsed = JSON.parse(body);
282
- } catch {
283
- throw new JudgeProviderError(`OpenAI reply is not JSON: ${body.slice(0, 200)}`, { model });
284
- }
285
- const content = parsed.choices?.[0]?.message?.content;
286
- if (typeof content !== "string" || content.length === 0) {
287
- throw new JudgeProviderError(
288
- `OpenAI reply carries no message content: ${body.slice(0, 300)}`,
289
- { model }
290
- );
291
- }
292
- return content;
293
- }
294
-
295
- // src/providers/anthropic.ts
296
- var ANTHROPIC_VERSION = "2023-06-01";
297
- var MAX_TOKENS = 1024;
298
- var AnthropicJudgeProvider = class {
299
- constructor(opts) {
300
- this.opts = opts;
301
- this.fetchImpl = opts.fetchImpl ?? fetch;
302
- }
303
- opts;
304
- fetchImpl;
305
- async judgeOnce(request) {
306
- const { apiKey, model, timeoutMs } = this.opts;
307
- const body = await postJson({
308
- url: "https://api.anthropic.com/v1/messages",
309
- headers: { "x-api-key": apiKey, "anthropic-version": ANTHROPIC_VERSION },
310
- body: {
311
- model,
312
- max_tokens: MAX_TOKENS,
313
- messages: [{ role: "user", content: VERDICT_PROMPT(request.rubric, request.text) }]
314
- },
315
- timeoutMs,
316
- backend: "Anthropic API",
317
- model,
318
- fetchImpl: this.fetchImpl
319
- });
320
- return { ...parseVerdictReply(extractTextBlock(body, model), model), model };
321
- }
322
- };
323
- function extractTextBlock(body, model) {
324
- let parsed;
325
- try {
326
- parsed = JSON.parse(body);
327
- } catch {
328
- throw new JudgeProviderError(`Anthropic API reply is not JSON: ${body.slice(0, 200)}`, {
329
- model
330
- });
331
- }
332
- const blocks = parsed.content;
333
- const text = blocks?.find((b) => b.type === "text")?.text;
334
- if (typeof text !== "string" || text.length === 0) {
335
- throw new JudgeProviderError(
336
- `Anthropic API reply carries no text block (refusal or empty): ${body.slice(0, 300)}`,
337
- { model }
338
- );
339
- }
340
- return text;
341
- }
342
-
343
- // src/providers/claude-cli.ts
344
- var MAX_STDOUT_BYTES = 10 * 1024 * 1024;
345
- var ClaudeCliJudgeProvider = class {
346
- constructor(opts) {
347
- this.opts = opts;
348
- }
349
- opts;
350
- async judgeOnce(request) {
351
- const { bin, model, timeoutMs } = this.opts;
352
- const args = [
353
- "-p",
354
- "--output-format",
355
- "json",
356
- "--strict-mcp-config",
357
- ...model ? ["--model", model] : [],
358
- VERDICT_PROMPT(request.rubric, request.text)
359
- ];
360
- const exec = this.opts.execImpl ?? await defaultExec();
361
- let stdout;
362
- try {
363
- ({ stdout } = await exec(bin, args, { timeout: timeoutMs, maxBuffer: MAX_STDOUT_BYTES }));
364
- } catch (e) {
365
- throw toTypedError(e, bin, timeoutMs);
366
- }
367
- const envelope = parseEnvelope(stdout, bin);
368
- const modelLabel = firstModelId(envelope) ?? model ?? "claude";
369
- if (envelope.is_error === true || typeof envelope.result !== "string" || !envelope.result) {
370
- throw new JudgeProviderError(
371
- `${bin} -p returned an error result: ${String(envelope.result ?? stdout).slice(0, 300)}`,
372
- { model: modelLabel }
373
- );
374
- }
375
- return { ...parseVerdictReply(envelope.result, modelLabel), model: modelLabel };
376
- }
377
- };
378
- async function defaultExec() {
379
- const { execFile } = await import("child_process");
380
- const { promisify } = await import("util");
381
- return promisify(execFile);
382
- }
383
- function toTypedError(e, bin, timeoutMs) {
384
- const err = e;
385
- if (err.code === "ENOENT") {
386
- return new JudgeConfigError(
387
- `Claude Code CLI not found ("${bin}") \u2014 install it (https://claude.com/claude-code) or point UNOTEST_JUDGE_CLAUDE_BIN at the binary`,
388
- { bin }
389
- );
390
- }
391
- if (err.code === "EACCES") {
392
- return new JudgeConfigError(`Claude Code CLI is not executable ("${bin}")`, { bin });
393
- }
394
- if (err.killed === true) {
395
- return new JudgeProviderError(`${bin} -p timed out after ${timeoutMs}ms`, { bin, timeoutMs });
396
- }
397
- const stderr = typeof err.stderr === "string" && err.stderr ? err.stderr : String(err.message ?? e);
398
- return new JudgeProviderError(`${bin} -p failed: ${stderr.slice(0, 500)}`, { bin });
399
- }
400
- function parseEnvelope(stdout, bin) {
401
- try {
402
- return JSON.parse(stdout);
403
- } catch {
404
- throw new JudgeProviderError(
405
- `${bin} -p did not return the JSON envelope: ${stdout.slice(0, 300)}`,
406
- { bin }
407
- );
408
- }
409
- }
410
- function firstModelId(envelope) {
411
- const keys = envelope.modelUsage ? Object.keys(envelope.modelUsage) : [];
412
- return keys.length > 0 ? keys[0] : void 0;
413
- }
414
-
415
- // src/policy.ts
416
- async function judgeWithRetries(provider, request, retries) {
417
- const maxAttempts = Math.max(0, retries) + 1;
418
- let last = null;
419
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
420
- const single = await provider.judgeOnce(request);
421
- last = {
422
- verdict: single.pass ? "pass" : "fail",
423
- reasoning: single.reasoning,
424
- model: single.model,
425
- attempts: attempt
426
- };
427
- if (single.pass) return last;
428
- }
429
- return last;
430
- }
431
- async function judgeWithVote(provider, request, votes) {
432
- const ballots = await Promise.all(
433
- Array.from({ length: votes }, () => provider.judgeOnce(request))
434
- );
435
- const passes = ballots.filter((b) => b.pass);
436
- const winners = passes.length * 2 > votes ? passes : ballots.filter((b) => !b.pass);
437
- const winner = winners[0];
438
- return {
439
- verdict: winner.pass ? "pass" : "fail",
440
- reasoning: `majority ${winners.length}/${votes}: ${winner.reasoning}`,
441
- model: winner.model,
442
- attempts: votes
443
- };
444
- }
445
-
446
- // src/env.ts
447
- import { JUDGE_ENV } from "@unotest/protocol";
448
- var PROVIDER_KINDS = [
449
- "fake",
450
- "vertex",
451
- "claude",
452
- "gemini",
453
- "openai",
454
- "anthropic"
455
- ];
456
- var DEFAULT_MODEL = {
457
- fake: void 0,
458
- vertex: "gemini-2.5-flash",
459
- gemini: "gemini-2.5-flash",
460
- openai: "gpt-5-mini",
461
- anthropic: "claude-haiku-4-5",
462
- claude: void 0
463
- };
464
- var API_KEY_ENV = {
465
- gemini: "GEMINI_API_KEY",
466
- openai: "OPENAI_API_KEY",
467
- anthropic: "ANTHROPIC_API_KEY"
468
- };
469
- var DEFAULT_RETRIES = 1;
470
- var DEFAULT_TIMEOUT_MS = 3e4;
471
- var DEFAULT_CLAUDE_TIMEOUT_MS = 12e4;
472
- function intEnv(env, name, fallback, min, max) {
473
- const raw = env[name];
474
- if (raw === void 0 || raw === "") return fallback;
475
- const n = Number.parseInt(raw, 10);
476
- if (Number.isNaN(n) || n < min || n > max) {
477
- throw new JudgeConfigError(`${name} must be an integer in [${min}, ${max}], got "${raw}"`);
478
- }
479
- return n;
480
- }
481
- function resolveJudgeServiceEnv(env) {
482
- const provider = env[JUDGE_ENV.provider];
483
- if (!provider || !PROVIDER_KINDS.includes(provider)) {
484
- const list = PROVIDER_KINDS.map((k) => `"${k}"`).join(" | ");
485
- throw new JudgeConfigError(
486
- provider === void 0 || provider === "" ? `${JUDGE_ENV.provider} is not set \u2014 one of ${list} ("fake" is deterministic and CI-safe)` : `${JUDGE_ENV.provider} must be one of ${list}, got "${provider}"`
487
- );
488
- }
489
- const votes = intEnv(env, JUDGE_ENV.vote, 1, 1, 9);
490
- if (votes % 2 === 0) {
491
- throw new JudgeConfigError(
492
- `${JUDGE_ENV.vote} must be odd (1, 3, 5, 7, 9) \u2014 an even vote can tie, and a tie has no verdict; got "${votes}"`
493
- );
494
- }
495
- const resolved = {
496
- provider,
497
- votes,
498
- retries: intEnv(env, JUDGE_ENV.retries, DEFAULT_RETRIES, 0, 10),
499
- timeoutMs: intEnv(
500
- env,
501
- JUDGE_ENV.callTimeoutMs,
502
- provider === "claude" ? DEFAULT_CLAUDE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS,
503
- 1e3,
504
- 6e5
505
- )
506
- };
507
- const model = env[JUDGE_ENV.model] || DEFAULT_MODEL[provider];
508
- if (model) resolved.model = model;
509
- if (provider === "vertex") {
510
- const project = env.GOOGLE_CLOUD_PROJECT;
511
- const location = env.GOOGLE_CLOUD_LOCATION;
512
- if (!project || !location) {
513
- throw new JudgeConfigError(
514
- `${JUDGE_ENV.provider}=vertex requires GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION (ADC auth, no API keys)`
515
- );
516
- }
517
- resolved.project = project;
518
- resolved.location = location;
519
- const accessToken = env[JUDGE_ENV.accessToken];
520
- if (accessToken) resolved.accessToken = accessToken;
521
- }
522
- const keyEnvName = API_KEY_ENV[provider];
523
- if (keyEnvName) {
524
- const apiKey = env[keyEnvName];
525
- if (!apiKey) {
526
- throw new JudgeConfigError(`${JUDGE_ENV.provider}=${provider} requires ${keyEnvName}`);
527
- }
528
- resolved.apiKey = apiKey;
529
- }
530
- if (provider === "claude") {
531
- resolved.claudeBin = env[JUDGE_ENV.claudeBin] || "claude";
532
- }
533
- return resolved;
534
- }
535
- function resolveJudgeServerEnv(env) {
536
- const resolved = {
537
- host: env[JUDGE_ENV.host] || "127.0.0.1",
538
- port: intEnv(env, JUDGE_ENV.port, 8790, 1, 65535)
539
- };
540
- const token = env[JUDGE_ENV.token];
541
- if (token) resolved.token = token;
542
- return resolved;
543
- }
544
-
545
- // src/judge-service.ts
546
- function buildProvider(env) {
547
- switch (env.provider) {
548
- case "fake":
549
- return new FakeJudgeProvider();
550
- case "vertex":
551
- return new VertexJudgeProvider({
552
- project: env.project,
553
- location: env.location,
554
- model: env.model,
555
- timeoutMs: env.timeoutMs,
556
- ...env.accessToken ? { accessToken: env.accessToken } : {}
557
- });
558
- case "gemini":
559
- return new GeminiJudgeProvider({
560
- apiKey: env.apiKey,
561
- model: env.model,
562
- timeoutMs: env.timeoutMs
563
- });
564
- case "openai":
565
- return new OpenAiJudgeProvider({
566
- apiKey: env.apiKey,
567
- model: env.model,
568
- timeoutMs: env.timeoutMs
569
- });
570
- case "anthropic":
571
- return new AnthropicJudgeProvider({
572
- apiKey: env.apiKey,
573
- model: env.model,
574
- timeoutMs: env.timeoutMs
575
- });
576
- case "claude":
577
- return new ClaudeCliJudgeProvider({
578
- bin: env.claudeBin,
579
- timeoutMs: env.timeoutMs,
580
- ...env.model ? { model: env.model } : {}
581
- });
582
- }
583
- }
584
- function createJudgeService(env, provider) {
585
- const p = provider ?? buildProvider(env);
586
- return {
587
- judge: (request) => env.votes > 1 ? judgeWithVote(p, request, env.votes) : judgeWithRetries(p, request, env.retries)
588
- };
589
- }
590
- function createJudgeServiceFromEnv(env) {
591
- return createJudgeService(resolveJudgeServiceEnv(env));
592
- }
593
-
594
- // src/server.ts
595
- import { createServer } from "http";
596
- import {
597
- JUDGE_ENV as JUDGE_ENV2,
598
- JUDGE_ROUTES
599
- } from "@unotest/protocol";
600
- var MAX_BODY_BYTES = 1024 * 1024;
601
- function startJudgeServer(service, env) {
602
- const server = createServer((req, res) => {
603
- void route(service, env, req, res);
604
- });
605
- return new Promise((resolve, reject) => {
606
- server.once("error", reject);
607
- server.listen(env.port, env.host, () => {
608
- const addr = server.address();
609
- const port = typeof addr === "object" && addr !== null ? addr.port : env.port;
610
- resolve({
611
- server,
612
- port,
613
- close: () => new Promise((res2, rej2) => server.close((e) => e ? rej2(e) : res2()))
614
- });
615
- });
616
- });
617
- }
618
- async function route(service, env, req, res) {
619
- const url = req.url ?? "/";
620
- if (req.method === "GET" && url === JUDGE_ROUTES.health) {
621
- sendJson(res, 200, { ok: true });
622
- return;
623
- }
624
- if (req.method !== "POST" || url !== JUDGE_ROUTES.judge) {
625
- sendError(res, 404, `unknown route ${req.method} ${url}`, "bad-request");
626
- return;
627
- }
628
- if (env.token && req.headers.authorization !== `Bearer ${env.token}`) {
629
- sendError(res, 401, `missing or wrong bearer token (${JUDGE_ENV2.token})`, "unauthorized");
630
- return;
631
- }
632
- let body;
633
- try {
634
- body = parseRequest(await readBody(req));
635
- } catch (e) {
636
- sendError(res, 400, e instanceof Error ? e.message : String(e), "bad-request");
637
- return;
638
- }
639
- try {
640
- sendJson(res, 200, await service.judge(body));
641
- } catch (e) {
642
- if (e instanceof JudgeProviderError) {
643
- sendError(res, 502, e.message, "provider-error");
644
- } else if (e instanceof JudgeConfigError) {
645
- sendError(res, 500, e.message, "config");
646
- } else {
647
- sendError(res, 500, e instanceof Error ? e.message : String(e), "internal");
648
- }
649
- }
650
- }
651
- function parseRequest(raw) {
652
- let parsed;
653
- try {
654
- parsed = JSON.parse(raw);
655
- } catch {
656
- throw new Error("request body is not JSON");
657
- }
658
- const { rubric, text } = parsed;
659
- if (typeof rubric !== "string" || rubric.trim() === "") {
660
- throw new Error('request body needs a non-empty string "rubric"');
661
- }
662
- if (typeof text !== "string") {
663
- throw new Error('request body needs a string "text"');
664
- }
665
- return { rubric, text };
666
- }
667
- function readBody(req) {
668
- return new Promise((resolve, reject) => {
669
- const chunks = [];
670
- let size = 0;
671
- req.on("data", (chunk) => {
672
- size += chunk.length;
673
- if (size > MAX_BODY_BYTES) {
674
- reject(new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`));
675
- req.destroy();
676
- return;
677
- }
678
- chunks.push(chunk);
679
- });
680
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
681
- req.on("error", reject);
682
- });
683
- }
684
- function sendJson(res, status, body) {
685
- const payload = JSON.stringify(body);
686
- res.writeHead(status, { "content-type": "application/json" });
687
- res.end(payload);
688
- }
689
- function sendError(res, status, error, code) {
690
- sendJson(res, status, { error, code });
691
- }
692
-
693
- export {
694
- JudgeError,
695
- JudgeConfigError,
696
- JudgeProviderError,
697
- FAKE_MODEL_ID,
698
- parseFakeRubric,
699
- FakeJudgeProvider,
700
- VERDICT_PROMPT,
701
- parseVerdictReply,
702
- VertexJudgeProvider,
703
- GeminiJudgeProvider,
704
- OpenAiJudgeProvider,
705
- AnthropicJudgeProvider,
706
- ClaudeCliJudgeProvider,
707
- judgeWithRetries,
708
- resolveJudgeServiceEnv,
709
- resolveJudgeServerEnv,
710
- buildProvider,
711
- createJudgeService,
712
- createJudgeServiceFromEnv,
713
- startJudgeServer
714
- };
715
- //# sourceMappingURL=chunk-74FL5RWE.js.map