@foldrun/core 2.0.0 → 2.2.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/catalog.d.ts +40 -0
- package/dist/src/catalog.js +45 -3
- package/dist/src/deploy.d.ts +6 -1
- package/dist/src/deploy.js +8 -5
- package/dist/src/model-credential.js +1 -1
- package/dist/src/model-env.js +1 -1
- package/dist/src/run-container.d.ts +4 -9
- package/dist/src/run-container.js +8 -1
- package/dist/src/runner.d.ts +2 -0
- package/dist/src/runner.js +13 -16
- package/dist/src/step-exec.d.ts +31 -17
- package/dist/src/step-exec.js +36 -36
- package/dist/src/store.d.ts +46 -12
- package/dist/src/store.js +60 -4
- package/dist/src/tool-loop.d.ts +4 -1
- package/dist/src/tool-loop.js +2 -0
- package/dist/src/translator.d.ts +12 -0
- package/dist/src/translator.js +36 -13
- package/package.json +1 -1
- package/src/catalog.ts +68 -4
- package/src/deploy.ts +11 -4
- package/src/model-credential.ts +1 -1
- package/src/model-env.ts +1 -1
- package/src/run-container.ts +12 -4
- package/src/runner.ts +15 -17
- package/src/step-exec.ts +54 -46
- package/src/store.ts +90 -10
- package/src/tool-loop.ts +6 -1
- package/src/translator.ts +39 -15
package/dist/src/catalog.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ export interface CatalogModel {
|
|
|
8
8
|
promptPrice: number | null;
|
|
9
9
|
/** USD per output token. */
|
|
10
10
|
completionPrice: number | null;
|
|
11
|
+
/** USD per input token read from / written to the prompt cache, when the
|
|
12
|
+
* gateway quotes them (OpenRouter's input_cache_read / input_cache_write).
|
|
13
|
+
* Models discount their cache differently — GPT-4o reads at half the
|
|
14
|
+
* input price, Claude at a tenth — so a quoted price beats any multiplier.
|
|
15
|
+
* Absent on catalogues cached before 2.1.1. */
|
|
16
|
+
cacheReadPrice?: number | null;
|
|
17
|
+
cacheWritePrice?: number | null;
|
|
11
18
|
/** Can this model make tool calls? Null when the gateway doesn't say. */
|
|
12
19
|
tools: boolean | null;
|
|
13
20
|
/** Does it take a reasoning-depth parameter? (What `effort:` needs.) */
|
|
@@ -23,6 +30,8 @@ export interface Catalog {
|
|
|
23
30
|
fetchedAt: string;
|
|
24
31
|
models: CatalogModel[];
|
|
25
32
|
}
|
|
33
|
+
/** The gateway's /models payload as entries. Exported for its tests. */
|
|
34
|
+
export declare function parseModels(payload: unknown): CatalogModel[];
|
|
26
35
|
/** The cache as it stands, however old — null only when nothing was ever
|
|
27
36
|
* fetched. Reading never touches the network. */
|
|
28
37
|
export declare function readCatalog(baseUrl: string): Catalog | null;
|
|
@@ -83,7 +92,38 @@ export declare function clampEffort(catalog: Catalog | null, modelId: string, ef
|
|
|
83
92
|
export declare function catalogCost(catalog: Catalog | null, modelId: string, usage: {
|
|
84
93
|
inputTokens: number;
|
|
85
94
|
outputTokens: number;
|
|
95
|
+
cacheReadTokens?: number;
|
|
96
|
+
cacheWriteTokens?: number;
|
|
86
97
|
}): number | null;
|
|
98
|
+
/** What a token costs on one model: input, output, and — when the gateway
|
|
99
|
+
* quotes them — reads from and writes to the prompt cache. */
|
|
100
|
+
export interface TokenPrice {
|
|
101
|
+
input: number;
|
|
102
|
+
output: number;
|
|
103
|
+
cacheRead?: number | null;
|
|
104
|
+
cacheWrite?: number | null;
|
|
105
|
+
}
|
|
106
|
+
/** A catalogue entry's prices, or null when it has no input/output price. */
|
|
107
|
+
export declare function tokenPriceOf(m: CatalogModel | null | undefined): TokenPrice | null;
|
|
108
|
+
/**
|
|
109
|
+
* The cost of a set of counts at a price. `inputTokens` is all input; the
|
|
110
|
+
* cache fields are parts of it. Cached input is priced at the model's own
|
|
111
|
+
* cache prices when the gateway quotes them, and only otherwise at
|
|
112
|
+
* Anthropic's multipliers over the input price. Before the counts were
|
|
113
|
+
* split (18 Oct 2026) every cached token was billed at the full rate.
|
|
114
|
+
*/
|
|
115
|
+
export declare function costOf(usage: {
|
|
116
|
+
inputTokens: number;
|
|
117
|
+
outputTokens: number;
|
|
118
|
+
cacheReadTokens?: number;
|
|
119
|
+
cacheWriteTokens?: number;
|
|
120
|
+
}, p: TokenPrice): number;
|
|
121
|
+
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
122
|
+
* quarter more than fresh input, a read a tenth of it. The fallback when a
|
|
123
|
+
* gateway quotes no cache price — for the mid-step ceiling (priceTurn) and
|
|
124
|
+
* the record's repricing (catalogCost) alike. */
|
|
125
|
+
export declare const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
126
|
+
export declare const CACHE_READ_MULTIPLIER = 0.1;
|
|
87
127
|
/** Catalogue entries as completion items: price per MTok, context, and the
|
|
88
128
|
* one capability that decides agent-worthiness. Tool-capable models first —
|
|
89
129
|
* on an agent platform they are almost always what the author wants. */
|
package/dist/src/catalog.js
CHANGED
|
@@ -29,7 +29,8 @@ const TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
29
29
|
const cacheFile = (baseUrl) => path.join(dataRoot(), "catalog",
|
|
30
30
|
// One file per endpoint, named legibly: catalog/openrouter.ai.json.
|
|
31
31
|
`${new URL(baseUrl).hostname.replace(/[^a-z0-9.-]/gi, "_")}.json`);
|
|
32
|
-
|
|
32
|
+
/** The gateway's /models payload as entries. Exported for its tests. */
|
|
33
|
+
export function parseModels(payload) {
|
|
33
34
|
const data = payload?.data;
|
|
34
35
|
if (!Array.isArray(data))
|
|
35
36
|
return [];
|
|
@@ -44,8 +45,14 @@ function parseModels(payload) {
|
|
|
44
45
|
? m.supported_parameters.map(String)
|
|
45
46
|
: null;
|
|
46
47
|
const pricing = (m.pricing ?? {});
|
|
48
|
+
// Unquoted is unknown, not free: Number(null) and Number("") are 0, and
|
|
49
|
+
// a gateway answering `input_cache_read: null` would have made caching
|
|
50
|
+
// cost nothing. An explicit "0" stays 0.
|
|
47
51
|
const price = (k) => {
|
|
48
|
-
const
|
|
52
|
+
const v = pricing[k];
|
|
53
|
+
if (v == null || (typeof v === "string" && v.trim() === ""))
|
|
54
|
+
return null;
|
|
55
|
+
const n = Number(v);
|
|
49
56
|
return Number.isFinite(n) ? n : null;
|
|
50
57
|
};
|
|
51
58
|
out.push({
|
|
@@ -54,6 +61,8 @@ function parseModels(payload) {
|
|
|
54
61
|
contextLength: Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : null,
|
|
55
62
|
promptPrice: price("prompt"),
|
|
56
63
|
completionPrice: price("completion"),
|
|
64
|
+
cacheReadPrice: price("input_cache_read"),
|
|
65
|
+
cacheWritePrice: price("input_cache_write"),
|
|
57
66
|
tools: supported ? supported.includes("tools") : null,
|
|
58
67
|
reasoning: supported ? supported.includes("reasoning") : null,
|
|
59
68
|
efforts: Array.isArray(m.reasoning?.supported_efforts)
|
|
@@ -200,8 +209,41 @@ export function catalogCost(catalog, modelId, usage) {
|
|
|
200
209
|
const m = findModel(catalog, modelId);
|
|
201
210
|
if (!m || m.promptPrice == null || m.completionPrice == null)
|
|
202
211
|
return null;
|
|
203
|
-
return usage
|
|
212
|
+
return costOf(usage, tokenPriceOf(m));
|
|
204
213
|
}
|
|
214
|
+
/** A catalogue entry's prices, or null when it has no input/output price. */
|
|
215
|
+
export function tokenPriceOf(m) {
|
|
216
|
+
if (!m || m.promptPrice == null || m.completionPrice == null)
|
|
217
|
+
return null;
|
|
218
|
+
return {
|
|
219
|
+
input: m.promptPrice,
|
|
220
|
+
output: m.completionPrice,
|
|
221
|
+
...(m.cacheReadPrice != null ? { cacheRead: m.cacheReadPrice } : {}),
|
|
222
|
+
...(m.cacheWritePrice != null ? { cacheWrite: m.cacheWritePrice } : {}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The cost of a set of counts at a price. `inputTokens` is all input; the
|
|
227
|
+
* cache fields are parts of it. Cached input is priced at the model's own
|
|
228
|
+
* cache prices when the gateway quotes them, and only otherwise at
|
|
229
|
+
* Anthropic's multipliers over the input price. Before the counts were
|
|
230
|
+
* split (18 Oct 2026) every cached token was billed at the full rate.
|
|
231
|
+
*/
|
|
232
|
+
export function costOf(usage, p) {
|
|
233
|
+
const read = usage.cacheReadTokens ?? 0;
|
|
234
|
+
const written = usage.cacheWriteTokens ?? 0;
|
|
235
|
+
const fresh = Math.max(0, usage.inputTokens - read - written);
|
|
236
|
+
return (fresh * p.input +
|
|
237
|
+
written * (p.cacheWrite ?? p.input * CACHE_WRITE_MULTIPLIER) +
|
|
238
|
+
read * (p.cacheRead ?? p.input * CACHE_READ_MULTIPLIER) +
|
|
239
|
+
usage.outputTokens * p.output);
|
|
240
|
+
}
|
|
241
|
+
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
242
|
+
* quarter more than fresh input, a read a tenth of it. The fallback when a
|
|
243
|
+
* gateway quotes no cache price — for the mid-step ceiling (priceTurn) and
|
|
244
|
+
* the record's repricing (catalogCost) alike. */
|
|
245
|
+
export const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
246
|
+
export const CACHE_READ_MULTIPLIER = 0.1;
|
|
205
247
|
/** Catalogue entries as completion items: price per MTok, context, and the
|
|
206
248
|
* one capability that decides agent-worthiness. Tool-capable models first —
|
|
207
249
|
* on an agent platform they are almost always what the author wants. */
|
package/dist/src/deploy.d.ts
CHANGED
|
@@ -97,7 +97,9 @@ export declare function deployIssues(files: DeployFile[]): DeployIssue[];
|
|
|
97
97
|
*/
|
|
98
98
|
export declare function runsInFlight(tenant: string, workspace: string): string[];
|
|
99
99
|
/** What a deploy would do, without doing it. */
|
|
100
|
-
export declare function planDeploy(tenant: string, workspace: string, files: DeployFile[]
|
|
100
|
+
export declare function planDeploy(tenant: string, workspace: string, files: DeployFile[], opts?: {
|
|
101
|
+
keepUntracked?: boolean;
|
|
102
|
+
}): DeployPlan;
|
|
101
103
|
export interface DeployOptions {
|
|
102
104
|
/** The commit these files came from, recorded so the dashboard can show it. */
|
|
103
105
|
commit?: string | null;
|
|
@@ -115,6 +117,9 @@ export interface DeployOptions {
|
|
|
115
117
|
* no window between the check and the swap. Absent: no check, as before.
|
|
116
118
|
*/
|
|
117
119
|
expectRemoved?: string[];
|
|
120
|
+
/** Keep what is not editable source (planDeploy, saveWorkspace): a
|
|
121
|
+
* restore takes history's tree, and history tracks source only. */
|
|
122
|
+
keepUntracked?: boolean;
|
|
118
123
|
}
|
|
119
124
|
/**
|
|
120
125
|
* Deploy a file list to a workspace, if it checks out.
|
package/dist/src/deploy.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import os from "node:os";
|
|
22
22
|
import path from "node:path";
|
|
23
|
-
import { saveWorkspace, keptOnDeploy, workspaceDir, listRunSummaries, parseFlow, parseToolDef, WORKSPACE_DIRS, assertSafeName, } from "./store.js";
|
|
23
|
+
import { saveWorkspace, keptOnDeploy, workspaceDir, listRunSummaries, listWorkspaceFiles, parseFlow, parseToolDef, WORKSPACE_DIRS, assertSafeName, } from "./store.js";
|
|
24
24
|
import { lintFlow } from "./flow-lint.js";
|
|
25
25
|
import { refNames } from "./refs.js";
|
|
26
26
|
import matter from "gray-matter";
|
|
@@ -438,7 +438,7 @@ export function runsInFlight(tenant, workspace) {
|
|
|
438
438
|
.map((r) => r.id);
|
|
439
439
|
}
|
|
440
440
|
/** What a deploy would do, without doing it. */
|
|
441
|
-
export function planDeploy(tenant, workspace, files) {
|
|
441
|
+
export function planDeploy(tenant, workspace, files, opts = {}) {
|
|
442
442
|
const dir = workspaceDir(tenant, workspace);
|
|
443
443
|
const shipped = new Map(files.map((f) => [path.normalize(f.path), f.content]));
|
|
444
444
|
const present = new Set();
|
|
@@ -480,7 +480,10 @@ export function planDeploy(tenant, workspace, files) {
|
|
|
480
480
|
// memory, the platform's own files. Reporting these as removals would be
|
|
481
481
|
// a lie, and the alarming kind; one predicate serves both.
|
|
482
482
|
const shippedPaths = new Set(shipped.keys());
|
|
483
|
-
|
|
483
|
+
// keepUntracked (a restore): what is not editable source stays, as
|
|
484
|
+
// saveWorkspace keeps it — see its meta.keepUntracked.
|
|
485
|
+
const tracked = opts.keepUntracked ? new Set(listWorkspaceFiles(tenant, workspace)) : null;
|
|
486
|
+
const removed = [...present].filter((rel) => !shipped.has(rel) && !keptOnDeploy(rel, shippedPaths) && (!tracked || tracked.has(rel)));
|
|
484
487
|
return {
|
|
485
488
|
files,
|
|
486
489
|
added: added.sort(),
|
|
@@ -502,7 +505,7 @@ export function planDeploy(tenant, workspace, files) {
|
|
|
502
505
|
export function deployWorkspace(tenant, workspace, files, opts = {}) {
|
|
503
506
|
assertSafeName(tenant, "tenant");
|
|
504
507
|
assertSafeName(workspace, "workspace");
|
|
505
|
-
const plan = planDeploy(tenant, workspace, files);
|
|
508
|
+
const plan = planDeploy(tenant, workspace, files, { keepUntracked: opts.keepUntracked });
|
|
506
509
|
const blocked = plan.blockedBy.length > 0 && !opts.force;
|
|
507
510
|
if (plan.issues.length > 0 || blocked) {
|
|
508
511
|
return { ...plan, applied: false, commit: null, preserved: 0 };
|
|
@@ -514,7 +517,7 @@ export function deployWorkspace(tenant, workspace, files, opts = {}) {
|
|
|
514
517
|
return { ...plan, applied: false, commit: null, preserved: 0, unexpectedRemovals };
|
|
515
518
|
}
|
|
516
519
|
}
|
|
517
|
-
const { preserved } = saveWorkspace(tenant, workspace, files, { commit: opts.commit ?? null, by: opts.by ?? "deploy", message: opts.message });
|
|
520
|
+
const { preserved } = saveWorkspace(tenant, workspace, files, { commit: opts.commit ?? null, by: opts.by ?? "deploy", message: opts.message, keepUntracked: opts.keepUntracked });
|
|
518
521
|
const commit = opts.commit ?? null;
|
|
519
522
|
if (commit)
|
|
520
523
|
writeDeployedCommit(tenant, workspace, commit);
|
|
@@ -60,7 +60,7 @@ export function credentialLine(c) {
|
|
|
60
60
|
//
|
|
61
61
|
// An account brings its own model key without writing a provider: block:
|
|
62
62
|
// three secrets in the ACCOUNT vault (set from Settings → Model, the API's
|
|
63
|
-
// /api/account/model, or `foldrun model set`). A provider: block at any
|
|
63
|
+
// /api/account/model, or `foldrun account model set`). A provider: block at any
|
|
64
64
|
// level still wins; this is what a step falls to when none names one, before
|
|
65
65
|
// the platform's own credential.
|
|
66
66
|
/** The account vault names the account's own model key lives under. */
|
package/dist/src/model-env.js
CHANGED
|
@@ -19,7 +19,7 @@ export class ModelKeyError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
export const NO_MODEL_KEY = "no model key: this account runs on its own API key, and none is set — add one in Settings → Model " +
|
|
22
|
-
"(or `foldrun model set <provider> --key …`), or write a provider: block in AGENTS.md";
|
|
22
|
+
"(or `foldrun account model set <provider> --key …`), or write a provider: block in AGENTS.md";
|
|
23
23
|
/**
|
|
24
24
|
* `platform` is a function: the caller's own way to build the platform's env
|
|
25
25
|
* (a step's, or the SDK's host env), called only when this account may use
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type HookRecord } from "./hooks.ts";
|
|
2
2
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { type ApiSpec, type Effort } from "./store.ts";
|
|
4
|
-
import type { EventExtra } from "./step-exec.ts";
|
|
4
|
+
import type { EventExtra, ExecUsage } from "./step-exec.ts";
|
|
5
|
+
import type { TokenPrice } from "./catalog.ts";
|
|
5
6
|
import { type RuntimeSpec } from "./runtime.ts";
|
|
6
7
|
import { type NetworkLevel } from "./network.ts";
|
|
7
8
|
import type { ScriptSpec } from "./script-tools.ts";
|
|
@@ -41,10 +42,7 @@ export interface ContainerStepInput {
|
|
|
41
42
|
* step-exec.ts. Values, so the driver can enforce them in the pod. */
|
|
42
43
|
budgetUsd?: number | null;
|
|
43
44
|
budgetNote?: string;
|
|
44
|
-
price?:
|
|
45
|
-
input: number;
|
|
46
|
-
output: number;
|
|
47
|
-
} | null;
|
|
45
|
+
price?: TokenPrice | null;
|
|
48
46
|
verify?: string;
|
|
49
47
|
/** `output: json` — checked inside, where the reply is. */
|
|
50
48
|
output?: "json";
|
|
@@ -146,10 +144,7 @@ export interface ContainerStepOutcome {
|
|
|
146
144
|
/** The parsed JSON of an `output: json` step, crossing back as a value. */
|
|
147
145
|
data?: unknown;
|
|
148
146
|
costUsd: number | null;
|
|
149
|
-
usage?:
|
|
150
|
-
inputTokens: number;
|
|
151
|
-
outputTokens: number;
|
|
152
|
-
} | null;
|
|
147
|
+
usage?: ExecUsage | null;
|
|
153
148
|
/** Every turn priced at its own model, when sub-agents ran on others —
|
|
154
149
|
* see ExecOutcome.turnsCostUsd. */
|
|
155
150
|
turnsCostUsd?: number;
|
|
@@ -1068,7 +1068,14 @@ export function parseDriverLine(line) {
|
|
|
1068
1068
|
usage: parsed.usage &&
|
|
1069
1069
|
typeof parsed.usage.inputTokens === "number" &&
|
|
1070
1070
|
typeof parsed.usage.outputTokens === "number"
|
|
1071
|
-
? {
|
|
1071
|
+
? {
|
|
1072
|
+
inputTokens: parsed.usage.inputTokens,
|
|
1073
|
+
outputTokens: parsed.usage.outputTokens,
|
|
1074
|
+
// A driver from before the split sends neither; zero, not
|
|
1075
|
+
// unknown, keeps the record's shape one thing.
|
|
1076
|
+
cacheReadTokens: typeof parsed.usage.cacheReadTokens === "number" ? parsed.usage.cacheReadTokens : 0,
|
|
1077
|
+
cacheWriteTokens: typeof parsed.usage.cacheWriteTokens === "number" ? parsed.usage.cacheWriteTokens : 0,
|
|
1078
|
+
}
|
|
1072
1079
|
: null,
|
|
1073
1080
|
res: parsed.res && typeof parsed.res === "object"
|
|
1074
1081
|
? {
|
package/dist/src/runner.d.ts
CHANGED
|
@@ -354,6 +354,8 @@ grantsOnly?: boolean): {
|
|
|
354
354
|
export declare function repriced(catalog: Catalog | null, model: string, usage: {
|
|
355
355
|
inputTokens: number;
|
|
356
356
|
outputTokens: number;
|
|
357
|
+
cacheReadTokens?: number;
|
|
358
|
+
cacheWriteTokens?: number;
|
|
357
359
|
} | null, sdkCost: number | null, push: (type: "info" | "error", text: string) => void,
|
|
358
360
|
/** The step's turns each priced at its own model — set when a sub-agent
|
|
359
361
|
* ran on a different model (ExecOutcome.turnsCostUsd). The lump `usage`
|
package/dist/src/runner.js
CHANGED
|
@@ -33,8 +33,8 @@ import { ASK_TOOL, applyOperatorEvent, askPromptLine, askTimeoutSec, buildAskToo
|
|
|
33
33
|
import { sendPlainNotification } from "./notify.js";
|
|
34
34
|
import { TOOL_MAP, BUILTIN_TOOLS, ownToolNames, toolRefs, legacyUseNames, legacyUseError, retiredToolNames, retiredToolError } from "./tool-names.js";
|
|
35
35
|
import { refNames } from "./refs.js";
|
|
36
|
-
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, liveRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun, runSummary, verdictOf, sumAttempts, parseSchemaText, STORAGE_DIR, adoptLegacyVersionKey, listAgents, } from "./store.js";
|
|
37
|
-
import { loadCatalog, checkModel, clampEffort, catalogCost, findModel } from "./catalog.js";
|
|
36
|
+
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, liveRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun, runSummary, verdictOf, sumAttempts, tokensOf, parseSchemaText, STORAGE_DIR, adoptLegacyVersionKey, listAgents, } from "./store.js";
|
|
37
|
+
import { loadCatalog, checkModel, clampEffort, catalogCost, findModel, tokenPriceOf } from "./catalog.js";
|
|
38
38
|
import { resolveSecrets, getSecret, materializeSecrets, secretSites } from "./secrets.js";
|
|
39
39
|
import { resolveNetwork, networkLine, proxyEnv, EGRESS_CA_ENV } from "./network.js";
|
|
40
40
|
import { materializeFileSecrets, cleanupFileSecrets } from "./secret-files.js";
|
|
@@ -1876,8 +1876,12 @@ stepIndex) {
|
|
|
1876
1876
|
// that names a provider uses its own; otherwise the platform's (in a
|
|
1877
1877
|
// run container) or this process's — never a login that just happens
|
|
1878
1878
|
// to be on the machine (model-credential.ts).
|
|
1879
|
-
|
|
1880
|
-
|
|
1879
|
+
// An account refused the platform's credential (below) is never shown
|
|
1880
|
+
// running on it: the record said "Claude login token" on a step that
|
|
1881
|
+
// was then refused for having no key (FOL-14, 18 Oct 2026).
|
|
1882
|
+
const noKey = !providerLabel && !platform.platformKeyAllowed(tenant);
|
|
1883
|
+
const credential = providerLabel || noKey ? null : resolveModelCredential(isolatedRun() ? platformModelEnv() : process.env);
|
|
1884
|
+
step.credential = credential ? credential.label : noKey ? "none — this account has no model key" : `provider: ${providerLabel}`;
|
|
1881
1885
|
if (credential) {
|
|
1882
1886
|
const line = credentialLine(credential);
|
|
1883
1887
|
push(line.type, line.text);
|
|
@@ -1890,7 +1894,7 @@ stepIndex) {
|
|
|
1890
1894
|
const own = Object.values(providerSecrets).concat(Object.values(providerEnv));
|
|
1891
1895
|
if (!providerLabel) {
|
|
1892
1896
|
push("error", `no model key: this account runs on its own API key, and none is set. Add one in Settings → Model ` +
|
|
1893
|
-
`(or \`foldrun model set <provider> --key …\`), or write a provider: block in AGENTS.md`);
|
|
1897
|
+
`(or \`foldrun account model set <provider> --key …\`), or write a provider: block in AGENTS.md`);
|
|
1894
1898
|
step.status = "failed";
|
|
1895
1899
|
save();
|
|
1896
1900
|
return;
|
|
@@ -1998,10 +2002,7 @@ stepIndex) {
|
|
|
1998
2002
|
const verdict = checkModel(catalog, wireModel, { tools: needsTools });
|
|
1999
2003
|
// Also what a token costs here, so a step can count its own spend
|
|
2000
2004
|
// turn by turn against its ceiling instead of learning at the end.
|
|
2001
|
-
|
|
2002
|
-
if (priced && priced.promptPrice != null && priced.completionPrice != null) {
|
|
2003
|
-
tokenPrice = { input: priced.promptPrice, output: priced.completionPrice };
|
|
2004
|
-
}
|
|
2005
|
+
tokenPrice = tokenPriceOf(findModel(catalog, wireModel));
|
|
2005
2006
|
if (!verdict.ok) {
|
|
2006
2007
|
push("error", verdict.reason);
|
|
2007
2008
|
step.status = "failed";
|
|
@@ -2621,7 +2622,7 @@ stepIndex) {
|
|
|
2621
2622
|
step.podLostTry = {
|
|
2622
2623
|
status: "lost",
|
|
2623
2624
|
costUsd: repriced(catalog, wireModel, first.usage ?? null, first.costUsd, push, first.turnsCostUsd),
|
|
2624
|
-
tokens: first.usage
|
|
2625
|
+
tokens: tokensOf(first.usage),
|
|
2625
2626
|
computeSecs: first.timing ? Math.round(first.timing.totalMs) / 1000 : null,
|
|
2626
2627
|
finishedAt: new Date().toISOString(),
|
|
2627
2628
|
error: decision.note.slice(0, 500),
|
|
@@ -2671,9 +2672,7 @@ stepIndex) {
|
|
|
2671
2672
|
if (outcome.data !== undefined)
|
|
2672
2673
|
step.data = redactData(outcome.data);
|
|
2673
2674
|
step.costUsd = repriced(catalog, wireModel, outcome.usage ?? null, outcome.costUsd, push, outcome.turnsCostUsd);
|
|
2674
|
-
step.tokens = outcome.usage
|
|
2675
|
-
? { input: outcome.usage.inputTokens, output: outcome.usage.outputTokens }
|
|
2676
|
-
: null;
|
|
2675
|
+
step.tokens = tokensOf(outcome.usage);
|
|
2677
2676
|
step.actual = outcome.res ?? null;
|
|
2678
2677
|
if (outcome.res?.peakMemBytes || outcome.res?.rxBytes) {
|
|
2679
2678
|
const mb = (n) => (n === null ? "?" : (n / 1024 / 1024).toFixed(0));
|
|
@@ -2870,9 +2869,7 @@ stepIndex) {
|
|
|
2870
2869
|
const consultCost = consultTools.drainCost();
|
|
2871
2870
|
const stepCost = repriced(catalog, wireModel, outcome.usage, outcome.costUsd, push, outcome.turnsCostUsd);
|
|
2872
2871
|
step.costUsd = stepCost === null && consultCost === 0 ? null : (stepCost ?? 0) + consultCost;
|
|
2873
|
-
step.tokens = outcome.usage
|
|
2874
|
-
? { input: outcome.usage.inputTokens, output: outcome.usage.outputTokens }
|
|
2875
|
-
: null;
|
|
2872
|
+
step.tokens = tokensOf(outcome.usage);
|
|
2876
2873
|
for (const line of apiTools.drainLog())
|
|
2877
2874
|
push("info", `api: ${line}`);
|
|
2878
2875
|
for (const line of scriptTools.drainLog())
|
package/dist/src/step-exec.d.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { type HookDef, type HookRecord } from "./hooks.ts";
|
|
2
2
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import type { Effort } from "./store.ts";
|
|
4
|
+
import { type TokenPrice } from "./catalog.ts";
|
|
4
5
|
import type { TestEffect } from "./test-mode.ts";
|
|
5
6
|
import type { OperatorEvent } from "./operator.ts";
|
|
6
7
|
import { type SubagentSpec } from "./subagents.ts";
|
|
7
8
|
import { type Limits } from "./limits.ts";
|
|
8
9
|
import { type BrowserPodOutcome, type ClassifyContext } from "./browser-pod.ts";
|
|
10
|
+
export interface ExecUsage {
|
|
11
|
+
inputTokens: number;
|
|
12
|
+
outputTokens: number;
|
|
13
|
+
cacheReadTokens: number;
|
|
14
|
+
cacheWriteTokens: number;
|
|
15
|
+
}
|
|
16
|
+
/** The four counts from one SDK usage object (snake_case, as the API
|
|
17
|
+
* reports it). The API counts fresh input apart from the cache's; here
|
|
18
|
+
* `inputTokens` is all of it, as it always was on the record, and the
|
|
19
|
+
* cache fields say how much of that the cache served or stored. */
|
|
20
|
+
export declare function usageFromSdk(u: Record<string, number | undefined>): ExecUsage;
|
|
9
21
|
export interface ExecOutcome {
|
|
10
22
|
status: "completed" | "failed";
|
|
11
23
|
result: string | null;
|
|
@@ -20,11 +32,12 @@ export interface ExecOutcome {
|
|
|
20
32
|
costUsd: number | null;
|
|
21
33
|
/** Token counts off the SDK's result message. costUsd is priced from
|
|
22
34
|
* Anthropic's table, which is wrong for a routed model — these are the
|
|
23
|
-
* raw numbers a caller with a gateway's own prices can reprice from.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
* raw numbers a caller with a gateway's own prices can reprice from.
|
|
36
|
+
* `inputTokens` is every input token; `cacheReadTokens` and
|
|
37
|
+
* `cacheWriteTokens` are the part of it the prompt cache served or
|
|
38
|
+
* stored, so a usage page can say how much of a step's context was
|
|
39
|
+
* re-read for a tenth of the price. */
|
|
40
|
+
usage: ExecUsage | null;
|
|
28
41
|
/** Set only when some turns ran on a different model from the step's — a
|
|
29
42
|
* sub-agent with its own `model:`. The sum of every turn, each priced at
|
|
30
43
|
* its own model. `usage` above is one lump and cannot be repriced at the
|
|
@@ -77,10 +90,7 @@ export interface ExecOptions {
|
|
|
77
90
|
/** Price per token for the model, when the catalogue knows it, so spend
|
|
78
91
|
* can be counted turn by turn instead of learned at the end. Without it
|
|
79
92
|
* the step prices its turns from the SDK's own running total. */
|
|
80
|
-
price?:
|
|
81
|
-
input: number;
|
|
82
|
-
output: number;
|
|
83
|
-
} | null;
|
|
93
|
+
price?: TokenPrice | null;
|
|
84
94
|
/** A check the step must pass to count as done: a shell command that must
|
|
85
95
|
* exit 0, or an eval-style assertion (`contains: x`, `not-contains: x`,
|
|
86
96
|
* `matches: re`, `file: path`, `judge: sentence`) — see checkVerify. */
|
|
@@ -178,15 +188,19 @@ export declare function knownPrice(model: string | undefined): {
|
|
|
178
188
|
/**
|
|
179
189
|
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
180
190
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
191
|
+
* Cache reads and writes are priced at the model's own cache prices when
|
|
192
|
+
* the catalogue quotes them, else at Anthropic's multipliers (costOf in
|
|
193
|
+
* catalog.ts, the same arithmetic the record is repriced with). Counting
|
|
194
|
+
* both at the full input rate — as this did —
|
|
195
|
+
* makes a long cached prompt read about ten times its price, which on a
|
|
196
|
+
* ceiling is not "the honest direction to be wrong in", it is a step
|
|
197
|
+
* killed for spending money it never spent.
|
|
198
|
+
*
|
|
199
|
+
* The run record's own cost is repriced from the catalogue afterwards and
|
|
200
|
+
* stays the authority on the bill. This number exists to stop a runaway
|
|
201
|
+
* step mid-turn, and wants to be close.
|
|
185
202
|
*/
|
|
186
|
-
export declare function priceTurn(u: Record<string, number | undefined>, price:
|
|
187
|
-
input: number;
|
|
188
|
-
output: number;
|
|
189
|
-
} | null): number;
|
|
203
|
+
export declare function priceTurn(u: Record<string, number | undefined>, price: TokenPrice | null): number;
|
|
190
204
|
/**
|
|
191
205
|
* Each step launched together gets an equal share of what is left of the
|
|
192
206
|
* run's budget: the shares sum to the remainder, so a group cannot end
|
package/dist/src/step-exec.js
CHANGED
|
@@ -12,6 +12,7 @@ import fs from "node:fs";
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
15
|
+
import { costOf } from "./catalog.js";
|
|
15
16
|
import { spawn } from "node:child_process";
|
|
16
17
|
import { checkPaths, checkBash, isFilesystemTool, linkWorkspace, workspaceLinkEnv, resolveAgentPath, isWithin } from "./confine.js";
|
|
17
18
|
import { DELEGATE_TOOLS, subagentGuard, toAgentDefinitions } from "./subagents.js";
|
|
@@ -19,6 +20,18 @@ import { hostSafeEnv } from "./host-env.js";
|
|
|
19
20
|
import { validateSchema, describeSchemaErrors, looksLikeSchema } from "./json-schema.js";
|
|
20
21
|
import { CallCounter, limitKeysFor } from "./limits.js";
|
|
21
22
|
import { classifyCall, podAt, readPodEvents } from "./browser-pod.js";
|
|
23
|
+
/** The four counts from one SDK usage object (snake_case, as the API
|
|
24
|
+
* reports it). The API counts fresh input apart from the cache's; here
|
|
25
|
+
* `inputTokens` is all of it, as it always was on the record, and the
|
|
26
|
+
* cache fields say how much of that the cache served or stored. */
|
|
27
|
+
export function usageFromSdk(u) {
|
|
28
|
+
return {
|
|
29
|
+
inputTokens: (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0),
|
|
30
|
+
outputTokens: u.output_tokens ?? 0,
|
|
31
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
32
|
+
cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
22
35
|
/** How often the stop flag is read while a step runs. */
|
|
23
36
|
export const STOP_POLL_MS = 2000;
|
|
24
37
|
/** The least a `verify:` gets when the step used up most of its `timeout:` —
|
|
@@ -65,32 +78,23 @@ export function knownPrice(model) {
|
|
|
65
78
|
return null;
|
|
66
79
|
return KNOWN_PRICES.find(([re]) => re.test(model))?.[1] ?? null;
|
|
67
80
|
}
|
|
68
|
-
/** Anthropic's cache multipliers, and the shape every gateway that bills
|
|
69
|
-
* for caching at all has copied: a write costs a quarter more than fresh
|
|
70
|
-
* input, a read a tenth of it. Counting both at the full input rate — as
|
|
71
|
-
* this did — makes a long cached prompt read about ten times its price,
|
|
72
|
-
* which on a ceiling is not "the honest direction to be wrong in", it is
|
|
73
|
-
* a step killed for spending money it never spent. */
|
|
74
|
-
const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
75
|
-
const CACHE_READ_MULTIPLIER = 0.1;
|
|
76
81
|
/**
|
|
77
82
|
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
78
83
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
84
|
+
* Cache reads and writes are priced at the model's own cache prices when
|
|
85
|
+
* the catalogue quotes them, else at Anthropic's multipliers (costOf in
|
|
86
|
+
* catalog.ts, the same arithmetic the record is repriced with). Counting
|
|
87
|
+
* both at the full input rate — as this did —
|
|
88
|
+
* makes a long cached prompt read about ten times its price, which on a
|
|
89
|
+
* ceiling is not "the honest direction to be wrong in", it is a step
|
|
90
|
+
* killed for spending money it never spent.
|
|
91
|
+
*
|
|
92
|
+
* The run record's own cost is repriced from the catalogue afterwards and
|
|
93
|
+
* stays the authority on the bill. This number exists to stop a runaway
|
|
94
|
+
* step mid-turn, and wants to be close.
|
|
83
95
|
*/
|
|
84
96
|
export function priceTurn(u, price) {
|
|
85
|
-
|
|
86
|
-
const fresh = u.input_tokens ?? 0;
|
|
87
|
-
const written = u.cache_creation_input_tokens ?? 0;
|
|
88
|
-
const read = u.cache_read_input_tokens ?? 0;
|
|
89
|
-
const outTok = u.output_tokens ?? 0;
|
|
90
|
-
return (fresh * p.input +
|
|
91
|
-
written * p.input * CACHE_WRITE_MULTIPLIER +
|
|
92
|
-
read * p.input * CACHE_READ_MULTIPLIER +
|
|
93
|
-
outTok * p.output);
|
|
97
|
+
return costOf(usageFromSdk(u), price ?? FALLBACK_PRICE);
|
|
94
98
|
}
|
|
95
99
|
/**
|
|
96
100
|
* Each step launched together gets an equal share of what is left of the
|
|
@@ -639,16 +643,10 @@ async function executeStepInner(opts, runQuery) {
|
|
|
639
643
|
costUsd = "total_cost_usd" in message ? (message.total_cost_usd ?? null) : null;
|
|
640
644
|
stopReason = "stop_reason" in message && typeof message.stop_reason === "string" ? message.stop_reason : null;
|
|
641
645
|
if ("usage" in message && message.usage) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
// gateways with cheaper cache reads, which errs on the honest side.
|
|
647
|
-
inputTokens: (u.input_tokens ?? 0) +
|
|
648
|
-
(u.cache_creation_input_tokens ?? 0) +
|
|
649
|
-
(u.cache_read_input_tokens ?? 0),
|
|
650
|
-
outputTokens: u.output_tokens ?? 0,
|
|
651
|
-
};
|
|
646
|
+
// Cache reads and writes counted beside the input total: with only
|
|
647
|
+
// the total (until 18 Oct 2026) the hit rate could not be read and
|
|
648
|
+
// the catalogue repriced every cached token at the full rate.
|
|
649
|
+
usage = usageFromSdk(message.usage);
|
|
652
650
|
}
|
|
653
651
|
}
|
|
654
652
|
}
|
|
@@ -671,16 +669,18 @@ async function executeStepInner(opts, runQuery) {
|
|
|
671
669
|
// died — means no total from the SDK. The turns it did take were billed by
|
|
672
670
|
// the provider all the same, so the step's cost is their sum.
|
|
673
671
|
if (costUsd === null && turns.size) {
|
|
674
|
-
|
|
675
|
-
let output = 0;
|
|
672
|
+
const summed = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
676
673
|
let priced = 0;
|
|
677
674
|
for (const { u: t, price } of turns.values()) {
|
|
678
|
-
|
|
679
|
-
|
|
675
|
+
const one = usageFromSdk(t);
|
|
676
|
+
summed.inputTokens += one.inputTokens;
|
|
677
|
+
summed.outputTokens += one.outputTokens;
|
|
678
|
+
summed.cacheReadTokens += one.cacheReadTokens;
|
|
679
|
+
summed.cacheWriteTokens += one.cacheWriteTokens;
|
|
680
680
|
priced += priceTurn(t, price);
|
|
681
681
|
}
|
|
682
682
|
costUsd = priced;
|
|
683
|
-
usage ??=
|
|
683
|
+
usage ??= summed;
|
|
684
684
|
emit("info", `cost from ${turns.size} model turn${turns.size === 1 ? "" : "s"}: $${priced.toFixed(4)} — the step ended before the model's closing total`);
|
|
685
685
|
}
|
|
686
686
|
if (ended === "timeout") {
|