@agentswf/engine 0.0.0-stage → 0.1.0-beta.1
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/package.json +39 -4
- package/src/accounting/format.ts +252 -0
- package/src/accounting/plans.ts +38 -0
- package/src/accounting/prices.ts +98 -0
- package/src/accounting/summary.ts +385 -0
- package/src/accounting/tokens.ts +34 -0
- package/src/agent-launcher.ts +127 -0
- package/src/allowance-command.ts +105 -0
- package/src/attempt-ending.ts +198 -0
- package/src/attempt-output.ts +132 -0
- package/src/attempt-view.ts +362 -0
- package/src/canonical-json.ts +12 -0
- package/src/control-plane.ts +424 -0
- package/src/deadlines.ts +106 -0
- package/src/decisions/directory.ts +432 -0
- package/src/decisions/fake.ts +60 -0
- package/src/decisions/openrouter.ts +161 -0
- package/src/decisions/seam.ts +70 -0
- package/src/display-path.ts +16 -0
- package/src/duration.ts +12 -0
- package/src/errors.ts +4 -0
- package/src/files.ts +78 -0
- package/src/herdr-placement.ts +182 -0
- package/src/herdr-run-session.ts +530 -0
- package/src/herdr-workspace-marks.ts +194 -0
- package/src/here.ts +289 -0
- package/src/index.ts +5 -0
- package/src/init-command.ts +240 -0
- package/src/init-template/AGENTS.md.template +16 -0
- package/src/init-template/hello/workflow.test.ts.template +17 -0
- package/src/init-template/hello/workflow.ts.template +26 -0
- package/src/init-template/tsconfig.json.template +11 -0
- package/src/jsonl.ts +74 -0
- package/src/logs-command.ts +605 -0
- package/src/machine.ts +27 -0
- package/src/message-operation.ts +245 -0
- package/src/messaging/limits.ts +35 -0
- package/src/messaging/messaging.ts +1303 -0
- package/src/messaging/routes.ts +79 -0
- package/src/messaging/text.ts +251 -0
- package/src/operation-events.ts +182 -0
- package/src/operation-liveness.ts +674 -0
- package/src/operator-aliases.ts +10 -0
- package/src/operator-cli.ts +600 -0
- package/src/operator-runtime.ts +503 -0
- package/src/pane-layout.ts +125 -0
- package/src/progress-view.ts +568 -0
- package/src/result-slots.ts +325 -0
- package/src/result-validation.ts +47 -0
- package/src/run-command.ts +242 -0
- package/src/run-commands.ts +215 -0
- package/src/run-event-log.ts +72 -0
- package/src/run-prepare.ts +178 -0
- package/src/run-progress.ts +137 -0
- package/src/run-read.ts +425 -0
- package/src/run-state.ts +120 -0
- package/src/run-usage.ts +393 -0
- package/src/run-view.ts +225 -0
- package/src/runs-command.ts +356 -0
- package/src/runs.ts +645 -0
- package/src/sandbox-homes.ts +211 -0
- package/src/sandboxes.ts +521 -0
- package/src/skills/fetch.ts +305 -0
- package/src/skills/run-skills.ts +181 -0
- package/src/skills/sources.ts +87 -0
- package/src/skills/tree.ts +60 -0
- package/src/stage-ledger.ts +535 -0
- package/src/stage-plan.ts +127 -0
- package/src/stage-schema.ts +88 -0
- package/src/stopped.ts +36 -0
- package/src/test-command.ts +110 -0
- package/src/testing/operation-events.ts +89 -0
- package/src/testing.ts +164 -0
- package/src/workflow-loader.ts +113 -0
- package/src/workflow-runner.ts +3666 -0
- package/src/workflow-test-preload.ts +13 -0
- package/src/workflow-testing/decisions.ts +129 -0
- package/src/workflow-testing/host.ts +811 -0
- package/src/workflow-testing/index.ts +420 -0
- package/src/workflow-testing/script.ts +447 -0
- package/src/workflow-testing/workflow-testing.typecheck.ts +47 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentswf/engine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "agents.wf: the engine that runs workflows made of coding agents",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/romanlv/agentswf",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/romanlv/agentswf.git",
|
|
10
|
+
"directory": "packages/engine"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"engines": {
|
|
14
|
+
"bun": ">=1.4.0"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"awf": "./src/operator-cli.ts"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/index.ts",
|
|
21
|
+
"./testing": "./src/testing.ts",
|
|
22
|
+
"./workflow-testing": "./src/workflow-testing/index.ts",
|
|
23
|
+
"./cli": "./src/operator-cli.ts"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"!src/**/*.test.ts",
|
|
28
|
+
"!src/**/*.eval.ts",
|
|
29
|
+
"!src/**/fixtures"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@agentswf/wf": "0.1.0-beta.0",
|
|
33
|
+
"@agentswf/contract": "0.1.0-beta.0",
|
|
34
|
+
"@agentswf/harness": "0.1.0-beta.0",
|
|
35
|
+
"@agentswf/sandbox": "0.1.0-beta.0",
|
|
36
|
+
"typebox": "^1.3.34"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountingFigures,
|
|
3
|
+
AttemptAccounting,
|
|
4
|
+
DecisionFigures,
|
|
5
|
+
} from "@agentswf/contract/records";
|
|
6
|
+
import { PUBLISHED_PRICES } from "./prices";
|
|
7
|
+
|
|
8
|
+
type Slice = AttemptAccounting["byStage"][number];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What an attempt cost: a line of its figures, its decisions on a line of their own when it asked
|
|
12
|
+
* any, then a row per stage or group when there is more than one. `stages: false` leaves a
|
|
13
|
+
* workflow's stage rows to a view that shows them beside its stages; a run without stages keeps its
|
|
14
|
+
* groups. A stage that opened no agent and asked nothing has no row, as it has no figures beside
|
|
15
|
+
* it in that view. Gaps are named, never shown as zero; what is complete is left to the record.
|
|
16
|
+
*/
|
|
17
|
+
export function describeAccounting(
|
|
18
|
+
accounting: AttemptAccounting,
|
|
19
|
+
options: { stages?: boolean } = {},
|
|
20
|
+
): string[] {
|
|
21
|
+
const staged = accounting.grouping === "stages";
|
|
22
|
+
const rows =
|
|
23
|
+
accounting.byStage.length > 1 && (!staged || options.stages !== false)
|
|
24
|
+
? accounting.byStage.filter((row) => !staged || row.agents > 0 || row.decisions)
|
|
25
|
+
: [];
|
|
26
|
+
const width = Math.max(0, ...rows.map(({ stage }) => stage.length));
|
|
27
|
+
return [
|
|
28
|
+
describeTotals(accounting),
|
|
29
|
+
...describeDecisionLine(accounting),
|
|
30
|
+
...rows.map((row) => sliceRow(row, width)),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The agents' count, the time, then their tokens, estimate, billing and gaps. A run that opened no
|
|
36
|
+
* agent is its time and what went unpriced; one that asked decisions says it opened no agent, so
|
|
37
|
+
* they are not taken for agents.
|
|
38
|
+
*/
|
|
39
|
+
function describeTotals(accounting: AttemptAccounting): string {
|
|
40
|
+
const { totals } = accounting;
|
|
41
|
+
const basis = shortBasis(accounting.basis);
|
|
42
|
+
const took = duration(accounting.wallMs);
|
|
43
|
+
const unpriced =
|
|
44
|
+
accounting.unpriced.length === 0 ? [] : [`unpriced: ${accounting.unpriced.join(", ")}`];
|
|
45
|
+
if (totals.agents === 0) {
|
|
46
|
+
return [...(totals.decisions ? [plural(0, "agent")] : []), took, ...unpriced].join(" · ");
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
plural(totals.agents, "agent"),
|
|
50
|
+
took,
|
|
51
|
+
tokensOf(totals),
|
|
52
|
+
...(totals.estimate === undefined ? [] : [`${estimate(totals.estimate)} at ${basis}`]),
|
|
53
|
+
...(totals.charged === undefined ? [] : [`${usd(totals.charged)} charged`]),
|
|
54
|
+
accounting.billing,
|
|
55
|
+
...(totals.billed < totals.agents ? [`billing known ${totals.billed}/${totals.agents}`] : []),
|
|
56
|
+
...usageGap(totals),
|
|
57
|
+
...gaps(totals),
|
|
58
|
+
...unpriced,
|
|
59
|
+
].join(" · ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A run of several attempts, in brief: how many, those whose cost is unknown, by why, how long in
|
|
64
|
+
* all, and the estimate when any agent ran. `unknown` counts the attempts with no cost recorded:
|
|
65
|
+
* interrupted, or ended without one, as an older record may be.
|
|
66
|
+
*/
|
|
67
|
+
export function describeAttempts(
|
|
68
|
+
sum: AttemptAccounting,
|
|
69
|
+
attempts: number,
|
|
70
|
+
unknown: { interrupted: number; ended: number },
|
|
71
|
+
): string {
|
|
72
|
+
const why = [
|
|
73
|
+
...(unknown.interrupted > 0 ? [`${unknown.interrupted} interrupted`] : []),
|
|
74
|
+
...(unknown.ended > 0 ? [`${unknown.ended} ended without a cost record`] : []),
|
|
75
|
+
];
|
|
76
|
+
const gap = why.length > 0 ? ` (${why.join(", ")}, cost unknown)` : "";
|
|
77
|
+
const priced =
|
|
78
|
+
sum.totals.agents > 0 && sum.totals.estimate !== undefined
|
|
79
|
+
? [estimate(sum.totals.estimate)]
|
|
80
|
+
: [];
|
|
81
|
+
return `run: ${[`${attempts} attempts${gap}`, duration(sum.wallMs), ...priced].join(", ")}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function describeDecisionLine(accounting: AttemptAccounting): string[] {
|
|
85
|
+
const { decisions } = accounting.totals;
|
|
86
|
+
return decisions ? [describeDecisions(decisions, shortBasis(accounting.basis))] : [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A workflow's stages, each to what its agents and decisions cost, for the view to show beside
|
|
91
|
+
* them; a stage that opened none and asked nothing is left out, not shown at zero.
|
|
92
|
+
*/
|
|
93
|
+
export function stageFigures(accounting: AttemptAccounting): Map<string, string> {
|
|
94
|
+
if (accounting.grouping !== "stages") return new Map();
|
|
95
|
+
return new Map(
|
|
96
|
+
accounting.byStage.flatMap((stage) => {
|
|
97
|
+
const figures = [
|
|
98
|
+
...(stage.agents === 0
|
|
99
|
+
? []
|
|
100
|
+
: [
|
|
101
|
+
plural(stage.agents, "agent"),
|
|
102
|
+
tokensOf(stage),
|
|
103
|
+
...priced(stage),
|
|
104
|
+
...agentGaps(stage),
|
|
105
|
+
]),
|
|
106
|
+
...(stage.decisions ? [plural(stage.decisions.calls, "decision")] : []),
|
|
107
|
+
...decisionCost(stage),
|
|
108
|
+
];
|
|
109
|
+
return figures.length === 0 ? [] : [[stage.stage, figures.join(" · ")] as const];
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A table's row: the agents and decisions counted together, `0 agents` for a slice with neither,
|
|
116
|
+
* then its time, and the estimate alone where there is one, as the row has room for.
|
|
117
|
+
*/
|
|
118
|
+
function sliceRow(stage: Slice, width: number): string {
|
|
119
|
+
const counted = [
|
|
120
|
+
...(stage.agents === 0 && stage.decisions ? [] : [plural(stage.agents, "agent")]),
|
|
121
|
+
...(stage.decisions ? [plural(stage.decisions.calls, "decision")] : []),
|
|
122
|
+
].join(", ");
|
|
123
|
+
const agents =
|
|
124
|
+
stage.agents === 0
|
|
125
|
+
? []
|
|
126
|
+
: [
|
|
127
|
+
...(stage.estimate === undefined ? [tokensOf(stage)] : priced(stage)),
|
|
128
|
+
...agentGaps(stage),
|
|
129
|
+
];
|
|
130
|
+
return [
|
|
131
|
+
` ${stage.stage.padEnd(width)} ${counted}`,
|
|
132
|
+
duration(stage.spanMs),
|
|
133
|
+
...agents,
|
|
134
|
+
...decisionCost(stage),
|
|
135
|
+
].join(" · ");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function tokensOf(figures: AccountingFigures): string {
|
|
139
|
+
return figures.known === 0 ? "no usage known" : plural(total(figures), "token", count);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function priced(stage: Slice): string[] {
|
|
143
|
+
return stage.estimate === undefined ? [] : [estimate(stage.estimate)];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function agentGaps(figures: AccountingFigures): string[] {
|
|
147
|
+
return [...usageGap(figures), ...gaps(figures)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function usageGap(figures: AccountingFigures): string[] {
|
|
151
|
+
return figures.known < figures.agents && figures.known > 0
|
|
152
|
+
? [`usage known ${figures.known}/${figures.agents}`]
|
|
153
|
+
: [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A slice's decisions past their count: their estimate, apart from its agents', and gaps. */
|
|
157
|
+
function decisionCost(stage: Slice): string[] {
|
|
158
|
+
const { decisions } = stage;
|
|
159
|
+
if (!decisions) return [];
|
|
160
|
+
const cost =
|
|
161
|
+
decisions.estimate === undefined
|
|
162
|
+
? decisions.known === 0
|
|
163
|
+
? ["no usage known"]
|
|
164
|
+
: []
|
|
165
|
+
: [
|
|
166
|
+
stage.agents === 0
|
|
167
|
+
? estimate(decisions.estimate)
|
|
168
|
+
: `${estimate(decisions.estimate)} in decisions`,
|
|
169
|
+
];
|
|
170
|
+
return [...cost, ...decisionGaps(decisions)];
|
|
171
|
+
}
|
|
172
|
+
/** The published table by its kind alone, its date being in the record; any other table in full. */
|
|
173
|
+
function shortBasis(basis: string): string {
|
|
174
|
+
return basis === PUBLISHED_PRICES.basis ? "list prices" : basis;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Decisions are not agents: their calls, tokens and cost stay on a line of their own. */
|
|
178
|
+
function describeDecisions(decisions: DecisionFigures, basis: string): string {
|
|
179
|
+
const tokens = decisions.tokens.input + decisions.tokens.output;
|
|
180
|
+
return [
|
|
181
|
+
` ${plural(decisions.calls, "decision")}`,
|
|
182
|
+
...(decisions.attempts > decisions.calls ? [plural(decisions.attempts, "request")] : []),
|
|
183
|
+
decisions.known === 0 ? "no usage known" : plural(tokens, "token", count),
|
|
184
|
+
...(decisions.estimate === undefined ? [] : [`${estimate(decisions.estimate)} at ${basis}`]),
|
|
185
|
+
...(decisions.charged === undefined ? [] : [`${usd(decisions.charged)} charged`]),
|
|
186
|
+
...decisionGaps(decisions),
|
|
187
|
+
].join(" · ");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** As for agents: unknown usage first, then usage known but not all priced. */
|
|
191
|
+
function decisionGaps(decisions: DecisionFigures): string[] {
|
|
192
|
+
return [
|
|
193
|
+
...(decisions.known < decisions.calls
|
|
194
|
+
? [`usage known ${decisions.known}/${decisions.calls}`]
|
|
195
|
+
: []),
|
|
196
|
+
...(decisions.priced < decisions.known
|
|
197
|
+
? [`priced ${decisions.priced}/${decisions.calls}`]
|
|
198
|
+
: []),
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function total({ tokens }: AccountingFigures): number {
|
|
203
|
+
return tokens.input + tokens.cacheRead + tokens.cacheWrite + tokens.output;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** An agent whose usage is known but not all priced adds only its priced part to an estimate. */
|
|
207
|
+
function gaps(figures: AccountingFigures): string[] {
|
|
208
|
+
return figures.priced < figures.known ? [`fully priced ${figures.priced}/${figures.agents}`] : [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function plural(amount: number, noun: string, show: (amount: number) => string = String): string {
|
|
212
|
+
return `${show(amount)} ${noun}${amount === 1 ? "" : "s"}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** A span as a person reads it: `42s`, `3m 05s`, `1h 02m`. */
|
|
216
|
+
export function duration(milliseconds: number): string {
|
|
217
|
+
const seconds = Math.max(0, Math.round(milliseconds / 1000));
|
|
218
|
+
if (seconds < 60) return `${seconds}s`;
|
|
219
|
+
const minutes = Math.floor(seconds / 60);
|
|
220
|
+
if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
221
|
+
return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, "0")}m`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** How long ago, for a person: `just now`, `5m ago`, `3h ago`, `2d ago`. */
|
|
225
|
+
export function ago(ms: number): string {
|
|
226
|
+
if (!Number.isFinite(ms)) return "at an unknown time";
|
|
227
|
+
if (ms < 60_000) return "just now";
|
|
228
|
+
const minutes = Math.round(ms / 60_000);
|
|
229
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
230
|
+
const hours = Math.round(minutes / 60);
|
|
231
|
+
return hours < 48 ? `${hours}h ago` : `${Math.round(hours / 24)}d ago`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function count(tokens: number): string {
|
|
235
|
+
if (tokens >= 999_500) return `${(tokens / 1_000_000).toFixed(2)}M`;
|
|
236
|
+
if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`;
|
|
237
|
+
return String(tokens);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function usd(amount: number): string {
|
|
241
|
+
return belowCent(amount) ? "<$0.01" : `$${amount.toFixed(2)}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Marked approximate, unless `<` already says so. */
|
|
245
|
+
function estimate(amount: number): string {
|
|
246
|
+
return belowCent(amount) ? usd(amount) : `~${usd(amount)}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A real cost too small to show in cents is not shown as nothing. */
|
|
250
|
+
function belowCent(amount: number): boolean {
|
|
251
|
+
return amount > 0 && amount < 0.005;
|
|
252
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each plan costs a month, by the name its harness reports, as the vendors listed it on the
|
|
3
|
+
* date of `PLAN_BASIS`. Policy, not observation: a record keeps the plan's name, and a price is
|
|
4
|
+
* looked up when it is shown. A plan not listed, or listed without a price, shows none.
|
|
5
|
+
*/
|
|
6
|
+
export const PLAN_BASIS = "plans 2026-10-05";
|
|
7
|
+
|
|
8
|
+
type Listed = { name: string; usdPerMonth?: number };
|
|
9
|
+
|
|
10
|
+
const PLANS: Readonly<Record<string, (plan: string, tier?: string) => Listed | undefined>> = {
|
|
11
|
+
// `claude auth status` says `pro` or `max`; Max's tier is in its account: 5x is $100, 20x $200.
|
|
12
|
+
claude: (plan, tier) => {
|
|
13
|
+
if (plan === "pro") return { name: "Pro", usdPerMonth: 20 };
|
|
14
|
+
if (plan !== "max") return undefined;
|
|
15
|
+
if (tier?.endsWith("_max_20x")) return { name: "Max 20x", usdPerMonth: 200 };
|
|
16
|
+
if (tier?.endsWith("_max_5x")) return { name: "Max 5x", usdPerMonth: 100 };
|
|
17
|
+
return { name: "Max" };
|
|
18
|
+
},
|
|
19
|
+
// OpenAI's own product names for its plan types: `prolite` is Pro 100, `promax` Pro 500.
|
|
20
|
+
codex: (plan) =>
|
|
21
|
+
(
|
|
22
|
+
({
|
|
23
|
+
free: { name: "Free", usdPerMonth: 0 },
|
|
24
|
+
go: { name: "Go", usdPerMonth: 8 },
|
|
25
|
+
plus: { name: "Plus", usdPerMonth: 20 },
|
|
26
|
+
prolite: { name: "Pro 100", usdPerMonth: 100 },
|
|
27
|
+
pro: { name: "Pro 200", usdPerMonth: 200 },
|
|
28
|
+
promax: { name: "Pro 500", usdPerMonth: 500 },
|
|
29
|
+
}) as Record<string, Listed>
|
|
30
|
+
)[plan],
|
|
31
|
+
// A team seat is Standard or Premium, $40 or $120, and `/usage` does not say which.
|
|
32
|
+
cursor: (plan) => (plan === "Team" ? { name: "Team" } : undefined),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The plan as people name it and its monthly price, where `PLAN_BASIS` lists them. */
|
|
36
|
+
export function planPrice(harness: string, plan: string, tier?: string): Listed | undefined {
|
|
37
|
+
return Object.hasOwn(PLANS, harness) ? PLANS[harness]!(plan, tier) : undefined;
|
|
38
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { TokenUsage } from "@agentswf/contract/records";
|
|
2
|
+
|
|
3
|
+
/** USD per million tokens, for every class a request can be billed in. */
|
|
4
|
+
export type ModelRate = {
|
|
5
|
+
input: number;
|
|
6
|
+
output: number;
|
|
7
|
+
cacheRead: number;
|
|
8
|
+
cacheWrite5m: number;
|
|
9
|
+
cacheWrite1h: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type PriceTable = {
|
|
13
|
+
/** Names the table a figure came from; figures from two bases are never compared. */
|
|
14
|
+
basis: string;
|
|
15
|
+
rate(model: string): ModelRate | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Anthropic prices cache writes at 1.25x input for five minutes and 2x for an hour. */
|
|
19
|
+
function anthropic(input: number, output: number, read = 0.1): ModelRate {
|
|
20
|
+
return {
|
|
21
|
+
input,
|
|
22
|
+
output,
|
|
23
|
+
cacheRead: input * read,
|
|
24
|
+
cacheWrite5m: input * 1.25,
|
|
25
|
+
cacheWrite1h: input * 2,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** OpenAI lists cached input and, for its newer models, a cache-write rate of 1.25x input. */
|
|
30
|
+
function openai(input: number, cached: number, output: number, write = input * 1.25): ModelRate {
|
|
31
|
+
return { input, output, cacheRead: cached, cacheWrite5m: write, cacheWrite1h: write };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* List prices, read on 2026-09-23 (Claude Sonnet 5.5 on 2026-09-29, GPT-6.1 Sol on 2026-10-01, Claude Haiku 5.5 on 2026-10-07) from https://platform.claude.com/docs/en/about-claude/pricing
|
|
36
|
+
* and https://developers.openai.com/api/docs/pricing, and on 2026-09-26 from OpenRouter for Jev. A subscription is not charged per token,
|
|
37
|
+
* but its allowance is drawn down roughly in proportion to these, so they price any run the same
|
|
38
|
+
* way whoever pays for it.
|
|
39
|
+
*
|
|
40
|
+
* Not modelled: OpenAI's long-context rates (2x input and 1.5x output, on gpt-5.6 and gpt-6 past
|
|
41
|
+
* a threshold, and on gpt-5.5 past 272K), and Claude Haiku 5.5's 5x rate on prompts past 100K, so
|
|
42
|
+
* a very long session on either is under-estimated.
|
|
43
|
+
*/
|
|
44
|
+
const RATES: Record<string, ModelRate> = {
|
|
45
|
+
"claude-fable-5-1": anthropic(10, 50, 0.025),
|
|
46
|
+
"claude-fable-5": anthropic(10, 50),
|
|
47
|
+
"claude-opus-5-5": anthropic(4, 20, 0.05),
|
|
48
|
+
"claude-opus-5": anthropic(5, 25),
|
|
49
|
+
"claude-opus-4-8": anthropic(5, 25),
|
|
50
|
+
"claude-opus-4-7": anthropic(5, 25),
|
|
51
|
+
"claude-opus-4-6": anthropic(5, 25),
|
|
52
|
+
"claude-opus-4-5": anthropic(5, 25),
|
|
53
|
+
"claude-sonnet-5-5": anthropic(2, 10, 0.05),
|
|
54
|
+
"claude-sonnet-5": anthropic(2, 10),
|
|
55
|
+
"claude-sonnet-4-6": anthropic(3, 15),
|
|
56
|
+
"claude-sonnet-4-5": anthropic(3, 15),
|
|
57
|
+
"claude-haiku-5-5": anthropic(0.1, 0.5),
|
|
58
|
+
"claude-haiku-4-5": anthropic(1, 5),
|
|
59
|
+
"gpt-6-astra": openai(10, 1, 50),
|
|
60
|
+
"gpt-6.1-sol": openai(2, 0.1, 10),
|
|
61
|
+
"gpt-6-sol": openai(2, 0.2, 10),
|
|
62
|
+
"gpt-6-luna": openai(0.1, 0.01, 0.5),
|
|
63
|
+
"gpt-5.6-sol": openai(4, 0.4, 20),
|
|
64
|
+
"gpt-5.6-terra": openai(2, 0.2, 12),
|
|
65
|
+
"gpt-5.6-luna": openai(0.2, 0.02, 1.2),
|
|
66
|
+
// No cache-write charge is listed for these.
|
|
67
|
+
"gpt-5.5": openai(5, 0.5, 30, 5),
|
|
68
|
+
"gpt-5.4": openai(2.5, 0.25, 15, 2.5),
|
|
69
|
+
// OpenRouter's listing, read on 2026-09-26: input only; output is reported and not charged.
|
|
70
|
+
"typesafe/jev-1.13": { input: 0.042, output: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0 },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A model is priced only as itself: its family id, the id with a release date after it, or either
|
|
75
|
+
* with a context tag such as `[1m]`. Anything else, even a close sibling such as `claude-opus-5-6`
|
|
76
|
+
* or `gpt-5.5-pro`, is unpriced rather than priced at a rate that is not its own.
|
|
77
|
+
*/
|
|
78
|
+
function lookup(model: string): ModelRate | undefined {
|
|
79
|
+
const own = (id: string) => (Object.hasOwn(RATES, id) ? RATES[id] : undefined);
|
|
80
|
+
const bare = model.replace(/\[[^\]]*\]$/, "");
|
|
81
|
+
const dated = /^(.+?)-(\d{8}|\d{4}-\d{2}-\d{2})$/.exec(bare);
|
|
82
|
+
return own(bare) ?? (dated ? own(dated[1]!) : undefined);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const PUBLISHED_PRICES: PriceTable = { basis: "list prices 2026-10-07", rate: lookup };
|
|
86
|
+
|
|
87
|
+
/** `cacheWrite1h` is the part of `cacheWrite` with an hour's lifetime; the rest is five minutes. */
|
|
88
|
+
export function costOf(tokens: TokenUsage, rate: ModelRate): number {
|
|
89
|
+
const hour = Math.min(tokens.cacheWrite, tokens.cacheWrite1h ?? 0);
|
|
90
|
+
return (
|
|
91
|
+
(tokens.input * rate.input +
|
|
92
|
+
tokens.cacheRead * rate.cacheRead +
|
|
93
|
+
(tokens.cacheWrite - hour) * rate.cacheWrite5m +
|
|
94
|
+
hour * rate.cacheWrite1h +
|
|
95
|
+
tokens.output * rate.output) /
|
|
96
|
+
1_000_000
|
|
97
|
+
);
|
|
98
|
+
}
|