@foldrun/core 2.3.0 → 2.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/dist/src/evals.js +6 -12
- package/dist/src/model-env.d.ts +14 -2
- package/dist/src/model-env.js +34 -9
- package/package.json +1 -1
- package/src/evals.ts +7 -12
- package/src/model-env.ts +42 -10
package/dist/src/evals.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// Deterministic checks run first and cost nothing. The judge only runs if
|
|
31
31
|
// they pass — no point paying a model to grade output already known to be
|
|
32
32
|
// wrong.
|
|
33
|
-
import { accountModelEnv } from "./model-env.js";
|
|
33
|
+
import { accountModelEnv, withModelEnv } from "./model-env.js";
|
|
34
34
|
import { sdkEnv } from "./step-exec.js";
|
|
35
35
|
import fs from "node:fs";
|
|
36
36
|
import path from "node:path";
|
|
@@ -382,7 +382,7 @@ async function judge(rubric, output, model, effort, env) {
|
|
|
382
382
|
}
|
|
383
383
|
return text.trim();
|
|
384
384
|
}
|
|
385
|
-
async function checkAssertion(assertion, output, agentDir, model, effort, judgeEnv = () => sdkEnv(undefined)) {
|
|
385
|
+
async function checkAssertion(assertion, output, agentDir, model, effort, judgeEnv = () => ({ env: sdkEnv(undefined) })) {
|
|
386
386
|
const hay = output.toLowerCase();
|
|
387
387
|
switch (assertion.type) {
|
|
388
388
|
case "contains": {
|
|
@@ -417,14 +417,14 @@ async function checkAssertion(assertion, output, agentDir, model, effort, judgeE
|
|
|
417
417
|
case "judge": {
|
|
418
418
|
// The judge runs on the account's own model key, never quietly on the
|
|
419
419
|
// platform's: a customer's eval is not the operator's bill.
|
|
420
|
-
let
|
|
420
|
+
let m;
|
|
421
421
|
try {
|
|
422
|
-
|
|
422
|
+
m = judgeEnv();
|
|
423
423
|
}
|
|
424
424
|
catch (err) {
|
|
425
425
|
return { assertion, passed: false, detail: `judge not run — ${err instanceof Error ? err.message : String(err)}` };
|
|
426
426
|
}
|
|
427
|
-
const verdict = await judge(assertion.value, output, model, effort, env);
|
|
427
|
+
const verdict = await withModelEnv(m, (env) => judge(assertion.value, output, model, effort, env));
|
|
428
428
|
const passed = /^\s*PASS\b/i.test(verdict);
|
|
429
429
|
return { assertion, passed, detail: verdict.slice(0, 300) || "judge returned nothing" };
|
|
430
430
|
}
|
|
@@ -495,13 +495,7 @@ export async function runEval(tenant, workspace, info) {
|
|
|
495
495
|
for (const a of cheap)
|
|
496
496
|
assertions.push(await checkAssertion(a, output, agentDir, info.model, info.effort));
|
|
497
497
|
if (assertions.every((r) => r.passed)) {
|
|
498
|
-
const judgeEnv = () =>
|
|
499
|
-
const m = accountModelEnv(tenant, workspace, () => sdkEnv(undefined));
|
|
500
|
-
if (m.format !== "anthropic") {
|
|
501
|
-
throw new Error(`this account's provider speaks ${m.format}; a judge needs an Anthropic-format provider`);
|
|
502
|
-
}
|
|
503
|
-
return m.env;
|
|
504
|
-
};
|
|
498
|
+
const judgeEnv = () => accountModelEnv(tenant, workspace, () => sdkEnv(undefined));
|
|
505
499
|
for (const a of judges)
|
|
506
500
|
assertions.push(await checkAssertion(a, output, agentDir, info.model, info.effort, judgeEnv));
|
|
507
501
|
}
|
package/dist/src/model-env.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TranslatorSpec } from "./translator.ts";
|
|
1
2
|
/** Refused for want of a usable key; `status` is what an HTTP layer answers. */
|
|
2
3
|
export declare class ModelKeyError extends Error {
|
|
3
4
|
readonly status: number;
|
|
@@ -7,11 +8,22 @@ export declare const NO_MODEL_KEY: string;
|
|
|
7
8
|
/**
|
|
8
9
|
* `platform` is a function: the caller's own way to build the platform's env
|
|
9
10
|
* (a step's, or the SDK's host env), called only when this account may use
|
|
10
|
-
* it. `format` says what the endpoint speaks
|
|
11
|
-
*
|
|
11
|
+
* it. `format` says what the endpoint speaks; `translator` is set when it is
|
|
12
|
+
* not Anthropic's, and `withModelEnv` puts the step's translator in front of
|
|
13
|
+
* it, so a judge or a draft works on any key a run works on (FOL-16).
|
|
12
14
|
*/
|
|
13
15
|
export declare function accountModelEnv(tenant: string, workspace: string, platformEnv: () => Record<string, string | undefined>): {
|
|
14
16
|
env: Record<string, string | undefined>;
|
|
15
17
|
supply: "provider" | "platform";
|
|
16
18
|
format: string;
|
|
19
|
+
translator: TranslatorSpec | null;
|
|
17
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Run `fn` with the model env, through a translator on this host when the
|
|
23
|
+
* provider speaks Chat Completions or Responses — the same one a step uses
|
|
24
|
+
* (runner.ts), started for this call and closed after it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function withModelEnv<T>(m: {
|
|
27
|
+
env: Record<string, string | undefined>;
|
|
28
|
+
translator?: TranslatorSpec | null;
|
|
29
|
+
}, fn: (env: Record<string, string | undefined>) => Promise<T>): Promise<T>;
|
package/dist/src/model-env.js
CHANGED
|
@@ -10,6 +10,8 @@ import { readAgentsMd } from "./runner.js";
|
|
|
10
10
|
import { getSecret } from "./secrets.js";
|
|
11
11
|
import { platform } from "./platform.js";
|
|
12
12
|
import { accountModelBlock, isClaudeLoginToken } from "./model-credential.js";
|
|
13
|
+
import { providerPreset } from "./providers.js";
|
|
14
|
+
import { startTranslator, translatorSpecFor } from "./translator.js";
|
|
13
15
|
/** Refused for want of a usable key; `status` is what an HTTP layer answers. */
|
|
14
16
|
export class ModelKeyError extends Error {
|
|
15
17
|
status;
|
|
@@ -23,8 +25,9 @@ export const NO_MODEL_KEY = "no model key: this account runs on its own API key,
|
|
|
23
25
|
/**
|
|
24
26
|
* `platform` is a function: the caller's own way to build the platform's env
|
|
25
27
|
* (a step's, or the SDK's host env), called only when this account may use
|
|
26
|
-
* it. `format` says what the endpoint speaks
|
|
27
|
-
*
|
|
28
|
+
* it. `format` says what the endpoint speaks; `translator` is set when it is
|
|
29
|
+
* not Anthropic's, and `withModelEnv` puts the step's translator in front of
|
|
30
|
+
* it, so a judge or a draft works on any key a run works on (FOL-16).
|
|
28
31
|
*/
|
|
29
32
|
export function accountModelEnv(tenant, workspace, platformEnv) {
|
|
30
33
|
const raw = readAgentsMd(workspaceDir(tenant, workspace))?.data?.provider ??
|
|
@@ -34,7 +37,7 @@ export function accountModelEnv(tenant, workspace, platformEnv) {
|
|
|
34
37
|
if (!spec?.baseUrl) {
|
|
35
38
|
if (!platform.platformKeyAllowed(tenant))
|
|
36
39
|
throw new ModelKeyError(NO_MODEL_KEY);
|
|
37
|
-
return { env: platformEnv(), supply: "platform", format: "anthropic" };
|
|
40
|
+
return { env: platformEnv(), supply: "platform", format: "anthropic", translator: null };
|
|
38
41
|
}
|
|
39
42
|
const sub = (text) => text.replace(/\$\{([A-Z][A-Z0-9_]*)\}/g, (whole, name) => getSecret(tenant, name, workspace)?.value ?? whole);
|
|
40
43
|
const token = sub(spec.token);
|
|
@@ -43,15 +46,37 @@ export function accountModelEnv(tenant, workspace, platformEnv) {
|
|
|
43
46
|
if (!platform.platformKeyAllowed(tenant) && isClaudeLoginToken(token)) {
|
|
44
47
|
throw new ModelKeyError("model key: a Claude login token (sk-ant-oat…) cannot be used here — use an API key from your provider", 422);
|
|
45
48
|
}
|
|
49
|
+
const headers = Object.fromEntries(Object.entries(spec.headers).map(([k, v]) => [k, sub(v)]));
|
|
50
|
+
const preset = providerPreset(spec.name);
|
|
46
51
|
return {
|
|
47
|
-
env: providerEnvFor({
|
|
52
|
+
env: providerEnvFor({ baseUrl: spec.baseUrl, token, auth: spec.auth, models: spec.models, headers }),
|
|
53
|
+
supply: "provider",
|
|
54
|
+
format: spec.format,
|
|
55
|
+
translator: translatorSpecFor({
|
|
56
|
+
format: spec.format,
|
|
48
57
|
baseUrl: spec.baseUrl,
|
|
49
58
|
token,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
headers,
|
|
60
|
+
params: spec.params,
|
|
61
|
+
name: spec.name,
|
|
62
|
+
maxTokensParam: preset?.maxTokensParam,
|
|
63
|
+
reasoningEffort: preset?.reasoningEffort,
|
|
53
64
|
}),
|
|
54
|
-
supply: "provider",
|
|
55
|
-
format: spec.format,
|
|
56
65
|
};
|
|
57
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Run `fn` with the model env, through a translator on this host when the
|
|
69
|
+
* provider speaks Chat Completions or Responses — the same one a step uses
|
|
70
|
+
* (runner.ts), started for this call and closed after it.
|
|
71
|
+
*/
|
|
72
|
+
export async function withModelEnv(m, fn) {
|
|
73
|
+
if (!m.translator)
|
|
74
|
+
return fn(m.env);
|
|
75
|
+
const t = await startTranslator(m.translator);
|
|
76
|
+
try {
|
|
77
|
+
return await fn({ ...m.env, ...t.env });
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await t.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
package/package.json
CHANGED
package/src/evals.ts
CHANGED
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
// they pass — no point paying a model to grade output already known to be
|
|
32
32
|
// wrong.
|
|
33
33
|
|
|
34
|
-
import { accountModelEnv } from "./model-env.ts";
|
|
34
|
+
import { accountModelEnv, withModelEnv } from "./model-env.ts";
|
|
35
|
+
import type { TranslatorSpec } from "./translator.ts";
|
|
35
36
|
import { sdkEnv } from "./step-exec.ts";
|
|
36
37
|
import fs from "node:fs";
|
|
37
38
|
import path from "node:path";
|
|
@@ -515,7 +516,7 @@ async function checkAssertion(
|
|
|
515
516
|
agentDir: string,
|
|
516
517
|
model: string,
|
|
517
518
|
effort: Effort | null,
|
|
518
|
-
judgeEnv: () => Record<string, string | undefined
|
|
519
|
+
judgeEnv: () => { env: Record<string, string | undefined>; translator?: TranslatorSpec | null } = () => ({ env: sdkEnv(undefined) }),
|
|
519
520
|
): Promise<AssertionResult> {
|
|
520
521
|
const hay = output.toLowerCase();
|
|
521
522
|
switch (assertion.type) {
|
|
@@ -550,13 +551,13 @@ async function checkAssertion(
|
|
|
550
551
|
case "judge": {
|
|
551
552
|
// The judge runs on the account's own model key, never quietly on the
|
|
552
553
|
// platform's: a customer's eval is not the operator's bill.
|
|
553
|
-
let
|
|
554
|
+
let m: ReturnType<typeof judgeEnv>;
|
|
554
555
|
try {
|
|
555
|
-
|
|
556
|
+
m = judgeEnv();
|
|
556
557
|
} catch (err) {
|
|
557
558
|
return { assertion, passed: false, detail: `judge not run — ${err instanceof Error ? err.message : String(err)}` };
|
|
558
559
|
}
|
|
559
|
-
const verdict = await judge(assertion.value, output, model, effort, env);
|
|
560
|
+
const verdict = await withModelEnv(m, (env) => judge(assertion.value, output, model, effort, env));
|
|
560
561
|
const passed = /^\s*PASS\b/i.test(verdict);
|
|
561
562
|
return { assertion, passed, detail: verdict.slice(0, 300) || "judge returned nothing" };
|
|
562
563
|
}
|
|
@@ -639,13 +640,7 @@ export async function runEval(
|
|
|
639
640
|
const judges = testCase.expect.filter((a) => a.type === "judge");
|
|
640
641
|
for (const a of cheap) assertions.push(await checkAssertion(a, output, agentDir, info.model, info.effort));
|
|
641
642
|
if (assertions.every((r) => r.passed)) {
|
|
642
|
-
const judgeEnv = () =>
|
|
643
|
-
const m = accountModelEnv(tenant, workspace, () => sdkEnv(undefined));
|
|
644
|
-
if (m.format !== "anthropic") {
|
|
645
|
-
throw new Error(`this account's provider speaks ${m.format}; a judge needs an Anthropic-format provider`);
|
|
646
|
-
}
|
|
647
|
-
return m.env;
|
|
648
|
-
};
|
|
643
|
+
const judgeEnv = () => accountModelEnv(tenant, workspace, () => sdkEnv(undefined));
|
|
649
644
|
for (const a of judges) assertions.push(await checkAssertion(a, output, agentDir, info.model, info.effort, judgeEnv));
|
|
650
645
|
} else {
|
|
651
646
|
for (const a of judges) {
|
package/src/model-env.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { readAgentsMd } from "./runner.ts";
|
|
|
11
11
|
import { getSecret } from "./secrets.ts";
|
|
12
12
|
import { platform } from "./platform.ts";
|
|
13
13
|
import { accountModelBlock, isClaudeLoginToken } from "./model-credential.ts";
|
|
14
|
+
import { providerPreset } from "./providers.ts";
|
|
15
|
+
import { startTranslator, translatorSpecFor, type TranslatorSpec } from "./translator.ts";
|
|
14
16
|
|
|
15
17
|
/** Refused for want of a usable key; `status` is what an HTTP layer answers. */
|
|
16
18
|
export class ModelKeyError extends Error {
|
|
@@ -28,14 +30,20 @@ export const NO_MODEL_KEY =
|
|
|
28
30
|
/**
|
|
29
31
|
* `platform` is a function: the caller's own way to build the platform's env
|
|
30
32
|
* (a step's, or the SDK's host env), called only when this account may use
|
|
31
|
-
* it. `format` says what the endpoint speaks
|
|
32
|
-
*
|
|
33
|
+
* it. `format` says what the endpoint speaks; `translator` is set when it is
|
|
34
|
+
* not Anthropic's, and `withModelEnv` puts the step's translator in front of
|
|
35
|
+
* it, so a judge or a draft works on any key a run works on (FOL-16).
|
|
33
36
|
*/
|
|
34
37
|
export function accountModelEnv(
|
|
35
38
|
tenant: string,
|
|
36
39
|
workspace: string,
|
|
37
40
|
platformEnv: () => Record<string, string | undefined>,
|
|
38
|
-
): {
|
|
41
|
+
): {
|
|
42
|
+
env: Record<string, string | undefined>;
|
|
43
|
+
supply: "provider" | "platform";
|
|
44
|
+
format: string;
|
|
45
|
+
translator: TranslatorSpec | null;
|
|
46
|
+
} {
|
|
39
47
|
const raw =
|
|
40
48
|
(readAgentsMd(workspaceDir(tenant, workspace))?.data?.provider as Record<string, unknown> | undefined) ??
|
|
41
49
|
(readAgentsMd(accountDir(tenant))?.data?.provider as Record<string, unknown> | undefined) ??
|
|
@@ -43,7 +51,7 @@ export function accountModelEnv(
|
|
|
43
51
|
const spec = parseProvider(raw);
|
|
44
52
|
if (!spec?.baseUrl) {
|
|
45
53
|
if (!platform.platformKeyAllowed(tenant)) throw new ModelKeyError(NO_MODEL_KEY);
|
|
46
|
-
return { env: platformEnv(), supply: "platform", format: "anthropic" };
|
|
54
|
+
return { env: platformEnv(), supply: "platform", format: "anthropic", translator: null };
|
|
47
55
|
}
|
|
48
56
|
const sub = (text: string) =>
|
|
49
57
|
text.replace(/\$\{([A-Z][A-Z0-9_]*)\}/g, (whole, name) => getSecret(tenant, name, workspace)?.value ?? whole);
|
|
@@ -52,15 +60,39 @@ export function accountModelEnv(
|
|
|
52
60
|
if (!platform.platformKeyAllowed(tenant) && isClaudeLoginToken(token)) {
|
|
53
61
|
throw new ModelKeyError("model key: a Claude login token (sk-ant-oat…) cannot be used here — use an API key from your provider", 422);
|
|
54
62
|
}
|
|
63
|
+
const headers = Object.fromEntries(Object.entries(spec.headers).map(([k, v]) => [k, sub(v)]));
|
|
64
|
+
const preset = providerPreset(spec.name);
|
|
55
65
|
return {
|
|
56
|
-
env: providerEnvFor({
|
|
66
|
+
env: providerEnvFor({ baseUrl: spec.baseUrl, token, auth: spec.auth, models: spec.models, headers }),
|
|
67
|
+
supply: "provider",
|
|
68
|
+
format: spec.format,
|
|
69
|
+
translator: translatorSpecFor({
|
|
70
|
+
format: spec.format,
|
|
57
71
|
baseUrl: spec.baseUrl,
|
|
58
72
|
token,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
headers,
|
|
74
|
+
params: spec.params,
|
|
75
|
+
name: spec.name,
|
|
76
|
+
maxTokensParam: preset?.maxTokensParam,
|
|
77
|
+
reasoningEffort: preset?.reasoningEffort,
|
|
62
78
|
}),
|
|
63
|
-
supply: "provider",
|
|
64
|
-
format: spec.format,
|
|
65
79
|
};
|
|
66
80
|
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Run `fn` with the model env, through a translator on this host when the
|
|
84
|
+
* provider speaks Chat Completions or Responses — the same one a step uses
|
|
85
|
+
* (runner.ts), started for this call and closed after it.
|
|
86
|
+
*/
|
|
87
|
+
export async function withModelEnv<T>(
|
|
88
|
+
m: { env: Record<string, string | undefined>; translator?: TranslatorSpec | null },
|
|
89
|
+
fn: (env: Record<string, string | undefined>) => Promise<T>,
|
|
90
|
+
): Promise<T> {
|
|
91
|
+
if (!m.translator) return fn(m.env);
|
|
92
|
+
const t = await startTranslator(m.translator);
|
|
93
|
+
try {
|
|
94
|
+
return await fn({ ...m.env, ...t.env });
|
|
95
|
+
} finally {
|
|
96
|
+
await t.close();
|
|
97
|
+
}
|
|
98
|
+
}
|