@unotest/judge 0.23.0 → 0.24.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 +68 -0
- package/README.md +23 -15
- package/dist/{chunk-4VUHPR26.js → chunk-74FL5RWE.js} +95 -40
- package/dist/cli.js +21 -18
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1 -1
- package/package.json +3 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# @unotest/judge
|
|
2
2
|
|
|
3
|
+
## [0.24.0] - 2026-08-26
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Judge: rendered text, one env namespace, preamble and majority vote.
|
|
8
|
+
|
|
9
|
+
**Breaking (judge env, no aliases).** Every `JUDGE_*` variable of the
|
|
10
|
+
`@unotest/judge` service is now `UNOTEST_JUDGE_*`, matching the client-side
|
|
11
|
+
names `@unotest/web` already used. `@unotest/judge` shipped a day before
|
|
12
|
+
this change with no known installs outside the monorepo, so the rename lands
|
|
13
|
+
without a deprecation window — the window would never be cheaper. Migration:
|
|
14
|
+
|
|
15
|
+
| Old | New |
|
|
16
|
+
| --------------------------- | ------------------------------------------- |
|
|
17
|
+
| `JUDGE_PROVIDER` | `UNOTEST_JUDGE_PROVIDER` |
|
|
18
|
+
| `JUDGE_MODEL` | `UNOTEST_JUDGE_MODEL` |
|
|
19
|
+
| `JUDGE_RETRIES` | `UNOTEST_JUDGE_RETRIES` |
|
|
20
|
+
| `JUDGE_TIMEOUT_MS` | `UNOTEST_JUDGE_CALL_TIMEOUT_MS` |
|
|
21
|
+
| `JUDGE_ACCESS_TOKEN` | `UNOTEST_JUDGE_ACCESS_TOKEN` |
|
|
22
|
+
| `JUDGE_CLAUDE_BIN` | `UNOTEST_JUDGE_CLAUDE_BIN` |
|
|
23
|
+
| `JUDGE_HOST` / `JUDGE_PORT` | `UNOTEST_JUDGE_HOST` / `UNOTEST_JUDGE_PORT` |
|
|
24
|
+
| `JUDGE_TOKEN` | `UNOTEST_JUDGE_TOKEN` |
|
|
25
|
+
|
|
26
|
+
Two names were deliberately not a straight prefixing. The service's per-call
|
|
27
|
+
budget became `UNOTEST_JUDGE_CALL_TIMEOUT_MS` because `UNOTEST_JUDGE_TIMEOUT_MS`
|
|
28
|
+
was already taken by the client's whole-request budget — same family, different
|
|
29
|
+
question. And the service's bearer token merged into the client's
|
|
30
|
+
`UNOTEST_JUDGE_TOKEN`: it is one secret, and in a shared overlay the two ends
|
|
31
|
+
have to carry the same value anyway.
|
|
32
|
+
|
|
33
|
+
**Behavioral fix: rendered text.** `assertJudge` and the DSL query
|
|
34
|
+
`getInnerText(loc)` now read `innerText`, not `textContent`. Both promised
|
|
35
|
+
rendered text and delivered the raw source-order concatenation: line breaks
|
|
36
|
+
gone, `display:none` subtrees and `<script>` contents folded in. A rubric
|
|
37
|
+
about structure ("the answer is three bullets") was unjudgeable, and
|
|
38
|
+
`getInnerText` returned text no user can see. Scenarios that relied on
|
|
39
|
+
hidden text reaching these two functions will now see it excluded.
|
|
40
|
+
|
|
41
|
+
**Vertex fixes.** `google-auth-library` moved from optional peer to a regular
|
|
42
|
+
dependency: as a peer it did not resolve under `npx @unotest/judge` at all,
|
|
43
|
+
which broke the advertised keyless path for anyone who never installs the
|
|
44
|
+
package locally. It is still loaded lazily, so non-vertex runs pay nothing.
|
|
45
|
+
Expired Application Default Credentials now produce
|
|
46
|
+
"run `gcloud auth application-default login`" instead of a raw Google OAuth
|
|
47
|
+
blob, read off the structured response body rather than message text.
|
|
48
|
+
|
|
49
|
+
**New: `UNOTEST_JUDGE_PREAMBLE`** (web side) — text prepended to every rubric
|
|
50
|
+
in the project, for standing context ("the app under test is a support bot,
|
|
51
|
+
Russian UI") that would otherwise be copy-pasted into each rubric. It applies
|
|
52
|
+
in both local and remote mode; a remote judge stays project-agnostic.
|
|
53
|
+
|
|
54
|
+
**New: `UNOTEST_JUDGE_VOTE=N`** (service side, odd N, default 1 = off) — N
|
|
55
|
+
independent calls decided by majority. It replaces `UNOTEST_JUDGE_RETRIES`
|
|
56
|
+
rather than stacking with it: re-asking is a deliberate bias toward `pass`,
|
|
57
|
+
a vote is deliberately symmetric, and running both would quietly restore the
|
|
58
|
+
bias the vote was chosen to remove. Ballots run concurrently, so a vote costs
|
|
59
|
+
N provider calls but roughly one call of wall-clock.
|
|
60
|
+
|
|
61
|
+
**Wire protocol.** New error code `config` on the judge's error responses:
|
|
62
|
+
the service's own setup is broken (bad env, expired credentials) and the
|
|
63
|
+
message carries the fix. `@unotest/web` surfaces it as a `ConfigError`
|
|
64
|
+
instead of burying it as a generic "judge unavailable" fault.
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- Updated dependencies
|
|
69
|
+
- @unotest/protocol@0.24.0
|
|
70
|
+
|
|
3
71
|
## [0.23.0] - 2026-08-26
|
|
4
72
|
|
|
5
73
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ output.
|
|
|
15
15
|
## Run
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
|
|
18
|
+
UNOTEST_JUDGE_PROVIDER=vertex \
|
|
19
19
|
GOOGLE_CLOUD_PROJECT=my-project \
|
|
20
20
|
GOOGLE_CLOUD_LOCATION=europe-west1 \
|
|
21
21
|
npx @unotest/judge
|
|
@@ -24,19 +24,24 @@ npx @unotest/judge
|
|
|
24
24
|
The service listens on `127.0.0.1:8790` by default. Point `@unotest/web`
|
|
25
25
|
at it with `UNOTEST_JUDGE_MODE=remote` + `UNOTEST_JUDGE_URL=http://127.0.0.1:8790`,
|
|
26
26
|
or skip the HTTP hop entirely with `UNOTEST_JUDGE_MODE=local` (in-process,
|
|
27
|
-
requires `@unotest/judge` installed in the project
|
|
27
|
+
requires `@unotest/judge` installed in the project — a project that runs
|
|
28
|
+
unotest through `npx`, with nothing installed locally, can only use
|
|
29
|
+
`remote`).
|
|
28
30
|
|
|
29
31
|
## Providers
|
|
30
32
|
|
|
31
33
|
- **`vertex`** — Google Vertex AI via Application Default Credentials.
|
|
32
34
|
No API keys: auth comes from ADC (`gcloud auth application-default login`,
|
|
33
35
|
workload identity, or `GOOGLE_APPLICATION_CREDENTIALS`). Requires the
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
`google-auth-library` (a dependency of this package, loaded only when
|
|
37
|
+
this provider runs) — or a static `UNOTEST_JUDGE_ACCESS_TOKEN` from
|
|
38
|
+
`gcloud auth print-access-token`. Temperature is pinned to 0.
|
|
36
39
|
- **`claude`** — the local Claude Code CLI (`claude -p`), spawned per
|
|
37
40
|
judgement without shell interpretation. Auth comes from your Claude Code
|
|
38
|
-
session, so a subscription works with no API key
|
|
39
|
-
|
|
41
|
+
session, so a subscription works with no API key — this is the provider
|
|
42
|
+
to reach for when no cloud credentials are at hand. `UNOTEST_JUDGE_MODEL`
|
|
43
|
+
is passed as `--model` (aliases like `sonnet` work); unset uses the CLI's
|
|
44
|
+
default.
|
|
40
45
|
- **`gemini`** — the Gemini API with `GEMINI_API_KEY`. Same wire format as
|
|
41
46
|
`vertex`, temperature pinned to 0.
|
|
42
47
|
- **`openai`** — OpenAI chat completions with `OPENAI_API_KEY`
|
|
@@ -57,24 +62,27 @@ zero provider SDKs.
|
|
|
57
62
|
|
|
58
63
|
| Variable | Meaning | Default |
|
|
59
64
|
| --- | --- | --- |
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
65
|
+
| `UNOTEST_JUDGE_PROVIDER` | `fake` \| `vertex` \| `claude` \| `gemini` \| `openai` \| `anthropic` | required |
|
|
66
|
+
| `UNOTEST_JUDGE_MODEL` | model id | vertex/gemini `gemini-2.5-flash`, openai `gpt-5-mini`, anthropic `claude-haiku-4-5`, claude: CLI default |
|
|
67
|
+
| `UNOTEST_JUDGE_RETRIES` | extra provider calls on `fail`; first `pass` wins | `1` |
|
|
68
|
+
| `UNOTEST_JUDGE_VOTE` | odd N; N independent calls, the majority decides. Replaces retries rather than stacking with them | `1` (off) |
|
|
69
|
+
| `UNOTEST_JUDGE_CALL_TIMEOUT_MS` | budget for ONE provider call, ms | `30000` (`120000` for claude) |
|
|
64
70
|
| `GOOGLE_CLOUD_PROJECT` | vertex: ADC project | required for vertex |
|
|
65
71
|
| `GOOGLE_CLOUD_LOCATION` | vertex: ADC location | required for vertex |
|
|
66
|
-
| `
|
|
72
|
+
| `UNOTEST_JUDGE_ACCESS_TOKEN` | vertex: static bearer override (skips ADC) | — |
|
|
67
73
|
| `GEMINI_API_KEY` | gemini: API key | required for gemini |
|
|
68
74
|
| `OPENAI_API_KEY` | openai: API key | required for openai |
|
|
69
75
|
| `ANTHROPIC_API_KEY` | anthropic: API key | required for anthropic |
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
76
|
+
| `UNOTEST_JUDGE_CLAUDE_BIN` | claude: binary override | `claude` |
|
|
77
|
+
| `UNOTEST_JUDGE_HOST` / `UNOTEST_JUDGE_PORT` | bind address | `127.0.0.1` / `8790` |
|
|
78
|
+
| `UNOTEST_JUDGE_TOKEN` | bearer token required on every `/judge` call. `@unotest/web` reads the SAME name to send it | off |
|
|
73
79
|
|
|
74
80
|
## HTTP API
|
|
75
81
|
|
|
76
82
|
- `POST /judge` — body `{"rubric": "...", "text": "..."}` → a
|
|
77
|
-
`JudgeVerdict` (above). Errors: `{"error": "...", "code": "unauthorized" | "bad-request" | "provider-error" | "internal"}`.
|
|
83
|
+
`JudgeVerdict` (above). Errors: `{"error": "...", "code": "unauthorized" | "bad-request" | "provider-error" | "config" | "internal"}`.
|
|
84
|
+
`config` means the operator must fix this service's setup (bad env,
|
|
85
|
+
expired ADC) and the message says what to run.
|
|
78
86
|
- `GET /health` — `{"ok": true}`.
|
|
79
87
|
|
|
80
88
|
Wire types are shared through `@unotest/protocol` (`JudgeRequest`,
|
|
@@ -33,7 +33,7 @@ var FakeJudgeProvider = class {
|
|
|
33
33
|
if (constraints.length === 0) {
|
|
34
34
|
return {
|
|
35
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 (
|
|
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
37
|
model: FAKE_MODEL_ID
|
|
38
38
|
};
|
|
39
39
|
}
|
|
@@ -89,6 +89,55 @@ function parseVerdictReply(text, model) {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
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
|
+
|
|
92
141
|
// src/providers/generate-content.ts
|
|
93
142
|
function generateContentBody(request) {
|
|
94
143
|
return {
|
|
@@ -175,27 +224,6 @@ var VertexJudgeProvider = class {
|
|
|
175
224
|
return this.tokenSource();
|
|
176
225
|
}
|
|
177
226
|
};
|
|
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
227
|
|
|
200
228
|
// src/providers/gemini.ts
|
|
201
229
|
var GeminiJudgeProvider = class {
|
|
@@ -356,7 +384,7 @@ function toTypedError(e, bin, timeoutMs) {
|
|
|
356
384
|
const err = e;
|
|
357
385
|
if (err.code === "ENOENT") {
|
|
358
386
|
return new JudgeConfigError(
|
|
359
|
-
`Claude Code CLI not found ("${bin}") \u2014 install it (https://claude.com/claude-code) or point
|
|
387
|
+
`Claude Code CLI not found ("${bin}") \u2014 install it (https://claude.com/claude-code) or point UNOTEST_JUDGE_CLAUDE_BIN at the binary`,
|
|
360
388
|
{ bin }
|
|
361
389
|
);
|
|
362
390
|
}
|
|
@@ -400,8 +428,23 @@ async function judgeWithRetries(provider, request, retries) {
|
|
|
400
428
|
}
|
|
401
429
|
return last;
|
|
402
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
|
+
}
|
|
403
445
|
|
|
404
446
|
// src/env.ts
|
|
447
|
+
import { JUDGE_ENV } from "@unotest/protocol";
|
|
405
448
|
var PROVIDER_KINDS = [
|
|
406
449
|
"fake",
|
|
407
450
|
"vertex",
|
|
@@ -436,57 +479,66 @@ function intEnv(env, name, fallback, min, max) {
|
|
|
436
479
|
return n;
|
|
437
480
|
}
|
|
438
481
|
function resolveJudgeServiceEnv(env) {
|
|
439
|
-
const provider = env.
|
|
482
|
+
const provider = env[JUDGE_ENV.provider];
|
|
440
483
|
if (!provider || !PROVIDER_KINDS.includes(provider)) {
|
|
441
484
|
const list = PROVIDER_KINDS.map((k) => `"${k}"`).join(" | ");
|
|
442
485
|
throw new JudgeConfigError(
|
|
443
|
-
provider === void 0 || provider === "" ?
|
|
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}"`
|
|
444
493
|
);
|
|
445
494
|
}
|
|
446
495
|
const resolved = {
|
|
447
496
|
provider,
|
|
448
|
-
|
|
497
|
+
votes,
|
|
498
|
+
retries: intEnv(env, JUDGE_ENV.retries, DEFAULT_RETRIES, 0, 10),
|
|
449
499
|
timeoutMs: intEnv(
|
|
450
500
|
env,
|
|
451
|
-
|
|
501
|
+
JUDGE_ENV.callTimeoutMs,
|
|
452
502
|
provider === "claude" ? DEFAULT_CLAUDE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS,
|
|
453
503
|
1e3,
|
|
454
504
|
6e5
|
|
455
505
|
)
|
|
456
506
|
};
|
|
457
|
-
const model = env.
|
|
507
|
+
const model = env[JUDGE_ENV.model] || DEFAULT_MODEL[provider];
|
|
458
508
|
if (model) resolved.model = model;
|
|
459
509
|
if (provider === "vertex") {
|
|
460
510
|
const project = env.GOOGLE_CLOUD_PROJECT;
|
|
461
511
|
const location = env.GOOGLE_CLOUD_LOCATION;
|
|
462
512
|
if (!project || !location) {
|
|
463
513
|
throw new JudgeConfigError(
|
|
464
|
-
|
|
514
|
+
`${JUDGE_ENV.provider}=vertex requires GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION (ADC auth, no API keys)`
|
|
465
515
|
);
|
|
466
516
|
}
|
|
467
517
|
resolved.project = project;
|
|
468
518
|
resolved.location = location;
|
|
469
|
-
|
|
519
|
+
const accessToken = env[JUDGE_ENV.accessToken];
|
|
520
|
+
if (accessToken) resolved.accessToken = accessToken;
|
|
470
521
|
}
|
|
471
522
|
const keyEnvName = API_KEY_ENV[provider];
|
|
472
523
|
if (keyEnvName) {
|
|
473
524
|
const apiKey = env[keyEnvName];
|
|
474
525
|
if (!apiKey) {
|
|
475
|
-
throw new JudgeConfigError(
|
|
526
|
+
throw new JudgeConfigError(`${JUDGE_ENV.provider}=${provider} requires ${keyEnvName}`);
|
|
476
527
|
}
|
|
477
528
|
resolved.apiKey = apiKey;
|
|
478
529
|
}
|
|
479
530
|
if (provider === "claude") {
|
|
480
|
-
resolved.claudeBin = env.
|
|
531
|
+
resolved.claudeBin = env[JUDGE_ENV.claudeBin] || "claude";
|
|
481
532
|
}
|
|
482
533
|
return resolved;
|
|
483
534
|
}
|
|
484
535
|
function resolveJudgeServerEnv(env) {
|
|
485
536
|
const resolved = {
|
|
486
|
-
host: env.
|
|
487
|
-
port: intEnv(env,
|
|
537
|
+
host: env[JUDGE_ENV.host] || "127.0.0.1",
|
|
538
|
+
port: intEnv(env, JUDGE_ENV.port, 8790, 1, 65535)
|
|
488
539
|
};
|
|
489
|
-
|
|
540
|
+
const token = env[JUDGE_ENV.token];
|
|
541
|
+
if (token) resolved.token = token;
|
|
490
542
|
return resolved;
|
|
491
543
|
}
|
|
492
544
|
|
|
@@ -532,7 +584,7 @@ function buildProvider(env) {
|
|
|
532
584
|
function createJudgeService(env, provider) {
|
|
533
585
|
const p = provider ?? buildProvider(env);
|
|
534
586
|
return {
|
|
535
|
-
judge: (request) => judgeWithRetries(p, request, env.retries)
|
|
587
|
+
judge: (request) => env.votes > 1 ? judgeWithVote(p, request, env.votes) : judgeWithRetries(p, request, env.retries)
|
|
536
588
|
};
|
|
537
589
|
}
|
|
538
590
|
function createJudgeServiceFromEnv(env) {
|
|
@@ -541,7 +593,10 @@ function createJudgeServiceFromEnv(env) {
|
|
|
541
593
|
|
|
542
594
|
// src/server.ts
|
|
543
595
|
import { createServer } from "http";
|
|
544
|
-
import {
|
|
596
|
+
import {
|
|
597
|
+
JUDGE_ENV as JUDGE_ENV2,
|
|
598
|
+
JUDGE_ROUTES
|
|
599
|
+
} from "@unotest/protocol";
|
|
545
600
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
546
601
|
function startJudgeServer(service, env) {
|
|
547
602
|
const server = createServer((req, res) => {
|
|
@@ -571,7 +626,7 @@ async function route(service, env, req, res) {
|
|
|
571
626
|
return;
|
|
572
627
|
}
|
|
573
628
|
if (env.token && req.headers.authorization !== `Bearer ${env.token}`) {
|
|
574
|
-
sendError(res, 401,
|
|
629
|
+
sendError(res, 401, `missing or wrong bearer token (${JUDGE_ENV2.token})`, "unauthorized");
|
|
575
630
|
return;
|
|
576
631
|
}
|
|
577
632
|
let body;
|
|
@@ -587,7 +642,7 @@ async function route(service, env, req, res) {
|
|
|
587
642
|
if (e instanceof JudgeProviderError) {
|
|
588
643
|
sendError(res, 502, e.message, "provider-error");
|
|
589
644
|
} else if (e instanceof JudgeConfigError) {
|
|
590
|
-
sendError(res, 500, e.message, "
|
|
645
|
+
sendError(res, 500, e.message, "config");
|
|
591
646
|
} else {
|
|
592
647
|
sendError(res, 500, e instanceof Error ? e.message : String(e), "internal");
|
|
593
648
|
}
|
|
@@ -657,4 +712,4 @@ export {
|
|
|
657
712
|
createJudgeServiceFromEnv,
|
|
658
713
|
startJudgeServer
|
|
659
714
|
};
|
|
660
|
-
//# sourceMappingURL=chunk-
|
|
715
|
+
//# sourceMappingURL=chunk-74FL5RWE.js.map
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
resolveJudgeServerEnv,
|
|
4
4
|
resolveJudgeServiceEnv,
|
|
5
5
|
startJudgeServer
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-74FL5RWE.js";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -39,23 +39,26 @@ function printHelp() {
|
|
|
39
39
|
"Usage: npx @unotest/judge [--help] [--version]",
|
|
40
40
|
"",
|
|
41
41
|
"Env:",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
42
|
+
" UNOTEST_JUDGE_PROVIDER fake (deterministic, CI-safe) | vertex (ADC) |",
|
|
43
|
+
" claude (Claude Code CLI, subscription auth) |",
|
|
44
|
+
" gemini | openai | anthropic (API keys)",
|
|
45
|
+
" UNOTEST_JUDGE_MODEL model id; defaults per provider:",
|
|
46
|
+
" vertex/gemini gemini-2.5-flash,",
|
|
47
|
+
" openai gpt-5-mini, anthropic claude-haiku-4-5,",
|
|
48
|
+
" claude: CLI default",
|
|
49
|
+
" UNOTEST_JUDGE_RETRIES extra provider calls on fail (default 1)",
|
|
50
|
+
" UNOTEST_JUDGE_VOTE N-call majority vote (default 1 = off)",
|
|
51
|
+
" UNOTEST_JUDGE_CALL_TIMEOUT_MS per-call budget, ms (default 30000;",
|
|
52
|
+
" claude 120000)",
|
|
53
|
+
" GOOGLE_CLOUD_PROJECT vertex: ADC project",
|
|
54
|
+
" GOOGLE_CLOUD_LOCATION vertex: ADC location",
|
|
55
|
+
" UNOTEST_JUDGE_ACCESS_TOKEN vertex: static bearer override (skips ADC)",
|
|
56
|
+
" GEMINI_API_KEY gemini: API key",
|
|
57
|
+
" OPENAI_API_KEY openai: API key",
|
|
58
|
+
" ANTHROPIC_API_KEY anthropic: API key",
|
|
59
|
+
" UNOTEST_JUDGE_CLAUDE_BIN claude: binary override (default claude)",
|
|
60
|
+
" UNOTEST_JUDGE_HOST / _PORT bind address (default 127.0.0.1:8790)",
|
|
61
|
+
" UNOTEST_JUDGE_TOKEN optional bearer required on every /judge call"
|
|
59
62
|
].join("\n")
|
|
60
63
|
);
|
|
61
64
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ interface VertexProviderOptions {
|
|
|
40
40
|
project: string;
|
|
41
41
|
location: string;
|
|
42
42
|
model: string;
|
|
43
|
-
/** Static bearer token override (
|
|
43
|
+
/** Static bearer token override (UNOTEST_JUDGE_ACCESS_TOKEN). Absent → ADC. */
|
|
44
44
|
accessToken?: string;
|
|
45
45
|
/** Per-call wall-clock budget, ms. */
|
|
46
46
|
timeoutMs: number;
|
|
@@ -110,7 +110,7 @@ type ExecFileFn = (file: string, args: string[], opts: {
|
|
|
110
110
|
maxBuffer: number;
|
|
111
111
|
}) => Promise<ExecFileResult>;
|
|
112
112
|
interface ClaudeCliProviderOptions {
|
|
113
|
-
/** Binary to spawn (
|
|
113
|
+
/** Binary to spawn (UNOTEST_JUDGE_CLAUDE_BIN, default "claude"). */
|
|
114
114
|
bin: string;
|
|
115
115
|
/** Passed as `--model` only when set — otherwise the CLI's default. */
|
|
116
116
|
model?: string;
|
|
@@ -137,11 +137,14 @@ declare function judgeWithRetries(provider: JudgeProvider, request: JudgeRequest
|
|
|
137
137
|
type JudgeProviderKind = "fake" | "vertex" | "claude" | "gemini" | "openai" | "anthropic";
|
|
138
138
|
interface JudgeServiceEnv {
|
|
139
139
|
provider: JudgeProviderKind;
|
|
140
|
-
/** Model id. Absent only for `claude` without
|
|
140
|
+
/** Model id. Absent only for `claude` without UNOTEST_JUDGE_MODEL (CLI default)
|
|
141
141
|
* and for `fake` (no model behind it). */
|
|
142
142
|
model?: string;
|
|
143
|
-
/** Extra provider calls allowed on `fail` (first `pass` wins).
|
|
143
|
+
/** Extra provider calls allowed on `fail` (first `pass` wins). Ignored
|
|
144
|
+
* when `votes` > 1 — see policy.ts. */
|
|
144
145
|
retries: number;
|
|
146
|
+
/** Independent calls whose majority decides the verdict. 1 = off. */
|
|
147
|
+
votes: number;
|
|
145
148
|
/** Per-provider-call wall-clock budget, ms. */
|
|
146
149
|
timeoutMs: number;
|
|
147
150
|
/** Vertex ADC target. */
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/judge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "LLM-judge service for the @unotest ecosystem: judges free-form text (chat replies, generated content) against a natural-language rubric and returns a structured pass/fail verdict with reasoning. Runs as a small HTTP service (`npx @unotest/judge`) or in-process. Providers: deterministic `fake` (CI-safe), Google Vertex AI via ADC, the local Claude Code CLI (subscription auth, no API key), and Gemini / OpenAI / Anthropic APIs via keys — all raw HTTP or a local process, zero provider SDKs. Backs the `assertJudge` DSL assertion of @unotest/web.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,15 +32,8 @@
|
|
|
32
32
|
"README.md"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"peerDependencies": {
|
|
38
|
-
"google-auth-library": "^9.0.0 || ^10.0.0"
|
|
39
|
-
},
|
|
40
|
-
"peerDependenciesMeta": {
|
|
41
|
-
"google-auth-library": {
|
|
42
|
-
"optional": true
|
|
43
|
-
}
|
|
35
|
+
"google-auth-library": "^10.0.0",
|
|
36
|
+
"@unotest/protocol": "^0.24.0"
|
|
44
37
|
},
|
|
45
38
|
"devDependencies": {
|
|
46
39
|
"@types/node": "^22.10.0",
|