@unotest/judge 0.23.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.
- package/CHANGELOG.md +179 -0
- package/README.md +126 -21
- package/dist/chunk-ADU4QPCJ.js +1157 -0
- package/dist/cli.js +72 -22
- package/dist/index.d.ts +188 -21
- package/dist/index.js +33 -3
- package/package.json +3 -10
- package/dist/chunk-4VUHPR26.js +0 -660
package/dist/chunk-4VUHPR26.js
DELETED
|
@@ -1,660 +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 (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/generate-content.ts
|
|
93
|
-
function generateContentBody(request) {
|
|
94
|
-
return {
|
|
95
|
-
contents: [
|
|
96
|
-
{ role: "user", parts: [{ text: VERDICT_PROMPT(request.rubric, request.text) }] }
|
|
97
|
-
],
|
|
98
|
-
generationConfig: {
|
|
99
|
-
temperature: 0,
|
|
100
|
-
responseMimeType: "application/json"
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function extractCandidateText(body, model, backend) {
|
|
105
|
-
let parsed;
|
|
106
|
-
try {
|
|
107
|
-
parsed = JSON.parse(body);
|
|
108
|
-
} catch {
|
|
109
|
-
throw new JudgeProviderError(`${backend} reply is not JSON: ${body.slice(0, 200)}`, { model });
|
|
110
|
-
}
|
|
111
|
-
const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
112
|
-
if (typeof text !== "string" || text.length === 0) {
|
|
113
|
-
throw new JudgeProviderError(
|
|
114
|
-
`${backend} reply carries no candidate text (blocked or empty): ${body.slice(0, 300)}`,
|
|
115
|
-
{ model }
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
return text;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// src/providers/http-json.ts
|
|
122
|
-
async function postJson(call) {
|
|
123
|
-
const { url, headers, body, timeoutMs, backend, model, fetchImpl } = call;
|
|
124
|
-
let res;
|
|
125
|
-
try {
|
|
126
|
-
res = await fetchImpl(url, {
|
|
127
|
-
method: "POST",
|
|
128
|
-
headers: { "content-type": "application/json", ...headers },
|
|
129
|
-
body: JSON.stringify(body),
|
|
130
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
131
|
-
});
|
|
132
|
-
} catch (e) {
|
|
133
|
-
throw new JudgeProviderError(
|
|
134
|
-
`cannot reach ${backend} at ${url} (${e instanceof Error ? e.message : String(e)})`,
|
|
135
|
-
{ url }
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
const text = await res.text();
|
|
139
|
-
if (!res.ok) {
|
|
140
|
-
throw new JudgeProviderError(
|
|
141
|
-
`${backend} replied ${res.status} for model "${model}": ${text.slice(0, 500)}`,
|
|
142
|
-
{ status: res.status, model }
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
return text;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// src/providers/vertex.ts
|
|
149
|
-
var VertexJudgeProvider = class {
|
|
150
|
-
constructor(opts) {
|
|
151
|
-
this.opts = opts;
|
|
152
|
-
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
153
|
-
}
|
|
154
|
-
opts;
|
|
155
|
-
fetchImpl;
|
|
156
|
-
tokenSource;
|
|
157
|
-
async judgeOnce(request) {
|
|
158
|
-
const { project, location, model, timeoutMs } = this.opts;
|
|
159
|
-
const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${model}:generateContent`;
|
|
160
|
-
const token = await this.accessToken();
|
|
161
|
-
const body = await postJson({
|
|
162
|
-
url,
|
|
163
|
-
headers: { authorization: `Bearer ${token}` },
|
|
164
|
-
body: generateContentBody(request),
|
|
165
|
-
timeoutMs,
|
|
166
|
-
backend: "Vertex AI",
|
|
167
|
-
model,
|
|
168
|
-
fetchImpl: this.fetchImpl
|
|
169
|
-
});
|
|
170
|
-
return { ...parseVerdictReply(extractCandidateText(body, model, "Vertex AI"), model), model };
|
|
171
|
-
}
|
|
172
|
-
async accessToken() {
|
|
173
|
-
if (this.opts.accessToken) return this.opts.accessToken;
|
|
174
|
-
this.tokenSource ??= await buildAdcTokenSource();
|
|
175
|
-
return this.tokenSource();
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
async function buildAdcTokenSource() {
|
|
179
|
-
const specifier = "google-auth-library";
|
|
180
|
-
let mod;
|
|
181
|
-
try {
|
|
182
|
-
mod = await import(specifier);
|
|
183
|
-
} catch {
|
|
184
|
-
throw new JudgeConfigError(
|
|
185
|
-
'Vertex provider needs Application Default Credentials \u2014 install the optional peer "google-auth-library" (`npm i google-auth-library`), or pass a token via JUDGE_ACCESS_TOKEN'
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
const auth = new mod.GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] });
|
|
189
|
-
return async () => {
|
|
190
|
-
const token = await auth.getAccessToken();
|
|
191
|
-
if (!token) {
|
|
192
|
-
throw new JudgeConfigError(
|
|
193
|
-
"ADC produced no access token \u2014 run `gcloud auth application-default login` or set GOOGLE_APPLICATION_CREDENTIALS"
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
return token;
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// src/providers/gemini.ts
|
|
201
|
-
var GeminiJudgeProvider = class {
|
|
202
|
-
constructor(opts) {
|
|
203
|
-
this.opts = opts;
|
|
204
|
-
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
205
|
-
}
|
|
206
|
-
opts;
|
|
207
|
-
fetchImpl;
|
|
208
|
-
async judgeOnce(request) {
|
|
209
|
-
const { apiKey, model, timeoutMs } = this.opts;
|
|
210
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
211
|
-
const body = await postJson({
|
|
212
|
-
url,
|
|
213
|
-
headers: { "x-goog-api-key": apiKey },
|
|
214
|
-
body: generateContentBody(request),
|
|
215
|
-
timeoutMs,
|
|
216
|
-
backend: "Gemini API",
|
|
217
|
-
model,
|
|
218
|
-
fetchImpl: this.fetchImpl
|
|
219
|
-
});
|
|
220
|
-
return { ...parseVerdictReply(extractCandidateText(body, model, "Gemini API"), model), model };
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
// src/providers/openai.ts
|
|
225
|
-
var OpenAiJudgeProvider = class {
|
|
226
|
-
constructor(opts) {
|
|
227
|
-
this.opts = opts;
|
|
228
|
-
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
229
|
-
}
|
|
230
|
-
opts;
|
|
231
|
-
fetchImpl;
|
|
232
|
-
async judgeOnce(request) {
|
|
233
|
-
const { apiKey, model, timeoutMs } = this.opts;
|
|
234
|
-
const body = await postJson({
|
|
235
|
-
url: "https://api.openai.com/v1/chat/completions",
|
|
236
|
-
headers: { authorization: `Bearer ${apiKey}` },
|
|
237
|
-
body: {
|
|
238
|
-
model,
|
|
239
|
-
messages: [{ role: "user", content: VERDICT_PROMPT(request.rubric, request.text) }],
|
|
240
|
-
response_format: { type: "json_object" }
|
|
241
|
-
},
|
|
242
|
-
timeoutMs,
|
|
243
|
-
backend: "OpenAI",
|
|
244
|
-
model,
|
|
245
|
-
fetchImpl: this.fetchImpl
|
|
246
|
-
});
|
|
247
|
-
return { ...parseVerdictReply(extractMessageContent(body, model), model), model };
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
function extractMessageContent(body, model) {
|
|
251
|
-
let parsed;
|
|
252
|
-
try {
|
|
253
|
-
parsed = JSON.parse(body);
|
|
254
|
-
} catch {
|
|
255
|
-
throw new JudgeProviderError(`OpenAI reply is not JSON: ${body.slice(0, 200)}`, { model });
|
|
256
|
-
}
|
|
257
|
-
const content = parsed.choices?.[0]?.message?.content;
|
|
258
|
-
if (typeof content !== "string" || content.length === 0) {
|
|
259
|
-
throw new JudgeProviderError(
|
|
260
|
-
`OpenAI reply carries no message content: ${body.slice(0, 300)}`,
|
|
261
|
-
{ model }
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
return content;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// src/providers/anthropic.ts
|
|
268
|
-
var ANTHROPIC_VERSION = "2023-06-01";
|
|
269
|
-
var MAX_TOKENS = 1024;
|
|
270
|
-
var AnthropicJudgeProvider = class {
|
|
271
|
-
constructor(opts) {
|
|
272
|
-
this.opts = opts;
|
|
273
|
-
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
274
|
-
}
|
|
275
|
-
opts;
|
|
276
|
-
fetchImpl;
|
|
277
|
-
async judgeOnce(request) {
|
|
278
|
-
const { apiKey, model, timeoutMs } = this.opts;
|
|
279
|
-
const body = await postJson({
|
|
280
|
-
url: "https://api.anthropic.com/v1/messages",
|
|
281
|
-
headers: { "x-api-key": apiKey, "anthropic-version": ANTHROPIC_VERSION },
|
|
282
|
-
body: {
|
|
283
|
-
model,
|
|
284
|
-
max_tokens: MAX_TOKENS,
|
|
285
|
-
messages: [{ role: "user", content: VERDICT_PROMPT(request.rubric, request.text) }]
|
|
286
|
-
},
|
|
287
|
-
timeoutMs,
|
|
288
|
-
backend: "Anthropic API",
|
|
289
|
-
model,
|
|
290
|
-
fetchImpl: this.fetchImpl
|
|
291
|
-
});
|
|
292
|
-
return { ...parseVerdictReply(extractTextBlock(body, model), model), model };
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
function extractTextBlock(body, model) {
|
|
296
|
-
let parsed;
|
|
297
|
-
try {
|
|
298
|
-
parsed = JSON.parse(body);
|
|
299
|
-
} catch {
|
|
300
|
-
throw new JudgeProviderError(`Anthropic API reply is not JSON: ${body.slice(0, 200)}`, {
|
|
301
|
-
model
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
const blocks = parsed.content;
|
|
305
|
-
const text = blocks?.find((b) => b.type === "text")?.text;
|
|
306
|
-
if (typeof text !== "string" || text.length === 0) {
|
|
307
|
-
throw new JudgeProviderError(
|
|
308
|
-
`Anthropic API reply carries no text block (refusal or empty): ${body.slice(0, 300)}`,
|
|
309
|
-
{ model }
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
return text;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// src/providers/claude-cli.ts
|
|
316
|
-
var MAX_STDOUT_BYTES = 10 * 1024 * 1024;
|
|
317
|
-
var ClaudeCliJudgeProvider = class {
|
|
318
|
-
constructor(opts) {
|
|
319
|
-
this.opts = opts;
|
|
320
|
-
}
|
|
321
|
-
opts;
|
|
322
|
-
async judgeOnce(request) {
|
|
323
|
-
const { bin, model, timeoutMs } = this.opts;
|
|
324
|
-
const args = [
|
|
325
|
-
"-p",
|
|
326
|
-
"--output-format",
|
|
327
|
-
"json",
|
|
328
|
-
"--strict-mcp-config",
|
|
329
|
-
...model ? ["--model", model] : [],
|
|
330
|
-
VERDICT_PROMPT(request.rubric, request.text)
|
|
331
|
-
];
|
|
332
|
-
const exec = this.opts.execImpl ?? await defaultExec();
|
|
333
|
-
let stdout;
|
|
334
|
-
try {
|
|
335
|
-
({ stdout } = await exec(bin, args, { timeout: timeoutMs, maxBuffer: MAX_STDOUT_BYTES }));
|
|
336
|
-
} catch (e) {
|
|
337
|
-
throw toTypedError(e, bin, timeoutMs);
|
|
338
|
-
}
|
|
339
|
-
const envelope = parseEnvelope(stdout, bin);
|
|
340
|
-
const modelLabel = firstModelId(envelope) ?? model ?? "claude";
|
|
341
|
-
if (envelope.is_error === true || typeof envelope.result !== "string" || !envelope.result) {
|
|
342
|
-
throw new JudgeProviderError(
|
|
343
|
-
`${bin} -p returned an error result: ${String(envelope.result ?? stdout).slice(0, 300)}`,
|
|
344
|
-
{ model: modelLabel }
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
return { ...parseVerdictReply(envelope.result, modelLabel), model: modelLabel };
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
async function defaultExec() {
|
|
351
|
-
const { execFile } = await import("child_process");
|
|
352
|
-
const { promisify } = await import("util");
|
|
353
|
-
return promisify(execFile);
|
|
354
|
-
}
|
|
355
|
-
function toTypedError(e, bin, timeoutMs) {
|
|
356
|
-
const err = e;
|
|
357
|
-
if (err.code === "ENOENT") {
|
|
358
|
-
return new JudgeConfigError(
|
|
359
|
-
`Claude Code CLI not found ("${bin}") \u2014 install it (https://claude.com/claude-code) or point JUDGE_CLAUDE_BIN at the binary`,
|
|
360
|
-
{ bin }
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
if (err.code === "EACCES") {
|
|
364
|
-
return new JudgeConfigError(`Claude Code CLI is not executable ("${bin}")`, { bin });
|
|
365
|
-
}
|
|
366
|
-
if (err.killed === true) {
|
|
367
|
-
return new JudgeProviderError(`${bin} -p timed out after ${timeoutMs}ms`, { bin, timeoutMs });
|
|
368
|
-
}
|
|
369
|
-
const stderr = typeof err.stderr === "string" && err.stderr ? err.stderr : String(err.message ?? e);
|
|
370
|
-
return new JudgeProviderError(`${bin} -p failed: ${stderr.slice(0, 500)}`, { bin });
|
|
371
|
-
}
|
|
372
|
-
function parseEnvelope(stdout, bin) {
|
|
373
|
-
try {
|
|
374
|
-
return JSON.parse(stdout);
|
|
375
|
-
} catch {
|
|
376
|
-
throw new JudgeProviderError(
|
|
377
|
-
`${bin} -p did not return the JSON envelope: ${stdout.slice(0, 300)}`,
|
|
378
|
-
{ bin }
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
function firstModelId(envelope) {
|
|
383
|
-
const keys = envelope.modelUsage ? Object.keys(envelope.modelUsage) : [];
|
|
384
|
-
return keys.length > 0 ? keys[0] : void 0;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// src/policy.ts
|
|
388
|
-
async function judgeWithRetries(provider, request, retries) {
|
|
389
|
-
const maxAttempts = Math.max(0, retries) + 1;
|
|
390
|
-
let last = null;
|
|
391
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
392
|
-
const single = await provider.judgeOnce(request);
|
|
393
|
-
last = {
|
|
394
|
-
verdict: single.pass ? "pass" : "fail",
|
|
395
|
-
reasoning: single.reasoning,
|
|
396
|
-
model: single.model,
|
|
397
|
-
attempts: attempt
|
|
398
|
-
};
|
|
399
|
-
if (single.pass) return last;
|
|
400
|
-
}
|
|
401
|
-
return last;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// src/env.ts
|
|
405
|
-
var PROVIDER_KINDS = [
|
|
406
|
-
"fake",
|
|
407
|
-
"vertex",
|
|
408
|
-
"claude",
|
|
409
|
-
"gemini",
|
|
410
|
-
"openai",
|
|
411
|
-
"anthropic"
|
|
412
|
-
];
|
|
413
|
-
var DEFAULT_MODEL = {
|
|
414
|
-
fake: void 0,
|
|
415
|
-
vertex: "gemini-2.5-flash",
|
|
416
|
-
gemini: "gemini-2.5-flash",
|
|
417
|
-
openai: "gpt-5-mini",
|
|
418
|
-
anthropic: "claude-haiku-4-5",
|
|
419
|
-
claude: void 0
|
|
420
|
-
};
|
|
421
|
-
var API_KEY_ENV = {
|
|
422
|
-
gemini: "GEMINI_API_KEY",
|
|
423
|
-
openai: "OPENAI_API_KEY",
|
|
424
|
-
anthropic: "ANTHROPIC_API_KEY"
|
|
425
|
-
};
|
|
426
|
-
var DEFAULT_RETRIES = 1;
|
|
427
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
428
|
-
var DEFAULT_CLAUDE_TIMEOUT_MS = 12e4;
|
|
429
|
-
function intEnv(env, name, fallback, min, max) {
|
|
430
|
-
const raw = env[name];
|
|
431
|
-
if (raw === void 0 || raw === "") return fallback;
|
|
432
|
-
const n = Number.parseInt(raw, 10);
|
|
433
|
-
if (Number.isNaN(n) || n < min || n > max) {
|
|
434
|
-
throw new JudgeConfigError(`${name} must be an integer in [${min}, ${max}], got "${raw}"`);
|
|
435
|
-
}
|
|
436
|
-
return n;
|
|
437
|
-
}
|
|
438
|
-
function resolveJudgeServiceEnv(env) {
|
|
439
|
-
const provider = env.JUDGE_PROVIDER;
|
|
440
|
-
if (!provider || !PROVIDER_KINDS.includes(provider)) {
|
|
441
|
-
const list = PROVIDER_KINDS.map((k) => `"${k}"`).join(" | ");
|
|
442
|
-
throw new JudgeConfigError(
|
|
443
|
-
provider === void 0 || provider === "" ? `JUDGE_PROVIDER is not set \u2014 one of ${list} ("fake" is deterministic and CI-safe)` : `JUDGE_PROVIDER must be one of ${list}, got "${provider}"`
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
const resolved = {
|
|
447
|
-
provider,
|
|
448
|
-
retries: intEnv(env, "JUDGE_RETRIES", DEFAULT_RETRIES, 0, 10),
|
|
449
|
-
timeoutMs: intEnv(
|
|
450
|
-
env,
|
|
451
|
-
"JUDGE_TIMEOUT_MS",
|
|
452
|
-
provider === "claude" ? DEFAULT_CLAUDE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS,
|
|
453
|
-
1e3,
|
|
454
|
-
6e5
|
|
455
|
-
)
|
|
456
|
-
};
|
|
457
|
-
const model = env.JUDGE_MODEL || DEFAULT_MODEL[provider];
|
|
458
|
-
if (model) resolved.model = model;
|
|
459
|
-
if (provider === "vertex") {
|
|
460
|
-
const project = env.GOOGLE_CLOUD_PROJECT;
|
|
461
|
-
const location = env.GOOGLE_CLOUD_LOCATION;
|
|
462
|
-
if (!project || !location) {
|
|
463
|
-
throw new JudgeConfigError(
|
|
464
|
-
"JUDGE_PROVIDER=vertex requires GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION (ADC auth, no API keys)"
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
resolved.project = project;
|
|
468
|
-
resolved.location = location;
|
|
469
|
-
if (env.JUDGE_ACCESS_TOKEN) resolved.accessToken = env.JUDGE_ACCESS_TOKEN;
|
|
470
|
-
}
|
|
471
|
-
const keyEnvName = API_KEY_ENV[provider];
|
|
472
|
-
if (keyEnvName) {
|
|
473
|
-
const apiKey = env[keyEnvName];
|
|
474
|
-
if (!apiKey) {
|
|
475
|
-
throw new JudgeConfigError(`JUDGE_PROVIDER=${provider} requires ${keyEnvName}`);
|
|
476
|
-
}
|
|
477
|
-
resolved.apiKey = apiKey;
|
|
478
|
-
}
|
|
479
|
-
if (provider === "claude") {
|
|
480
|
-
resolved.claudeBin = env.JUDGE_CLAUDE_BIN || "claude";
|
|
481
|
-
}
|
|
482
|
-
return resolved;
|
|
483
|
-
}
|
|
484
|
-
function resolveJudgeServerEnv(env) {
|
|
485
|
-
const resolved = {
|
|
486
|
-
host: env.JUDGE_HOST || "127.0.0.1",
|
|
487
|
-
port: intEnv(env, "JUDGE_PORT", 8790, 1, 65535)
|
|
488
|
-
};
|
|
489
|
-
if (env.JUDGE_TOKEN) resolved.token = env.JUDGE_TOKEN;
|
|
490
|
-
return resolved;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// src/judge-service.ts
|
|
494
|
-
function buildProvider(env) {
|
|
495
|
-
switch (env.provider) {
|
|
496
|
-
case "fake":
|
|
497
|
-
return new FakeJudgeProvider();
|
|
498
|
-
case "vertex":
|
|
499
|
-
return new VertexJudgeProvider({
|
|
500
|
-
project: env.project,
|
|
501
|
-
location: env.location,
|
|
502
|
-
model: env.model,
|
|
503
|
-
timeoutMs: env.timeoutMs,
|
|
504
|
-
...env.accessToken ? { accessToken: env.accessToken } : {}
|
|
505
|
-
});
|
|
506
|
-
case "gemini":
|
|
507
|
-
return new GeminiJudgeProvider({
|
|
508
|
-
apiKey: env.apiKey,
|
|
509
|
-
model: env.model,
|
|
510
|
-
timeoutMs: env.timeoutMs
|
|
511
|
-
});
|
|
512
|
-
case "openai":
|
|
513
|
-
return new OpenAiJudgeProvider({
|
|
514
|
-
apiKey: env.apiKey,
|
|
515
|
-
model: env.model,
|
|
516
|
-
timeoutMs: env.timeoutMs
|
|
517
|
-
});
|
|
518
|
-
case "anthropic":
|
|
519
|
-
return new AnthropicJudgeProvider({
|
|
520
|
-
apiKey: env.apiKey,
|
|
521
|
-
model: env.model,
|
|
522
|
-
timeoutMs: env.timeoutMs
|
|
523
|
-
});
|
|
524
|
-
case "claude":
|
|
525
|
-
return new ClaudeCliJudgeProvider({
|
|
526
|
-
bin: env.claudeBin,
|
|
527
|
-
timeoutMs: env.timeoutMs,
|
|
528
|
-
...env.model ? { model: env.model } : {}
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
function createJudgeService(env, provider) {
|
|
533
|
-
const p = provider ?? buildProvider(env);
|
|
534
|
-
return {
|
|
535
|
-
judge: (request) => judgeWithRetries(p, request, env.retries)
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
function createJudgeServiceFromEnv(env) {
|
|
539
|
-
return createJudgeService(resolveJudgeServiceEnv(env));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// src/server.ts
|
|
543
|
-
import { createServer } from "http";
|
|
544
|
-
import { JUDGE_ROUTES } from "@unotest/protocol";
|
|
545
|
-
var MAX_BODY_BYTES = 1024 * 1024;
|
|
546
|
-
function startJudgeServer(service, env) {
|
|
547
|
-
const server = createServer((req, res) => {
|
|
548
|
-
void route(service, env, req, res);
|
|
549
|
-
});
|
|
550
|
-
return new Promise((resolve, reject) => {
|
|
551
|
-
server.once("error", reject);
|
|
552
|
-
server.listen(env.port, env.host, () => {
|
|
553
|
-
const addr = server.address();
|
|
554
|
-
const port = typeof addr === "object" && addr !== null ? addr.port : env.port;
|
|
555
|
-
resolve({
|
|
556
|
-
server,
|
|
557
|
-
port,
|
|
558
|
-
close: () => new Promise((res2, rej2) => server.close((e) => e ? rej2(e) : res2()))
|
|
559
|
-
});
|
|
560
|
-
});
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
async function route(service, env, req, res) {
|
|
564
|
-
const url = req.url ?? "/";
|
|
565
|
-
if (req.method === "GET" && url === JUDGE_ROUTES.health) {
|
|
566
|
-
sendJson(res, 200, { ok: true });
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
if (req.method !== "POST" || url !== JUDGE_ROUTES.judge) {
|
|
570
|
-
sendError(res, 404, `unknown route ${req.method} ${url}`, "bad-request");
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
if (env.token && req.headers.authorization !== `Bearer ${env.token}`) {
|
|
574
|
-
sendError(res, 401, "missing or wrong bearer token (JUDGE_TOKEN)", "unauthorized");
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
let body;
|
|
578
|
-
try {
|
|
579
|
-
body = parseRequest(await readBody(req));
|
|
580
|
-
} catch (e) {
|
|
581
|
-
sendError(res, 400, e instanceof Error ? e.message : String(e), "bad-request");
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
try {
|
|
585
|
-
sendJson(res, 200, await service.judge(body));
|
|
586
|
-
} catch (e) {
|
|
587
|
-
if (e instanceof JudgeProviderError) {
|
|
588
|
-
sendError(res, 502, e.message, "provider-error");
|
|
589
|
-
} else if (e instanceof JudgeConfigError) {
|
|
590
|
-
sendError(res, 500, e.message, "internal");
|
|
591
|
-
} else {
|
|
592
|
-
sendError(res, 500, e instanceof Error ? e.message : String(e), "internal");
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
function parseRequest(raw) {
|
|
597
|
-
let parsed;
|
|
598
|
-
try {
|
|
599
|
-
parsed = JSON.parse(raw);
|
|
600
|
-
} catch {
|
|
601
|
-
throw new Error("request body is not JSON");
|
|
602
|
-
}
|
|
603
|
-
const { rubric, text } = parsed;
|
|
604
|
-
if (typeof rubric !== "string" || rubric.trim() === "") {
|
|
605
|
-
throw new Error('request body needs a non-empty string "rubric"');
|
|
606
|
-
}
|
|
607
|
-
if (typeof text !== "string") {
|
|
608
|
-
throw new Error('request body needs a string "text"');
|
|
609
|
-
}
|
|
610
|
-
return { rubric, text };
|
|
611
|
-
}
|
|
612
|
-
function readBody(req) {
|
|
613
|
-
return new Promise((resolve, reject) => {
|
|
614
|
-
const chunks = [];
|
|
615
|
-
let size = 0;
|
|
616
|
-
req.on("data", (chunk) => {
|
|
617
|
-
size += chunk.length;
|
|
618
|
-
if (size > MAX_BODY_BYTES) {
|
|
619
|
-
reject(new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`));
|
|
620
|
-
req.destroy();
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
chunks.push(chunk);
|
|
624
|
-
});
|
|
625
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
626
|
-
req.on("error", reject);
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
function sendJson(res, status, body) {
|
|
630
|
-
const payload = JSON.stringify(body);
|
|
631
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
632
|
-
res.end(payload);
|
|
633
|
-
}
|
|
634
|
-
function sendError(res, status, error, code) {
|
|
635
|
-
sendJson(res, status, { error, code });
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
export {
|
|
639
|
-
JudgeError,
|
|
640
|
-
JudgeConfigError,
|
|
641
|
-
JudgeProviderError,
|
|
642
|
-
FAKE_MODEL_ID,
|
|
643
|
-
parseFakeRubric,
|
|
644
|
-
FakeJudgeProvider,
|
|
645
|
-
VERDICT_PROMPT,
|
|
646
|
-
parseVerdictReply,
|
|
647
|
-
VertexJudgeProvider,
|
|
648
|
-
GeminiJudgeProvider,
|
|
649
|
-
OpenAiJudgeProvider,
|
|
650
|
-
AnthropicJudgeProvider,
|
|
651
|
-
ClaudeCliJudgeProvider,
|
|
652
|
-
judgeWithRetries,
|
|
653
|
-
resolveJudgeServiceEnv,
|
|
654
|
-
resolveJudgeServerEnv,
|
|
655
|
-
buildProvider,
|
|
656
|
-
createJudgeService,
|
|
657
|
-
createJudgeServiceFromEnv,
|
|
658
|
-
startJudgeServer
|
|
659
|
-
};
|
|
660
|
-
//# sourceMappingURL=chunk-4VUHPR26.js.map
|