@basein/runner 0.3.24 → 0.3.26
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/README.md +27 -26
- package/dist/auth/client.d.ts +4 -1
- package/dist/auth/client.js +83 -14
- package/dist/bin/bir-hooks.js +4 -2
- package/dist/bin/bir.js +153 -37
- package/dist/bin/investigate.js +5 -0
- package/dist/bin/scenario-edit.d.ts +5 -0
- package/dist/bin/scenario-edit.js +17 -1
- package/dist/config/adapters/claude-code.d.ts +51 -7
- package/dist/config/adapters/claude-code.js +105 -18
- package/dist/config/generate.d.ts +61 -13
- package/dist/config/generate.js +108 -20
- package/dist/config/resolve.d.ts +5 -0
- package/dist/config/resolve.js +7 -0
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +28 -0
- package/dist/control/daemon.js +62 -0
- package/dist/control/paths.d.ts +19 -1
- package/dist/control/paths.js +20 -2
- package/dist/control/pin-guard.js +8 -2
- package/dist/control/recorder-main.js +17 -5
- package/dist/control/server.d.ts +63 -9
- package/dist/control/server.js +350 -62
- package/dist/control/transcript.d.ts +64 -3
- package/dist/control/transcript.js +318 -37
- package/dist/jsonrpc/framing.d.ts +18 -3
- package/dist/jsonrpc/framing.js +57 -4
- package/dist/proxy/intercept.d.ts +4 -0
- package/dist/proxy/intercept.js +4 -0
- package/dist/proxy/relay.d.ts +13 -0
- package/dist/proxy/relay.js +12 -1
- package/dist/proxy/session.d.ts +8 -1
- package/dist/proxy/session.js +67 -10
- package/dist/record/recorder.d.ts +6 -1
- package/dist/record/redact.d.ts +18 -4
- package/dist/record/redact.js +538 -79
- package/dist/record/remote-recorder.d.ts +26 -5
- package/dist/record/remote-recorder.js +174 -18
- package/dist/record/truncate.js +3 -1
- package/dist/replay/bash-channel.d.ts +7 -1
- package/dist/replay/bash-channel.js +9 -3
- package/dist/replay/builtins.d.ts +14 -0
- package/dist/replay/builtins.js +460 -95
- package/dist/replay/bundle.d.ts +80 -0
- package/dist/replay/bundle.js +455 -24
- package/dist/replay/controller.d.ts +64 -1
- package/dist/replay/controller.js +238 -38
- package/dist/replay/derive.js +46 -4
- package/dist/replay/handover.d.ts +13 -0
- package/dist/replay/handover.js +18 -0
- package/dist/replay/plan.d.ts +15 -0
- package/dist/replay/plan.js +48 -30
- package/dist/replay/pricing.d.ts +61 -11
- package/dist/replay/pricing.js +135 -42
- package/dist/replay/safety.js +43 -5
- package/dist/upstream/http-client.d.ts +6 -3
- package/dist/upstream/http-client.js +54 -13
- package/dist/upstream/stdio-client.d.ts +7 -0
- package/dist/upstream/stdio-client.js +1 -1
- package/dist/util/atomic-write.d.ts +9 -1
- package/dist/util/atomic-write.js +9 -6
- package/dist/util/service-health.d.ts +7 -0
- package/dist/util/service-health.js +10 -3
- package/docs/calculatedReplay.md +32 -0
- package/package.json +1 -1
- package/production.md +2 -2
- package/scripts/install.ps1 +14 -2
package/dist/replay/handover.js
CHANGED
|
@@ -129,6 +129,24 @@ export function buildHandoverNote(n) {
|
|
|
129
129
|
}
|
|
130
130
|
return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* The note for a plan that ran every one of its steps while its caller was
|
|
134
|
+
* gone (Esc on `run_scenario`, or the client's timeout), so that the results
|
|
135
|
+
* reached nobody. Nothing stopped, so no hand-over note exists; without this
|
|
136
|
+
* one the model was never told the steps ran, and the next request of the same
|
|
137
|
+
* task ran them again — a row added twice, a mail sent twice.
|
|
138
|
+
*/
|
|
139
|
+
export function buildUnseenResultNote(n) {
|
|
140
|
+
const intent = n.intent.length > 240 ? `${n.intent.slice(0, 240)}…` : n.intent;
|
|
141
|
+
const lines = [
|
|
142
|
+
"[BaseInstRunner calculated replay] The calculated scenario for the earlier request ran while its result",
|
|
143
|
+
"was not being waited for, so nobody has seen what it did.",
|
|
144
|
+
];
|
|
145
|
+
if (intent)
|
|
146
|
+
lines.push(`Intent: ${intent}`);
|
|
147
|
+
lines.push(`${n.executed === n.totalSteps ? `All ${n.totalSteps}` : `${n.executed} of ${n.totalSteps}`} steps of the scenario ran, and their side effects are in place. Do not run them again.`, "If you need what they returned, look at the state they changed, or tell the user what ran and ask.");
|
|
148
|
+
return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
|
|
149
|
+
}
|
|
132
150
|
/** "Step 2", "Steps 1–3", "Steps 1, 3 and 5–6": 1-based numbers in order, runs joined. */
|
|
133
151
|
export function stepsPhrase(numbers) {
|
|
134
152
|
if (numbers.length === 0)
|
package/dist/replay/plan.d.ts
CHANGED
|
@@ -125,6 +125,14 @@ export interface DriveOptions {
|
|
|
125
125
|
* they always were (bundle.ts, `AssembleOptions.reportTool`).
|
|
126
126
|
*/
|
|
127
127
|
reportTool?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Lay a run that went entirely well out briefly, with the plan's summary in
|
|
130
|
+
* it (bundle.ts, `AssembleOptions.brief`), and say so in the result's
|
|
131
|
+
* `brief`. For a caller whose agent answers from the bundle alone —
|
|
132
|
+
* `bir-scenario`'s run of a whole plan — not for an operator's `bir replay`,
|
|
133
|
+
* which reads every result. A run that went wrong is laid out in full.
|
|
134
|
+
*/
|
|
135
|
+
brief?: boolean;
|
|
128
136
|
}
|
|
129
137
|
/**
|
|
130
138
|
* Supplies a step's recorded output when its tool cannot run here (§8.1).
|
|
@@ -213,6 +221,13 @@ export interface ComposeResult {
|
|
|
213
221
|
* the end of the planned steps.
|
|
214
222
|
*/
|
|
215
223
|
endedAt: number;
|
|
224
|
+
/**
|
|
225
|
+
* The bundle is brief ({@link DriveOptions.brief}), and the plan's summary,
|
|
226
|
+
* when it has one, is in `text`: the caller must not deliver it again.
|
|
227
|
+
*/
|
|
228
|
+
brief?: boolean;
|
|
229
|
+
/** Building the summary for a brief bundle threw: why. The bundle went without it. */
|
|
230
|
+
summaryError?: string;
|
|
216
231
|
}
|
|
217
232
|
/** Where, and why, a run stopped short of the chain's end (fallbk.md). */
|
|
218
233
|
export interface PlanStop {
|
package/dist/replay/plan.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* output, and skipping.
|
|
22
22
|
*/
|
|
23
23
|
import { evalParamsLogic, evalResponseParamsLogic, evalToolInputLogic, evalToolOutputLogic, evalWhenLogic, LogicTimeoutError, } from "./logic.js";
|
|
24
|
-
import { assembleBundle, bundleInput, MAX_REPLAY_REASON, testWords } from "./bundle.js";
|
|
24
|
+
import { assembleBundle, bundleInput, MAX_REPLAY_REASON, testWords, wentWell, } from "./bundle.js";
|
|
25
25
|
import { redact, redactText } from "../record/redact.js";
|
|
26
26
|
import { sinkOfPlan, unsafeParameter } from "./safety.js";
|
|
27
27
|
import { testedOnlyTargets } from "./targets.js";
|
|
@@ -883,6 +883,29 @@ export class ScenarioReplayPlan {
|
|
|
883
883
|
// The intent is a paragraph the analyser wrote; it is context, not an
|
|
884
884
|
// instruction, so it is clipped rather than dumped whole into the prompt.
|
|
885
885
|
const intent = this.intent.length > 240 ? `${this.intent.slice(0, 240)}…` : this.intent;
|
|
886
|
+
if (this.mode === "direct") {
|
|
887
|
+
// The steps are not listed. The model does not call them, and what the
|
|
888
|
+
// tool returns names every call and says whether the run was whole or
|
|
889
|
+
// stopped short — what a list here once told it ahead of time. The list
|
|
890
|
+
// cost the agent up to 2,000 characters in the first call of every turn
|
|
891
|
+
// a plan was armed for (docs/calculatedReplay.md, "How a bundle reads"),
|
|
892
|
+
// and the tool's result shows an operator watching the session what ran.
|
|
893
|
+
return [
|
|
894
|
+
"[BaseInstRunner calculated replay] A known-good tool sequence was recorded",
|
|
895
|
+
`for this request${intent ? ` (intent: ${intent})` : ""}.`,
|
|
896
|
+
"",
|
|
897
|
+
`Call ${directToolName} once, with no arguments, before any other tool. It runs the`,
|
|
898
|
+
"sequence and returns what each call did.",
|
|
899
|
+
subTask
|
|
900
|
+
? "Then continue your task from the results it returns, calling tools as needed."
|
|
901
|
+
: this.stopsEarly()
|
|
902
|
+
? "Then continue the user's request from the results it returns."
|
|
903
|
+
: "Then answer the user's request from the results it returns.",
|
|
904
|
+
// A parked step (fallbk.md D3): the model is told up front that the
|
|
905
|
+
// plan is partial, so the hand-over that follows is expected.
|
|
906
|
+
...(this.stopsEarly() ? ["", "The system will then hand the rest of the task back to you."] : []),
|
|
907
|
+
].join("\n");
|
|
908
|
+
}
|
|
886
909
|
// Each step's reasoning is another session's narration ("I'll now drop the
|
|
887
910
|
// temp table…"), injected into this one. It is a label here, not a
|
|
888
911
|
// paragraph: its first line, clipped. A chain long enough that even the
|
|
@@ -931,30 +954,6 @@ export class ScenarioReplayPlan {
|
|
|
931
954
|
const tail = this.stopsEarly()
|
|
932
955
|
? ["", "The system will then hand the rest of the task back to you."]
|
|
933
956
|
: [];
|
|
934
|
-
if (this.mode === "direct") {
|
|
935
|
-
// The sequence is listed even though the model does not call it itself:
|
|
936
|
-
// it is about to receive these results, and knowing what was run is what
|
|
937
|
-
// lets it tell a complete answer from a partial one. It is also the only
|
|
938
|
-
// place an operator watching the session can see what a replay is doing.
|
|
939
|
-
return [
|
|
940
|
-
"[BaseInstRunner calculated replay] A known-good tool sequence was recorded",
|
|
941
|
-
`for this request${intent ? ` (intent: ${intent})` : ""}.`,
|
|
942
|
-
"",
|
|
943
|
-
anyTest
|
|
944
|
-
? 'It runs these tools, in order, and returns their results. A step marked "only when" runs only when this request needs it, and the results say which ones did not:'
|
|
945
|
-
: "It runs these tools, in order, and returns their results:",
|
|
946
|
-
"",
|
|
947
|
-
...lines,
|
|
948
|
-
"",
|
|
949
|
-
`Call ${directToolName} once, with no arguments, before any other tool.`,
|
|
950
|
-
subTask
|
|
951
|
-
? "Then continue your task from the results it returns, calling tools as needed."
|
|
952
|
-
: this.stopsEarly()
|
|
953
|
-
? "Then continue the user's request from the results it returns."
|
|
954
|
-
: "Then answer the user's request from the results it returns.",
|
|
955
|
-
...tail,
|
|
956
|
-
].join("\n");
|
|
957
|
-
}
|
|
958
957
|
return [
|
|
959
958
|
"[BaseInstRunner calculated replay] A known-good tool sequence was previously",
|
|
960
959
|
"recorded for this request. Call exactly these tools, in order, one at a time:",
|
|
@@ -1343,12 +1342,29 @@ export class ScenarioReplayPlan {
|
|
|
1343
1342
|
position: this.stopAt,
|
|
1344
1343
|
};
|
|
1345
1344
|
}
|
|
1345
|
+
const layout = {
|
|
1346
|
+
handover: stopped !== undefined,
|
|
1347
|
+
subTask: this.subTask,
|
|
1348
|
+
reportTool: opts.reportTool,
|
|
1349
|
+
};
|
|
1350
|
+
// Brief only for a run that went entirely well (bundle.ts, `wentWell`). A
|
|
1351
|
+
// tool error that is not an MCP `isError` — a JSON-RPC error, which the
|
|
1352
|
+
// bundle cannot tell from a result — is counted in `errored`, and is just
|
|
1353
|
+
// as much a reason for the agent to see everything.
|
|
1354
|
+
const brief = opts.brief === true && errored === 0 && wentWell(entries, layout);
|
|
1355
|
+
let summaryError;
|
|
1356
|
+
if (brief) {
|
|
1357
|
+
let summary;
|
|
1358
|
+
try {
|
|
1359
|
+
summary = this.responseModel();
|
|
1360
|
+
}
|
|
1361
|
+
catch (err) {
|
|
1362
|
+
summaryError = errText(err);
|
|
1363
|
+
}
|
|
1364
|
+
layout.brief = { summary };
|
|
1365
|
+
}
|
|
1346
1366
|
return {
|
|
1347
|
-
text: assembleBundle(entries, maxChars,
|
|
1348
|
-
handover: stopped !== undefined,
|
|
1349
|
-
subTask: this.subTask,
|
|
1350
|
-
reportTool: opts.reportTool,
|
|
1351
|
-
}),
|
|
1367
|
+
text: assembleBundle(entries, maxChars, layout),
|
|
1352
1368
|
executed,
|
|
1353
1369
|
recorded: recordedCount,
|
|
1354
1370
|
skipped,
|
|
@@ -1359,6 +1375,8 @@ export class ScenarioReplayPlan {
|
|
|
1359
1375
|
stopped,
|
|
1360
1376
|
aborted: aborted || undefined,
|
|
1361
1377
|
endedAt: this.stepIndex,
|
|
1378
|
+
brief: brief || undefined,
|
|
1379
|
+
summaryError,
|
|
1362
1380
|
};
|
|
1363
1381
|
}
|
|
1364
1382
|
/**
|
package/dist/replay/pricing.d.ts
CHANGED
|
@@ -15,27 +15,77 @@
|
|
|
15
15
|
* numbers are comparable. For that row, right now, they are not.
|
|
16
16
|
*
|
|
17
17
|
* Bump the version — on **every** side — whenever a row changes.
|
|
18
|
+
*
|
|
19
|
+
* WHAT A PRICE DEPENDS ON BESIDE THE MODEL (Anthropic's pricing page, read
|
|
20
|
+
* 2026-11-11). Until this version a turn was priced from four token counts per
|
|
21
|
+
* model, and three things the bill reads were not looked at:
|
|
22
|
+
*
|
|
23
|
+
* - WHICH CACHE WAS WRITTEN. A 5-minute cache write bills at 1.25x input, a
|
|
24
|
+
* 1-hour one at 2x. Claude Code writes the 1-hour cache: in the transcripts
|
|
25
|
+
* this was checked against, every cache write was a 1-hour one, and each was
|
|
26
|
+
* priced 37.5% too low.
|
|
27
|
+
* - THE REQUEST'S OWN PROMPT LENGTH, on Claude Haiku 5.5: a request whose prompt
|
|
28
|
+
* (input, cache reads and cache writes together) is over 100,000 tokens pays
|
|
29
|
+
* five times the price. Each request is priced on its own.
|
|
30
|
+
* - HOW THE REQUEST RAN: fast mode bills at twice the standard prices, and
|
|
31
|
+
* US-only inference at 1.1x.
|
|
32
|
+
*
|
|
33
|
+
* So a usage now says these of itself ({@link TokenUsage}), and whoever folds
|
|
34
|
+
* requests into totals keeps the requests that are priced differently apart
|
|
35
|
+
* ({@link requestRates}).
|
|
18
36
|
*/
|
|
19
37
|
/** Identifies {@link MODEL_PRICING}. Must equal the service's constant. */
|
|
20
|
-
export declare const PRICING_VERSION = "2026-
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
* unseen version bumps by falling back to the longest matching family, so a new
|
|
24
|
-
* snapshot never silently prices at $0.
|
|
25
|
-
*/
|
|
26
|
-
export declare function resolveModelPricing(model: string): {
|
|
38
|
+
export declare const PRICING_VERSION = "2026-11-11";
|
|
39
|
+
/** One rate card, in $/million tokens. */
|
|
40
|
+
export interface ModelRates {
|
|
27
41
|
input: number;
|
|
28
42
|
output: number;
|
|
29
43
|
cacheRead: number;
|
|
44
|
+
/** A write to the 5-minute cache. */
|
|
30
45
|
cacheWrite: number;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
/** A write to the 1-hour cache. */
|
|
47
|
+
cacheWrite1h: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A model's rate card, or undefined when the table has no row for it. With
|
|
51
|
+
* `longPrompt`, the card of a request over the model's prompt-length
|
|
52
|
+
* threshold, for a model that has one.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveModelPricing(name: string, opts?: {
|
|
55
|
+
longPrompt?: boolean;
|
|
56
|
+
}): ModelRates | undefined;
|
|
57
|
+
export declare function hasModelPricing(name: string): boolean;
|
|
58
|
+
/** What of a request, beside its model and its token counts, sets its prices. */
|
|
59
|
+
export interface RateFlags {
|
|
60
|
+
/** Its prompt was over the model's long-prompt threshold (Haiku 5.5: 100,000 tokens). */
|
|
61
|
+
longPrompt?: boolean;
|
|
62
|
+
/** It ran in fast mode. */
|
|
63
|
+
fast?: boolean;
|
|
64
|
+
/** It ran on US-only inference. */
|
|
65
|
+
usOnly?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface TokenUsage extends RateFlags {
|
|
34
68
|
inputTokens: number;
|
|
35
69
|
outputTokens: number;
|
|
36
70
|
cacheReadTokens?: number;
|
|
71
|
+
/** Every cache write, the 1-hour ones among them. */
|
|
37
72
|
cacheCreationTokens?: number;
|
|
73
|
+
/** Of {@link cacheCreationTokens}, the tokens written to the 1-hour cache. */
|
|
74
|
+
cacheCreation1hTokens?: number;
|
|
38
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The flags of ONE request, from its own `usage` as the API reports it: its
|
|
78
|
+
* prompt length (input, cache reads and cache writes together), its `speed`
|
|
79
|
+
* and its `inference_geo`. Only what is so is set, so two requests priced
|
|
80
|
+
* alike give equal flags — and {@link rateKey} the same key.
|
|
81
|
+
*/
|
|
82
|
+
export declare function requestRates(name: string, request: {
|
|
83
|
+
promptTokens: number;
|
|
84
|
+
speed?: unknown;
|
|
85
|
+
inferenceGeo?: unknown;
|
|
86
|
+
}): RateFlags;
|
|
87
|
+
/** A short text that is equal for two sets of flags exactly when they price alike. "" for none. */
|
|
88
|
+
export declare function rateKey(flags: RateFlags): string;
|
|
39
89
|
/** Cost in USD. An unpriced model yields 0 — the caller reports `measured: false`. */
|
|
40
|
-
export declare function calculateCostUsd(
|
|
90
|
+
export declare function calculateCostUsd(name: string, usage: TokenUsage): number;
|
|
41
91
|
//# sourceMappingURL=pricing.d.ts.map
|
package/dist/replay/pricing.js
CHANGED
|
@@ -15,70 +15,163 @@
|
|
|
15
15
|
* numbers are comparable. For that row, right now, they are not.
|
|
16
16
|
*
|
|
17
17
|
* Bump the version — on **every** side — whenever a row changes.
|
|
18
|
+
*
|
|
19
|
+
* WHAT A PRICE DEPENDS ON BESIDE THE MODEL (Anthropic's pricing page, read
|
|
20
|
+
* 2026-11-11). Until this version a turn was priced from four token counts per
|
|
21
|
+
* model, and three things the bill reads were not looked at:
|
|
22
|
+
*
|
|
23
|
+
* - WHICH CACHE WAS WRITTEN. A 5-minute cache write bills at 1.25x input, a
|
|
24
|
+
* 1-hour one at 2x. Claude Code writes the 1-hour cache: in the transcripts
|
|
25
|
+
* this was checked against, every cache write was a 1-hour one, and each was
|
|
26
|
+
* priced 37.5% too low.
|
|
27
|
+
* - THE REQUEST'S OWN PROMPT LENGTH, on Claude Haiku 5.5: a request whose prompt
|
|
28
|
+
* (input, cache reads and cache writes together) is over 100,000 tokens pays
|
|
29
|
+
* five times the price. Each request is priced on its own.
|
|
30
|
+
* - HOW THE REQUEST RAN: fast mode bills at twice the standard prices, and
|
|
31
|
+
* US-only inference at 1.1x.
|
|
32
|
+
*
|
|
33
|
+
* So a usage now says these of itself ({@link TokenUsage}), and whoever folds
|
|
34
|
+
* requests into totals keeps the requests that are priced differently apart
|
|
35
|
+
* ({@link requestRates}).
|
|
18
36
|
*/
|
|
19
37
|
/** Identifies {@link MODEL_PRICING}. Must equal the service's constant. */
|
|
20
|
-
export const PRICING_VERSION = "2026-
|
|
38
|
+
export const PRICING_VERSION = "2026-11-11";
|
|
21
39
|
/**
|
|
22
|
-
* A 5-minute cache write bills at 1.25x
|
|
23
|
-
*
|
|
24
|
-
*
|
|
40
|
+
* A rate card from its list prices. A 5-minute cache write bills at 1.25x
|
|
41
|
+
* input and a 1-hour one at 2x, on every model. A cache read bills at 0.1x
|
|
42
|
+
* input, except on the models whose list price says otherwise (0.05x on Opus
|
|
43
|
+
* 5.5 and Sonnet 5.5; 0.025x on Fable and Mythos 5.1 and 5.2), which pass
|
|
44
|
+
* their read price in dollars.
|
|
25
45
|
*/
|
|
26
46
|
const priced = (input, output, cacheRead = input * 0.1) => ({
|
|
27
47
|
input,
|
|
28
48
|
output,
|
|
29
49
|
cacheRead,
|
|
30
50
|
cacheWrite: input * 1.25,
|
|
51
|
+
cacheWrite1h: input * 2,
|
|
31
52
|
});
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"claude-
|
|
40
|
-
|
|
41
|
-
"claude-
|
|
42
|
-
"claude-
|
|
43
|
-
"claude-
|
|
44
|
-
"claude-
|
|
45
|
-
"claude-
|
|
46
|
-
|
|
53
|
+
const model = (rates, longPrompt) => longPrompt ? { rates, longPrompt } : { rates };
|
|
54
|
+
/**
|
|
55
|
+
* A Map, not an object: a model's name comes from a transcript or from another
|
|
56
|
+
* host's payload, and `table["constructor"]` on an object is a function, which
|
|
57
|
+
* priced a turn at NaN.
|
|
58
|
+
*/
|
|
59
|
+
const MODEL_PRICING = new Map([
|
|
60
|
+
["claude-haiku-4-5", model(priced(1.0, 5.0))],
|
|
61
|
+
// Priced by prompt length: up to 100,000 tokens, and over.
|
|
62
|
+
["claude-haiku-5-5", model(priced(0.1, 0.5), { overTokens: 100_000, rates: priced(0.5, 2.5) })],
|
|
63
|
+
["claude-sonnet-4", model(priced(3.0, 15.0))],
|
|
64
|
+
["claude-sonnet-4-0", model(priced(3.0, 15.0))],
|
|
65
|
+
["claude-sonnet-4-5", model(priced(3.0, 15.0))],
|
|
66
|
+
["claude-sonnet-4-6", model(priced(3.0, 15.0))],
|
|
67
|
+
["claude-sonnet-5", model(priced(2.0, 10.0))],
|
|
68
|
+
["claude-sonnet-5-5", model(priced(2.0, 10.0, 0.1))],
|
|
69
|
+
["claude-opus-4", model(priced(15.0, 75.0))],
|
|
70
|
+
["claude-opus-4-0", model(priced(15.0, 75.0))],
|
|
71
|
+
["claude-opus-4-1", model(priced(15.0, 75.0))],
|
|
72
|
+
["claude-opus-4-5", model(priced(5.0, 25.0))],
|
|
73
|
+
["claude-opus-4-6", model(priced(5.0, 25.0))],
|
|
74
|
+
["claude-opus-4-7", model(priced(5.0, 25.0))],
|
|
75
|
+
["claude-opus-4-8", model(priced(5.0, 25.0))],
|
|
76
|
+
["claude-opus-5", model(priced(5.0, 25.0))],
|
|
77
|
+
["claude-opus-5-5", model(priced(4.0, 20.0, 0.2))],
|
|
78
|
+
["claude-fable-5", model(priced(10.0, 50.0))],
|
|
79
|
+
["claude-fable-5-1", model(priced(10.0, 50.0, 0.25))],
|
|
80
|
+
["claude-fable-5-2", model(priced(8.0, 40.0, 0.2))],
|
|
81
|
+
["claude-mythos-5", model(priced(10.0, 50.0))],
|
|
82
|
+
["claude-mythos-5-1", model(priced(10.0, 50.0, 0.25))],
|
|
83
|
+
["claude-mythos-5-2", model(priced(8.0, 40.0, 0.2))],
|
|
84
|
+
]);
|
|
85
|
+
/** Fast mode bills every kind of token at twice the standard price. */
|
|
86
|
+
const FAST_FACTOR = 2;
|
|
87
|
+
/** US-only inference (`inference_geo: "us"`) bills every kind of token at 1.1x. */
|
|
88
|
+
const US_ONLY_FACTOR = 1.1;
|
|
89
|
+
/**
|
|
90
|
+
* What may follow a model's name and still be that model: a dated snapshot
|
|
91
|
+
* (`-20251001`, `@20251001`), a cloud's version tag (`-v1:0`), `-latest`.
|
|
92
|
+
*/
|
|
93
|
+
const SNAPSHOT_SUFFIX = /^(?:[-@]\d{8})?(?:-v\d+(?::\d+)?)?(?:-latest)?$/;
|
|
47
94
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
95
|
+
* The table's row for a name: the name itself, or the name with a provider
|
|
96
|
+
* prefix (`anthropic/`, `us.anthropic.`), a host's tag (`[1m]`) or a snapshot
|
|
97
|
+
* suffix taken off.
|
|
98
|
+
*
|
|
99
|
+
* NEVER A NEWER VERSION PRICED AS AN OLDER ONE. This used to take the longest
|
|
100
|
+
* row that prefixed the name, "so a new snapshot never silently prices at $0".
|
|
101
|
+
* A new *version* is not a snapshot: `claude-opus-5-5` was priced as Opus 5
|
|
102
|
+
* until its row came, and `claude-fable-5-2` as Fable 5 — a quarter too dear
|
|
103
|
+
* on input and output, five times on cache reads — and each such turn was
|
|
104
|
+
* reported as measured. A version with no row has no price: the turn is
|
|
105
|
+
* reported unmeasured, and says which model to add (`run.unmeasured`).
|
|
51
106
|
*/
|
|
52
|
-
|
|
53
|
-
const exact = MODEL_PRICING
|
|
107
|
+
function pricedModel(name) {
|
|
108
|
+
const exact = MODEL_PRICING.get(name);
|
|
54
109
|
if (exact)
|
|
55
110
|
return exact;
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
return
|
|
61
|
-
// Longest declared family that prefixes the name — so a dated snapshot of a
|
|
62
|
-
// known model prices as that model rather than as nothing.
|
|
111
|
+
const at = name.indexOf("claude-");
|
|
112
|
+
const bare = (at >= 0 ? name.slice(at) : name).replace(/\[[^\]]*\]$/, "");
|
|
113
|
+
const known = MODEL_PRICING.get(bare);
|
|
114
|
+
if (known)
|
|
115
|
+
return known;
|
|
63
116
|
let best;
|
|
64
|
-
for (const key of
|
|
65
|
-
if (bare.startsWith(key)
|
|
117
|
+
for (const key of MODEL_PRICING.keys()) {
|
|
118
|
+
if (!bare.startsWith(key) || !SNAPSHOT_SUFFIX.test(bare.slice(key.length)))
|
|
119
|
+
continue;
|
|
120
|
+
if (!best || key.length > best.length)
|
|
66
121
|
best = key;
|
|
67
122
|
}
|
|
68
|
-
return best ? MODEL_PRICING
|
|
123
|
+
return best ? MODEL_PRICING.get(best) : undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A model's rate card, or undefined when the table has no row for it. With
|
|
127
|
+
* `longPrompt`, the card of a request over the model's prompt-length
|
|
128
|
+
* threshold, for a model that has one.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveModelPricing(name, opts = {}) {
|
|
131
|
+
const row = pricedModel(name);
|
|
132
|
+
if (!row)
|
|
133
|
+
return undefined;
|
|
134
|
+
return opts.longPrompt && row.longPrompt ? row.longPrompt.rates : row.rates;
|
|
135
|
+
}
|
|
136
|
+
export function hasModelPricing(name) {
|
|
137
|
+
return pricedModel(name) !== undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The flags of ONE request, from its own `usage` as the API reports it: its
|
|
141
|
+
* prompt length (input, cache reads and cache writes together), its `speed`
|
|
142
|
+
* and its `inference_geo`. Only what is so is set, so two requests priced
|
|
143
|
+
* alike give equal flags — and {@link rateKey} the same key.
|
|
144
|
+
*/
|
|
145
|
+
export function requestRates(name, request) {
|
|
146
|
+
const flags = {};
|
|
147
|
+
const over = pricedModel(name)?.longPrompt?.overTokens;
|
|
148
|
+
if (over !== undefined && request.promptTokens > over)
|
|
149
|
+
flags.longPrompt = true;
|
|
150
|
+
if (request.speed === "fast")
|
|
151
|
+
flags.fast = true;
|
|
152
|
+
if (request.inferenceGeo === "us")
|
|
153
|
+
flags.usOnly = true;
|
|
154
|
+
return flags;
|
|
69
155
|
}
|
|
70
|
-
|
|
71
|
-
|
|
156
|
+
/** A short text that is equal for two sets of flags exactly when they price alike. "" for none. */
|
|
157
|
+
export function rateKey(flags) {
|
|
158
|
+
return `${flags.longPrompt ? "|long" : ""}${flags.fast ? "|fast" : ""}${flags.usOnly ? "|us" : ""}`;
|
|
72
159
|
}
|
|
73
160
|
/** Cost in USD. An unpriced model yields 0 — the caller reports `measured: false`. */
|
|
74
|
-
export function calculateCostUsd(
|
|
75
|
-
const p = resolveModelPricing(
|
|
161
|
+
export function calculateCostUsd(name, usage) {
|
|
162
|
+
const p = resolveModelPricing(name, usage);
|
|
76
163
|
if (!p)
|
|
77
164
|
return 0;
|
|
165
|
+
const writes = Math.max(0, usage.cacheCreationTokens ?? 0);
|
|
166
|
+
const writes1h = Math.min(writes, Math.max(0, usage.cacheCreation1hTokens ?? 0));
|
|
167
|
+
const factor = (usage.fast ? FAST_FACTOR : 1) * (usage.usOnly ? US_ONLY_FACTOR : 1);
|
|
78
168
|
const perToken = 1e-6;
|
|
79
|
-
return (
|
|
80
|
-
|
|
81
|
-
(usage.
|
|
82
|
-
|
|
169
|
+
return (factor *
|
|
170
|
+
perToken *
|
|
171
|
+
(usage.inputTokens * p.input +
|
|
172
|
+
usage.outputTokens * p.output +
|
|
173
|
+
(usage.cacheReadTokens ?? 0) * p.cacheRead +
|
|
174
|
+
(writes - writes1h) * p.cacheWrite +
|
|
175
|
+
writes1h * p.cacheWrite1h));
|
|
83
176
|
}
|
|
84
177
|
//# sourceMappingURL=pricing.js.map
|
package/dist/replay/safety.js
CHANGED
|
@@ -49,7 +49,30 @@ const WRITE_BUILTINS = new Set(["Edit", "MultiEdit", "Write", "NotebookEdit"]);
|
|
|
49
49
|
* with the server.
|
|
50
50
|
*/
|
|
51
51
|
const MCP_SHELL_WORD = /(?:^|[_.-])(?:shell|bash|sh|zsh|ssh|exec|execute|command|cmd|terminal|powershell|pwsh)(?:$|[_.-])/i;
|
|
52
|
-
const MCP_COMMAND_WORD = /(?:^|[_.-])(?:sql|tsql|query|exec|execute|shell|bash|sh|zsh|ssh|command|cmd|script|eval|terminal|psql|mysql|mssql|sqlite|duckdb|postgres|postgresql|powershell|pwsh)(?:$|[_.-])/i;
|
|
52
|
+
const MCP_COMMAND_WORD = /(?:^|[_.-])(?:sql|tsql|query|exec|execute|shell|bash|sh|zsh|ssh|command|cmd|script|eval|cypher|terminal|psql|mysql|mssql|sqlite|duckdb|postgres|postgresql|powershell|pwsh)(?:$|[_.-])/i;
|
|
53
|
+
/**
|
|
54
|
+
* A tool whose input is code runs it as surely as a shell runs a command:
|
|
55
|
+
* `run_code`, `execute_python`, `code_interpreter`, `python_repl`, a
|
|
56
|
+
* `javascript_tool`, `browser_evaluate`, `evaluate_script`. Known by the words
|
|
57
|
+
* that say the code is run, and not by the language's name alone: as words of
|
|
58
|
+
* their own, `code`, `python`, `javascript` and `evaluate` made a command of
|
|
59
|
+
* `search_code`, `get_postal_code`, `send_verification_code`, `evaluate_answer`
|
|
60
|
+
* and of every tool of a `python-docs` or `claude-code` server — and with one
|
|
61
|
+
* such step every value of the plan was held to a command's rules, so a query
|
|
62
|
+
* with a quote in it handed the plan over. Whole words only, as above:
|
|
63
|
+
* `decode_token` and a `barcode` have the letters of `code` and are not it.
|
|
64
|
+
*/
|
|
65
|
+
const MCP_CODE_RUNNER = new RegExp(`(?:^|[_.-])(?:${[
|
|
66
|
+
"(?:run|exec|execute|eval)[_.-](?:code|python|py|javascript|js|typescript|ts|node)",
|
|
67
|
+
"(?:code|python|py|javascript|js|typescript|node)[_.-](?:run|runner|exec|execute|executor|eval|interpreter|repl)",
|
|
68
|
+
"(?:javascript|js)[_.-]tool",
|
|
69
|
+
"(?:browser|page|puppeteer|playwright|frame|tab|chrome|devtools)[_.-]evaluate",
|
|
70
|
+
"evaluate[_.-](?:script|js|javascript|expression|code)",
|
|
71
|
+
].join("|")})(?:$|[_.-])`, "i");
|
|
72
|
+
/** Whether a name, split into words ({@link nameWords}), says its input is run. */
|
|
73
|
+
function saysRun(words) {
|
|
74
|
+
return MCP_COMMAND_WORD.test(words) || MCP_CODE_RUNNER.test(words);
|
|
75
|
+
}
|
|
53
76
|
/**
|
|
54
77
|
* A tool or server name as delimited words: camelCase, PascalCase and acronym
|
|
55
78
|
* runs are split with `_`, so `executeSql` reads as `execute_Sql`, `runSQL` as
|
|
@@ -66,6 +89,18 @@ function mcpNameSays(toolName, words) {
|
|
|
66
89
|
return false;
|
|
67
90
|
return words.test(nameWords(mcp.toolName)) || words.test(nameWords(mcp.serverName));
|
|
68
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether an MCP tool's name, its server's, or the two read together say its
|
|
94
|
+
* input is run: `mcp__python__run` says it only with both (`python_run`).
|
|
95
|
+
*/
|
|
96
|
+
function mcpRunsInput(toolName) {
|
|
97
|
+
const mcp = parseQualifiedName(toolName);
|
|
98
|
+
if (!mcp)
|
|
99
|
+
return false;
|
|
100
|
+
const tool = nameWords(mcp.toolName);
|
|
101
|
+
const server = nameWords(mcp.serverName);
|
|
102
|
+
return saysRun(tool) || saysRun(server) || MCP_CODE_RUNNER.test(`${server}_${tool}`);
|
|
103
|
+
}
|
|
69
104
|
/**
|
|
70
105
|
* Characters and sequences that end a quoted string or begin another command
|
|
71
106
|
* in a POSIX shell, PowerShell or SQL. Deliberately wide: an apostrophe in a
|
|
@@ -105,13 +140,13 @@ export function sinkOf(toolName, host) {
|
|
|
105
140
|
return "command";
|
|
106
141
|
if (host.write?.includes(name))
|
|
107
142
|
return "write";
|
|
108
|
-
return
|
|
143
|
+
return saysRun(nameWords(name)) ? "command" : undefined;
|
|
109
144
|
}
|
|
110
145
|
if (SHELL_BUILTINS.has(name))
|
|
111
146
|
return "command";
|
|
112
147
|
if (WRITE_BUILTINS.has(name))
|
|
113
148
|
return "write";
|
|
114
|
-
return
|
|
149
|
+
return mcpRunsInput(name) ? "command" : undefined;
|
|
115
150
|
}
|
|
116
151
|
/** Whether this tool's input is run or written: a shell, a file edit, a query. */
|
|
117
152
|
export function isCommandSink(toolName) {
|
|
@@ -189,8 +224,11 @@ const SQL_BOOLEAN_WORD = /\b(?:or|and|union)\b/gi;
|
|
|
189
224
|
* keeps it.
|
|
190
225
|
*/
|
|
191
226
|
function offShape(value, sample) {
|
|
192
|
-
// A number stays a number: `1 OR 1=1` is not a user id.
|
|
193
|
-
|
|
227
|
+
// A number stays a number: `1 OR 1=1` is not a user id. A number kept as
|
|
228
|
+
// text (`"42"`) is a number all the same: with that sample, `"… WHERE
|
|
229
|
+
// user_id = " + parameters.v` took the value `user_id` and matched every
|
|
230
|
+
// row, where the sample 42 refused it.
|
|
231
|
+
if (typeof sample === "number" || (typeof sample === "string" && NUMERIC.test(sample))) {
|
|
194
232
|
if (typeof value === "number")
|
|
195
233
|
return Number.isFinite(value) ? undefined : String(value);
|
|
196
234
|
if (typeof value !== "string" || !NUMERIC.test(value))
|
|
@@ -41,12 +41,14 @@
|
|
|
41
41
|
* `initialize` got a 405 and the server showed as failed every session).
|
|
42
42
|
* A stream that ends — cleanly or not — is noticed, the requests riding it
|
|
43
43
|
* are answered, and it is re-established with backoff; the new session is
|
|
44
|
-
* re-initialised with the host's own `initialize` before anything else
|
|
44
|
+
* re-initialised with the host's own `initialize` before anything else,
|
|
45
|
+
* and a stream whose re-initialisation fails is a reconnect that failed.
|
|
45
46
|
* 4. **Session expiry is survivable.** A 404 to a request that carried a
|
|
46
47
|
* session means the server forgot us (a redeploy, an idle timeout). The
|
|
47
48
|
* spec says start a new session; the proxy does it for the host — replays
|
|
48
49
|
* the host's cached `initialize` under a `bir:` id, sends `initialized`,
|
|
49
|
-
* and retries the request once.
|
|
50
|
+
* and retries the request once. A replay that fails is tried again when
|
|
51
|
+
* the next request meets the same 404.
|
|
50
52
|
* 5. **No timeout shorter than the host's.** The transport (http-transport.ts)
|
|
51
53
|
* bounds the connect only; how long a tool may take is Claude Code's call,
|
|
52
54
|
* and its cancellation reaches the server immediately (rule 2). What no
|
|
@@ -200,7 +202,8 @@ export declare class HttpUpstreamClient extends BaseUpstreamClient {
|
|
|
200
202
|
/**
|
|
201
203
|
* Replay the host's `initialize` (under a `bir:` id) and `initialized`.
|
|
202
204
|
* `news`: say it on stderr when it worked — after a forgotten session or an
|
|
203
|
-
* outage; after a routine reconnect it is detail. A failure is
|
|
205
|
+
* outage; after a routine reconnect it is detail. A failure is said, unless
|
|
206
|
+
* `failureSaid`: it is one more of an outage whose first failure was.
|
|
204
207
|
*/
|
|
205
208
|
private reinitialize;
|
|
206
209
|
/**
|