@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.
Files changed (67) hide show
  1. package/README.md +27 -26
  2. package/dist/auth/client.d.ts +4 -1
  3. package/dist/auth/client.js +83 -14
  4. package/dist/bin/bir-hooks.js +4 -2
  5. package/dist/bin/bir.js +153 -37
  6. package/dist/bin/investigate.js +5 -0
  7. package/dist/bin/scenario-edit.d.ts +5 -0
  8. package/dist/bin/scenario-edit.js +17 -1
  9. package/dist/config/adapters/claude-code.d.ts +51 -7
  10. package/dist/config/adapters/claude-code.js +105 -18
  11. package/dist/config/generate.d.ts +61 -13
  12. package/dist/config/generate.js +108 -20
  13. package/dist/config/resolve.d.ts +5 -0
  14. package/dist/config/resolve.js +7 -0
  15. package/dist/control/correlation.js +0 -0
  16. package/dist/control/daemon.d.ts +28 -0
  17. package/dist/control/daemon.js +62 -0
  18. package/dist/control/paths.d.ts +19 -1
  19. package/dist/control/paths.js +20 -2
  20. package/dist/control/pin-guard.js +8 -2
  21. package/dist/control/recorder-main.js +17 -5
  22. package/dist/control/server.d.ts +63 -9
  23. package/dist/control/server.js +350 -62
  24. package/dist/control/transcript.d.ts +64 -3
  25. package/dist/control/transcript.js +318 -37
  26. package/dist/jsonrpc/framing.d.ts +18 -3
  27. package/dist/jsonrpc/framing.js +57 -4
  28. package/dist/proxy/intercept.d.ts +4 -0
  29. package/dist/proxy/intercept.js +4 -0
  30. package/dist/proxy/relay.d.ts +13 -0
  31. package/dist/proxy/relay.js +12 -1
  32. package/dist/proxy/session.d.ts +8 -1
  33. package/dist/proxy/session.js +67 -10
  34. package/dist/record/recorder.d.ts +6 -1
  35. package/dist/record/redact.d.ts +18 -4
  36. package/dist/record/redact.js +538 -79
  37. package/dist/record/remote-recorder.d.ts +26 -5
  38. package/dist/record/remote-recorder.js +174 -18
  39. package/dist/record/truncate.js +3 -1
  40. package/dist/replay/bash-channel.d.ts +7 -1
  41. package/dist/replay/bash-channel.js +9 -3
  42. package/dist/replay/builtins.d.ts +14 -0
  43. package/dist/replay/builtins.js +460 -95
  44. package/dist/replay/bundle.d.ts +80 -0
  45. package/dist/replay/bundle.js +455 -24
  46. package/dist/replay/controller.d.ts +64 -1
  47. package/dist/replay/controller.js +238 -38
  48. package/dist/replay/derive.js +46 -4
  49. package/dist/replay/handover.d.ts +13 -0
  50. package/dist/replay/handover.js +18 -0
  51. package/dist/replay/plan.d.ts +15 -0
  52. package/dist/replay/plan.js +48 -30
  53. package/dist/replay/pricing.d.ts +61 -11
  54. package/dist/replay/pricing.js +135 -42
  55. package/dist/replay/safety.js +43 -5
  56. package/dist/upstream/http-client.d.ts +6 -3
  57. package/dist/upstream/http-client.js +54 -13
  58. package/dist/upstream/stdio-client.d.ts +7 -0
  59. package/dist/upstream/stdio-client.js +1 -1
  60. package/dist/util/atomic-write.d.ts +9 -1
  61. package/dist/util/atomic-write.js +9 -6
  62. package/dist/util/service-health.d.ts +7 -0
  63. package/dist/util/service-health.js +10 -3
  64. package/docs/calculatedReplay.md +32 -0
  65. package/package.json +1 -1
  66. package/production.md +2 -2
  67. package/scripts/install.ps1 +14 -2
@@ -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)
@@ -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 {
@@ -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
  /**
@@ -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-09-24";
21
- /**
22
- * Tolerate provider prefixes (`anthropic/`, `us.anthropic.`), date snapshots and
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
- } | undefined;
32
- export declare function hasModelPricing(model: string): boolean;
33
- export interface TokenUsage {
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(model: string, usage: TokenUsage): number;
90
+ export declare function calculateCostUsd(name: string, usage: TokenUsage): number;
41
91
  //# sourceMappingURL=pricing.d.ts.map
@@ -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-09-24";
38
+ export const PRICING_VERSION = "2026-11-11";
21
39
  /**
22
- * A 5-minute cache write bills at 1.25x input. Cache reads bill at 0.1x input,
23
- * except on models whose list price says otherwise (Opus 5.5, Fable 5.1), which
24
- * pass their read price in dollars.
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 MODEL_PRICING = {
33
- "claude-haiku-4-5": priced(1.0, 5.0),
34
- "claude-haiku-4-5-20251001": priced(1.0, 5.0),
35
- "claude-sonnet-4-5": priced(3.0, 15.0),
36
- "claude-sonnet-4-6": priced(3.0, 15.0),
37
- "claude-sonnet-5": priced(2.0, 10.0),
38
- "claude-opus-4-5": priced(5.0, 25.0),
39
- "claude-opus-4-6": priced(5.0, 25.0),
40
- "claude-opus-4-7": priced(5.0, 25.0),
41
- "claude-opus-4-8": priced(5.0, 25.0),
42
- "claude-opus-5": priced(5.0, 25.0),
43
- "claude-opus-5-5": priced(4.0, 20.0, 0.2),
44
- "claude-fable-5": priced(10.0, 50.0),
45
- "claude-fable-5-1": priced(10.0, 50.0, 0.25),
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
- * Tolerate provider prefixes (`anthropic/`, `us.anthropic.`), date snapshots and
49
- * unseen version bumps by falling back to the longest matching family, so a new
50
- * snapshot never silently prices at $0.
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
- export function resolveModelPricing(model) {
53
- const exact = MODEL_PRICING[model];
107
+ function pricedModel(name) {
108
+ const exact = MODEL_PRICING.get(name);
54
109
  if (exact)
55
110
  return exact;
56
- // Strip a provider prefix: "us.anthropic.claude-haiku-4-5-v1:0" → "claude-haiku-4-5-v1:0".
57
- const idx = model.indexOf("claude-");
58
- const bare = idx >= 0 ? model.slice(idx) : model;
59
- if (MODEL_PRICING[bare])
60
- return MODEL_PRICING[bare];
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 Object.keys(MODEL_PRICING)) {
65
- if (bare.startsWith(key) && (!best || key.length > best.length))
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[best] : undefined;
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
- export function hasModelPricing(model) {
71
- return resolveModelPricing(model) !== undefined;
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(model, usage) {
75
- const p = resolveModelPricing(model);
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 (usage.inputTokens * p.input * perToken +
80
- usage.outputTokens * p.output * perToken +
81
- (usage.cacheReadTokens ?? 0) * p.cacheRead * perToken +
82
- (usage.cacheCreationTokens ?? 0) * p.cacheWrite * perToken);
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
@@ -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 plainNameSays(name, MCP_COMMAND_WORD) ? "command" : undefined;
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 mcpNameSays(name, MCP_COMMAND_WORD) ? "command" : undefined;
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
- if (typeof sample === "number") {
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 always said.
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
  /**