@foldrun/core 2.1.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 +35 -2
- package/dist/src/catalog.js +39 -12
- package/dist/src/run-container.d.ts +2 -4
- package/dist/src/runner.js +2 -5
- package/dist/src/step-exec.d.ts +10 -14
- package/dist/src/step-exec.js +9 -16
- package/package.json +1 -1
- package/src/catalog.ts +60 -12
- package/src/run-container.ts +2 -1
- package/src/runner.ts +3 -6
- package/src/step-exec.ts +13 -22
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;
|
|
@@ -86,9 +95,33 @@ export declare function catalogCost(catalog: Catalog | null, modelId: string, us
|
|
|
86
95
|
cacheReadTokens?: number;
|
|
87
96
|
cacheWriteTokens?: number;
|
|
88
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;
|
|
89
121
|
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
90
|
-
* quarter more than fresh input, a read a tenth of it.
|
|
91
|
-
* mid-step ceiling (priceTurn) and
|
|
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. */
|
|
92
125
|
export declare const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
93
126
|
export declare const CACHE_READ_MULTIPLIER = 0.1;
|
|
94
127
|
/** Catalogue entries as completion items: price per MTok, context, and the
|
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,21 +209,39 @@ 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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
return costOf(usage, tokenPriceOf(m));
|
|
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) {
|
|
209
233
|
const read = usage.cacheReadTokens ?? 0;
|
|
210
234
|
const written = usage.cacheWriteTokens ?? 0;
|
|
211
235
|
const fresh = Math.max(0, usage.inputTokens - read - written);
|
|
212
|
-
|
|
213
|
-
|
|
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);
|
|
214
240
|
}
|
|
215
241
|
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
216
|
-
* quarter more than fresh input, a read a tenth of it.
|
|
217
|
-
* mid-step ceiling (priceTurn) and
|
|
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. */
|
|
218
245
|
export const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
219
246
|
export const CACHE_READ_MULTIPLIER = 0.1;
|
|
220
247
|
/** Catalogue entries as completion items: price per MTok, context, and the
|
|
@@ -2,6 +2,7 @@ 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
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";
|
package/dist/src/runner.js
CHANGED
|
@@ -34,7 +34,7 @@ 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
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 } from "./catalog.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";
|
|
@@ -2002,10 +2002,7 @@ stepIndex) {
|
|
|
2002
2002
|
const verdict = checkModel(catalog, wireModel, { tools: needsTools });
|
|
2003
2003
|
// Also what a token costs here, so a step can count its own spend
|
|
2004
2004
|
// turn by turn against its ceiling instead of learning at the end.
|
|
2005
|
-
|
|
2006
|
-
if (priced && priced.promptPrice != null && priced.completionPrice != null) {
|
|
2007
|
-
tokenPrice = { input: priced.promptPrice, output: priced.completionPrice };
|
|
2008
|
-
}
|
|
2005
|
+
tokenPrice = tokenPriceOf(findModel(catalog, wireModel));
|
|
2009
2006
|
if (!verdict.ok) {
|
|
2010
2007
|
push("error", verdict.reason);
|
|
2011
2008
|
step.status = "failed";
|
package/dist/src/step-exec.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
@@ -89,10 +90,7 @@ export interface ExecOptions {
|
|
|
89
90
|
/** Price per token for the model, when the catalogue knows it, so spend
|
|
90
91
|
* can be counted turn by turn instead of learned at the end. Without it
|
|
91
92
|
* the step prices its turns from the SDK's own running total. */
|
|
92
|
-
price?:
|
|
93
|
-
input: number;
|
|
94
|
-
output: number;
|
|
95
|
-
} | null;
|
|
93
|
+
price?: TokenPrice | null;
|
|
96
94
|
/** A check the step must pass to count as done: a shell command that must
|
|
97
95
|
* exit 0, or an eval-style assertion (`contains: x`, `not-contains: x`,
|
|
98
96
|
* `matches: re`, `file: path`, `judge: sentence`) — see checkVerify. */
|
|
@@ -190,21 +188,19 @@ export declare function knownPrice(model: string | undefined): {
|
|
|
190
188
|
/**
|
|
191
189
|
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
192
190
|
*
|
|
193
|
-
* Cache reads and writes are priced at
|
|
194
|
-
*
|
|
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
195
|
* makes a long cached prompt read about ten times its price, which on a
|
|
196
196
|
* ceiling is not "the honest direction to be wrong in", it is a step
|
|
197
197
|
* killed for spending money it never spent.
|
|
198
198
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* number exists to stop a runaway step mid-turn, and wants to be close.
|
|
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.
|
|
203
202
|
*/
|
|
204
|
-
export declare function priceTurn(u: Record<string, number | undefined>, price:
|
|
205
|
-
input: number;
|
|
206
|
-
output: number;
|
|
207
|
-
} | null): number;
|
|
203
|
+
export declare function priceTurn(u: Record<string, number | undefined>, price: TokenPrice | null): number;
|
|
208
204
|
/**
|
|
209
205
|
* Each step launched together gets an equal share of what is left of the
|
|
210
206
|
* run's budget: the shares sum to the remainder, so a group cannot end
|
package/dist/src/step-exec.js
CHANGED
|
@@ -12,7 +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 {
|
|
15
|
+
import { costOf } from "./catalog.js";
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
17
|
import { checkPaths, checkBash, isFilesystemTool, linkWorkspace, workspaceLinkEnv, resolveAgentPath, isWithin } from "./confine.js";
|
|
18
18
|
import { DELEGATE_TOOLS, subagentGuard, toAgentDefinitions } from "./subagents.js";
|
|
@@ -81,27 +81,20 @@ export function knownPrice(model) {
|
|
|
81
81
|
/**
|
|
82
82
|
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
83
83
|
*
|
|
84
|
-
* Cache reads and writes are priced at
|
|
85
|
-
*
|
|
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 —
|
|
86
88
|
* makes a long cached prompt read about ten times its price, which on a
|
|
87
89
|
* ceiling is not "the honest direction to be wrong in", it is a step
|
|
88
90
|
* killed for spending money it never spent.
|
|
89
91
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* number exists to stop a runaway step mid-turn, and wants to be close.
|
|
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.
|
|
94
95
|
*/
|
|
95
96
|
export function priceTurn(u, price) {
|
|
96
|
-
|
|
97
|
-
const fresh = u.input_tokens ?? 0;
|
|
98
|
-
const written = u.cache_creation_input_tokens ?? 0;
|
|
99
|
-
const read = u.cache_read_input_tokens ?? 0;
|
|
100
|
-
const outTok = u.output_tokens ?? 0;
|
|
101
|
-
return (fresh * p.input +
|
|
102
|
-
written * p.input * CACHE_WRITE_MULTIPLIER +
|
|
103
|
-
read * p.input * CACHE_READ_MULTIPLIER +
|
|
104
|
-
outTok * p.output);
|
|
97
|
+
return costOf(usageFromSdk(u), price ?? FALLBACK_PRICE);
|
|
105
98
|
}
|
|
106
99
|
/**
|
|
107
100
|
* Each step launched together gets an equal share of what is left of the
|
package/package.json
CHANGED
package/src/catalog.ts
CHANGED
|
@@ -34,6 +34,13 @@ export interface CatalogModel {
|
|
|
34
34
|
promptPrice: number | null;
|
|
35
35
|
/** USD per output token. */
|
|
36
36
|
completionPrice: number | null;
|
|
37
|
+
/** USD per input token read from / written to the prompt cache, when the
|
|
38
|
+
* gateway quotes them (OpenRouter's input_cache_read / input_cache_write).
|
|
39
|
+
* Models discount their cache differently — GPT-4o reads at half the
|
|
40
|
+
* input price, Claude at a tenth — so a quoted price beats any multiplier.
|
|
41
|
+
* Absent on catalogues cached before 2.1.1. */
|
|
42
|
+
cacheReadPrice?: number | null;
|
|
43
|
+
cacheWritePrice?: number | null;
|
|
37
44
|
/** Can this model make tool calls? Null when the gateway doesn't say. */
|
|
38
45
|
tools: boolean | null;
|
|
39
46
|
/** Does it take a reasoning-depth parameter? (What `effort:` needs.) */
|
|
@@ -63,7 +70,8 @@ const cacheFile = (baseUrl: string) =>
|
|
|
63
70
|
`${new URL(baseUrl).hostname.replace(/[^a-z0-9.-]/gi, "_")}.json`,
|
|
64
71
|
);
|
|
65
72
|
|
|
66
|
-
|
|
73
|
+
/** The gateway's /models payload as entries. Exported for its tests. */
|
|
74
|
+
export function parseModels(payload: unknown): CatalogModel[] {
|
|
67
75
|
const data = (payload as { data?: unknown[] })?.data;
|
|
68
76
|
if (!Array.isArray(data)) return [];
|
|
69
77
|
const out: CatalogModel[] = [];
|
|
@@ -75,8 +83,13 @@ function parseModels(payload: unknown): CatalogModel[] {
|
|
|
75
83
|
? m.supported_parameters.map(String)
|
|
76
84
|
: null;
|
|
77
85
|
const pricing = (m.pricing ?? {}) as Record<string, unknown>;
|
|
86
|
+
// Unquoted is unknown, not free: Number(null) and Number("") are 0, and
|
|
87
|
+
// a gateway answering `input_cache_read: null` would have made caching
|
|
88
|
+
// cost nothing. An explicit "0" stays 0.
|
|
78
89
|
const price = (k: string) => {
|
|
79
|
-
const
|
|
90
|
+
const v = pricing[k];
|
|
91
|
+
if (v == null || (typeof v === "string" && v.trim() === "")) return null;
|
|
92
|
+
const n = Number(v);
|
|
80
93
|
return Number.isFinite(n) ? n : null;
|
|
81
94
|
};
|
|
82
95
|
out.push({
|
|
@@ -85,6 +98,8 @@ function parseModels(payload: unknown): CatalogModel[] {
|
|
|
85
98
|
contextLength: Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : null,
|
|
86
99
|
promptPrice: price("prompt"),
|
|
87
100
|
completionPrice: price("completion"),
|
|
101
|
+
cacheReadPrice: price("input_cache_read"),
|
|
102
|
+
cacheWritePrice: price("input_cache_write"),
|
|
88
103
|
tools: supported ? supported.includes("tools") : null,
|
|
89
104
|
reasoning: supported ? supported.includes("reasoning") : null,
|
|
90
105
|
efforts: Array.isArray((m.reasoning as Record<string, unknown> | undefined)?.supported_efforts)
|
|
@@ -249,22 +264,55 @@ export function catalogCost(
|
|
|
249
264
|
): number | null {
|
|
250
265
|
const m = findModel(catalog, modelId);
|
|
251
266
|
if (!m || m.promptPrice == null || m.completionPrice == null) return null;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
267
|
+
return costOf(usage, tokenPriceOf(m)!);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** What a token costs on one model: input, output, and — when the gateway
|
|
271
|
+
* quotes them — reads from and writes to the prompt cache. */
|
|
272
|
+
export interface TokenPrice {
|
|
273
|
+
input: number;
|
|
274
|
+
output: number;
|
|
275
|
+
cacheRead?: number | null;
|
|
276
|
+
cacheWrite?: number | null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** A catalogue entry's prices, or null when it has no input/output price. */
|
|
280
|
+
export function tokenPriceOf(m: CatalogModel | null | undefined): TokenPrice | null {
|
|
281
|
+
if (!m || m.promptPrice == null || m.completionPrice == null) return null;
|
|
282
|
+
return {
|
|
283
|
+
input: m.promptPrice,
|
|
284
|
+
output: m.completionPrice,
|
|
285
|
+
...(m.cacheReadPrice != null ? { cacheRead: m.cacheReadPrice } : {}),
|
|
286
|
+
...(m.cacheWritePrice != null ? { cacheWrite: m.cacheWritePrice } : {}),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The cost of a set of counts at a price. `inputTokens` is all input; the
|
|
292
|
+
* cache fields are parts of it. Cached input is priced at the model's own
|
|
293
|
+
* cache prices when the gateway quotes them, and only otherwise at
|
|
294
|
+
* Anthropic's multipliers over the input price. Before the counts were
|
|
295
|
+
* split (18 Oct 2026) every cached token was billed at the full rate.
|
|
296
|
+
*/
|
|
297
|
+
export function costOf(
|
|
298
|
+
usage: { inputTokens: number; outputTokens: number; cacheReadTokens?: number; cacheWriteTokens?: number },
|
|
299
|
+
p: TokenPrice,
|
|
300
|
+
): number {
|
|
258
301
|
const read = usage.cacheReadTokens ?? 0;
|
|
259
302
|
const written = usage.cacheWriteTokens ?? 0;
|
|
260
303
|
const fresh = Math.max(0, usage.inputTokens - read - written);
|
|
261
|
-
|
|
262
|
-
|
|
304
|
+
return (
|
|
305
|
+
fresh * p.input +
|
|
306
|
+
written * (p.cacheWrite ?? p.input * CACHE_WRITE_MULTIPLIER) +
|
|
307
|
+
read * (p.cacheRead ?? p.input * CACHE_READ_MULTIPLIER) +
|
|
308
|
+
usage.outputTokens * p.output
|
|
309
|
+
);
|
|
263
310
|
}
|
|
264
311
|
|
|
265
312
|
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
266
|
-
* quarter more than fresh input, a read a tenth of it.
|
|
267
|
-
* mid-step ceiling (priceTurn) and
|
|
313
|
+
* quarter more than fresh input, a read a tenth of it. The fallback when a
|
|
314
|
+
* gateway quotes no cache price — for the mid-step ceiling (priceTurn) and
|
|
315
|
+
* the record's repricing (catalogCost) alike. */
|
|
268
316
|
export const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
269
317
|
export const CACHE_READ_MULTIPLIER = 0.1;
|
|
270
318
|
|
package/src/run-container.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
|
28
28
|
import { isPlatformPath, type ApiSpec, type Effort } from "./store.ts";
|
|
29
29
|
import { divertedSummary, isDivertedPath, overlayTestWrites } from "./test-mode.ts";
|
|
30
30
|
import type { EventExtra, ExecUsage } from "./step-exec.ts";
|
|
31
|
+
import type { TokenPrice } from "./catalog.ts";
|
|
31
32
|
import { isFileValue, fileContent } from "./secrets.ts";
|
|
32
33
|
import { safeTenantSegment, systemInstallSh, SYSTEM_FROM_INPUT_SH, INSTALL_SOCKET, type RuntimeSpec } from "./runtime.ts";
|
|
33
34
|
import { egressCaSh, type NetworkLevel } from "./network.ts";
|
|
@@ -69,7 +70,7 @@ export interface ContainerStepInput {
|
|
|
69
70
|
* step-exec.ts. Values, so the driver can enforce them in the pod. */
|
|
70
71
|
budgetUsd?: number | null;
|
|
71
72
|
budgetNote?: string;
|
|
72
|
-
price?:
|
|
73
|
+
price?: TokenPrice | null;
|
|
73
74
|
verify?: string;
|
|
74
75
|
/** `output: json` — checked inside, where the reply is. */
|
|
75
76
|
output?: "json";
|
package/src/runner.ts
CHANGED
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
listAgents,
|
|
83
83
|
runCost,
|
|
84
84
|
} from "./store.ts";
|
|
85
|
-
import { loadCatalog, checkModel, clampEffort, catalogCost, findModel, type Catalog } from "./catalog.ts";
|
|
85
|
+
import { loadCatalog, checkModel, clampEffort, catalogCost, findModel, tokenPriceOf, type Catalog, type TokenPrice } from "./catalog.ts";
|
|
86
86
|
import { resolveSecrets, getSecret, materializeSecrets, secretSites } from "./secrets.ts";
|
|
87
87
|
import { resolveNetwork, networkLine, proxyEnv, EGRESS_CA_ENV } from "./network.ts";
|
|
88
88
|
import { materializeFileSecrets, cleanupFileSecrets } from "./secret-files.ts";
|
|
@@ -2171,7 +2171,7 @@ async function runStep(
|
|
|
2171
2171
|
// step on knowledge; unknown ids, presets and offline all pass through
|
|
2172
2172
|
// to the model call, which stays the authority of last resort.
|
|
2173
2173
|
let catalog: Catalog | null = null;
|
|
2174
|
-
let tokenPrice:
|
|
2174
|
+
let tokenPrice: TokenPrice | null = null;
|
|
2175
2175
|
// The env this run's model calls actually ride: the workspace's own
|
|
2176
2176
|
// provider block when one is declared, otherwise the platform's — the
|
|
2177
2177
|
// "models included" path, where the host env points every run at the
|
|
@@ -2197,10 +2197,7 @@ async function runStep(
|
|
|
2197
2197
|
const verdict = checkModel(catalog, wireModel, { tools: needsTools });
|
|
2198
2198
|
// Also what a token costs here, so a step can count its own spend
|
|
2199
2199
|
// turn by turn against its ceiling instead of learning at the end.
|
|
2200
|
-
|
|
2201
|
-
if (priced && priced.promptPrice != null && priced.completionPrice != null) {
|
|
2202
|
-
tokenPrice = { input: priced.promptPrice, output: priced.completionPrice };
|
|
2203
|
-
}
|
|
2200
|
+
tokenPrice = tokenPriceOf(findModel(catalog, wireModel));
|
|
2204
2201
|
if (!verdict.ok) {
|
|
2205
2202
|
push("error", verdict.reason!);
|
|
2206
2203
|
step.status = "failed";
|
package/src/step-exec.ts
CHANGED
|
@@ -15,7 +15,7 @@ import path from "node:path";
|
|
|
15
15
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
16
16
|
import type { HookInput, McpServerConfig, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
17
17
|
import type { Effort } from "./store.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { costOf, type TokenPrice } from "./catalog.ts";
|
|
19
19
|
import type { TestEffect } from "./test-mode.ts";
|
|
20
20
|
import type { OperatorEvent } from "./operator.ts";
|
|
21
21
|
import { spawn } from "node:child_process";
|
|
@@ -119,7 +119,7 @@ export interface ExecOptions {
|
|
|
119
119
|
/** Price per token for the model, when the catalogue knows it, so spend
|
|
120
120
|
* can be counted turn by turn instead of learned at the end. Without it
|
|
121
121
|
* the step prices its turns from the SDK's own running total. */
|
|
122
|
-
price?:
|
|
122
|
+
price?: TokenPrice | null;
|
|
123
123
|
/** A check the step must pass to count as done: a shell command that must
|
|
124
124
|
* exit 0, or an eval-style assertion (`contains: x`, `not-contains: x`,
|
|
125
125
|
* `matches: re`, `file: path`, `judge: sentence`) — see checkVerify. */
|
|
@@ -229,29 +229,20 @@ export function knownPrice(model: string | undefined): { input: number; output:
|
|
|
229
229
|
/**
|
|
230
230
|
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
231
231
|
*
|
|
232
|
-
* Cache reads and writes are priced at
|
|
233
|
-
*
|
|
232
|
+
* Cache reads and writes are priced at the model's own cache prices when
|
|
233
|
+
* the catalogue quotes them, else at Anthropic's multipliers (costOf in
|
|
234
|
+
* catalog.ts, the same arithmetic the record is repriced with). Counting
|
|
235
|
+
* both at the full input rate — as this did —
|
|
234
236
|
* makes a long cached prompt read about ten times its price, which on a
|
|
235
237
|
* ceiling is not "the honest direction to be wrong in", it is a step
|
|
236
238
|
* killed for spending money it never spent.
|
|
237
239
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* number exists to stop a runaway step mid-turn, and wants to be close.
|
|
240
|
+
* The run record's own cost is repriced from the catalogue afterwards and
|
|
241
|
+
* stays the authority on the bill. This number exists to stop a runaway
|
|
242
|
+
* step mid-turn, and wants to be close.
|
|
242
243
|
*/
|
|
243
|
-
export function priceTurn(u: Record<string, number | undefined>, price:
|
|
244
|
-
|
|
245
|
-
const fresh = u.input_tokens ?? 0;
|
|
246
|
-
const written = u.cache_creation_input_tokens ?? 0;
|
|
247
|
-
const read = u.cache_read_input_tokens ?? 0;
|
|
248
|
-
const outTok = u.output_tokens ?? 0;
|
|
249
|
-
return (
|
|
250
|
-
fresh * p.input +
|
|
251
|
-
written * p.input * CACHE_WRITE_MULTIPLIER +
|
|
252
|
-
read * p.input * CACHE_READ_MULTIPLIER +
|
|
253
|
-
outTok * p.output
|
|
254
|
-
);
|
|
244
|
+
export function priceTurn(u: Record<string, number | undefined>, price: TokenPrice | null): number {
|
|
245
|
+
return costOf(usageFromSdk(u), price ?? FALLBACK_PRICE);
|
|
255
246
|
}
|
|
256
247
|
|
|
257
248
|
/**
|
|
@@ -633,10 +624,10 @@ async function executeStepInner(opts: ExecOptions, runQuery: QueryFn): Promise<E
|
|
|
633
624
|
// runs its turns on that model, and pricing them at the parent's put a
|
|
634
625
|
// max sub-agent under a fast parent at a fifth of its cost — against the
|
|
635
626
|
// ceiling and on the bill.
|
|
636
|
-
const turns = new Map<string, { u: Record<string, number | undefined>; price:
|
|
627
|
+
const turns = new Map<string, { u: Record<string, number | undefined>; price: TokenPrice | null }>();
|
|
637
628
|
let anonymousTurns = 0;
|
|
638
629
|
const specByName = new Map((subagents ?? []).map((s) => [s.name, s]));
|
|
639
|
-
const priceOf = (message: unknown):
|
|
630
|
+
const priceOf = (message: unknown): TokenPrice | null => {
|
|
640
631
|
const parent = (message as { parent_tool_use_id?: string | null }).parent_tool_use_id;
|
|
641
632
|
if (!parent) return opts.price ?? null;
|
|
642
633
|
const declared = specByName.get(delegations.get(parent) ?? "")?.model;
|