@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 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 env;
420
+ let m;
421
421
  try {
422
- env = judgeEnv();
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
  }
@@ -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 — a caller with no translator
11
- * refuses anything but anthropic.
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>;
@@ -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 — a caller with no translator
27
- * refuses anything but anthropic.
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
- auth: spec.auth,
51
- models: spec.models,
52
- headers: Object.fromEntries(Object.entries(spec.headers).map(([k, v]) => [k, sub(v)])),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldrun/core",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Agents are just folders. The format, the runner, the checks.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
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> = () => sdkEnv(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 env: Record<string, string | undefined>;
554
+ let m: ReturnType<typeof judgeEnv>;
554
555
  try {
555
- env = judgeEnv();
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 — a caller with no translator
32
- * refuses anything but anthropic.
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
- ): { env: Record<string, string | undefined>; supply: "provider" | "platform"; format: string } {
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
- auth: spec.auth,
60
- models: spec.models,
61
- headers: Object.fromEntries(Object.entries(spec.headers).map(([k, v]) => [k, sub(v)])),
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
+ }