@basein/runner 0.3.25 → 0.3.27
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.d.ts +13 -0
- package/dist/bin/investigate.js +72 -2
- 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 +75 -11
- package/dist/control/server.js +440 -77
- 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 +15 -11
- package/dist/replay/bundle.js +167 -33
- package/dist/replay/controller.d.ts +92 -1
- package/dist/replay/controller.js +246 -47
- 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 +41 -0
- package/dist/replay/plan.js +171 -3
- package/dist/replay/pricing.d.ts +61 -11
- package/dist/replay/pricing.js +135 -42
- package/dist/replay/reads-only.d.ts +48 -0
- package/dist/replay/reads-only.js +161 -0
- 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/package.json +1 -1
- package/production.md +2 -2
- package/scripts/install.ps1 +14 -2
package/dist/replay/plan.js
CHANGED
|
@@ -132,6 +132,47 @@ function clipReasoning(reasoning) {
|
|
|
132
132
|
const first = (reasoning ?? "").trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
133
133
|
return first.length > MAX_REASONING_CHARS ? `${first.slice(0, MAX_REASONING_CHARS)}…` : first;
|
|
134
134
|
}
|
|
135
|
+
// ── the recording's values in a directive's text (B2 is11.md) ────────────────
|
|
136
|
+
/**
|
|
137
|
+
* A recorded value as a text may name it: a string or a number at least this
|
|
138
|
+
* long — a one-character sample ("5", "a") is found in too many words — and
|
|
139
|
+
* no longer than a name or a title is.
|
|
140
|
+
*/
|
|
141
|
+
const RECORDED_VALUE_MIN = 2;
|
|
142
|
+
const RECORDED_VALUE_MAX = 200;
|
|
143
|
+
/** One of this request's values, as a directive shows it: this long at most. */
|
|
144
|
+
const SHOWN_VALUE_MAX = 60;
|
|
145
|
+
/** This many of this request's values at most on a direct directive's values line… */
|
|
146
|
+
const SHOWN_VALUES = 4;
|
|
147
|
+
/** …and this long at most, all of them. */
|
|
148
|
+
const SHOWN_VALUES_CHARS = 200;
|
|
149
|
+
/** The recorded value `value` as a text may name it, or undefined. */
|
|
150
|
+
function recordedText(value) {
|
|
151
|
+
const text = typeof value === "number" && Number.isFinite(value)
|
|
152
|
+
? String(value)
|
|
153
|
+
: typeof value === "string"
|
|
154
|
+
? value.trim()
|
|
155
|
+
: undefined;
|
|
156
|
+
return text !== undefined && text.length >= RECORDED_VALUE_MIN && text.length <= RECORDED_VALUE_MAX ? text : undefined;
|
|
157
|
+
}
|
|
158
|
+
/** A value of this request's as plain text — never a mark — or undefined. */
|
|
159
|
+
function valueText(value) {
|
|
160
|
+
let text;
|
|
161
|
+
if (typeof value === "boolean")
|
|
162
|
+
text = String(value);
|
|
163
|
+
else if (typeof value === "number" && Number.isFinite(value))
|
|
164
|
+
text = String(value);
|
|
165
|
+
else if (typeof value === "string")
|
|
166
|
+
text = value.replace(/\s+/g, " ").trim();
|
|
167
|
+
if (!text || markedKey(text) !== undefined)
|
|
168
|
+
return undefined;
|
|
169
|
+
return text;
|
|
170
|
+
}
|
|
171
|
+
const clipValue = (text) => text.length > SHOWN_VALUE_MAX ? `${text.slice(0, SHOWN_VALUE_MAX)}…` : text;
|
|
172
|
+
const sameText = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
173
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
174
|
+
/** A parameter's role, for a value a text must not name: `assignee_name` → `<assignee name>`. */
|
|
175
|
+
const roleOf = (key) => `<${key.replace(/[_-]+/g, " ").trim() || key}>`;
|
|
135
176
|
/** A step's computed arguments, as a steer directive shows them: this long at most. */
|
|
136
177
|
const MAX_DIRECTIVE_ARGS_CHARS = 240;
|
|
137
178
|
/**
|
|
@@ -854,6 +895,120 @@ export class ScenarioReplayPlan {
|
|
|
854
895
|
}
|
|
855
896
|
return out;
|
|
856
897
|
}
|
|
898
|
+
/**
|
|
899
|
+
* This request's value of `key`, as a directive may show it, or undefined
|
|
900
|
+
* (B2 is11.md): once the reading has settled, a plain value the reading
|
|
901
|
+
* took from the request — never a mark, and never a value a step works out
|
|
902
|
+
* (`from: "step"`): before the run nobody has that one, and the recorded one
|
|
903
|
+
* is another request's (issue 1212).
|
|
904
|
+
*/
|
|
905
|
+
shownValueOf(key, p) {
|
|
906
|
+
if (!this.paramsSettled || p?.from === "step")
|
|
907
|
+
return undefined;
|
|
908
|
+
return valueText(this.derivedParams[key]);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* A text written from the recording — the intent, a step's reasoning — put
|
|
912
|
+
* right for this request, for a directive to show (B2 is11.md).
|
|
913
|
+
*
|
|
914
|
+
* Both name the recording's values: "set Olga up", "moves each lead to
|
|
915
|
+
* Olga". Shown beside a request for Carlos, the name tells the model the
|
|
916
|
+
* plan is for someone else. A careful model then checks before it runs
|
|
917
|
+
* anything, and a direct plan's first call that was not `run_scenario`
|
|
918
|
+
* retired it — so the replay was lost on exactly the turn replay is for: the
|
|
919
|
+
* same task with a new value. The steps never read the text: they take
|
|
920
|
+
* Carlos from the request.
|
|
921
|
+
*
|
|
922
|
+
* Each recorded value of a parameter found in the text, as a whole word, is
|
|
923
|
+
* replaced by this request's own value where the plan has one it may show
|
|
924
|
+
* ({@link shownValueOf}), and else by the parameter's role (`<assignee
|
|
925
|
+
* name>`): a target the request did not name, a value a step works out, or
|
|
926
|
+
* any value while the reading has not settled. A value the same as the
|
|
927
|
+
* recorded one stays as it is. One pass, longest value first, so nothing put
|
|
928
|
+
* in is replaced again.
|
|
929
|
+
*/
|
|
930
|
+
forThisRequest(text) {
|
|
931
|
+
return this.rewriter()(text);
|
|
932
|
+
}
|
|
933
|
+
/** {@link forThisRequest} as one function, built once for a directive's many texts. */
|
|
934
|
+
rewriter() {
|
|
935
|
+
const schema = this.scenario.paramsObject;
|
|
936
|
+
if (!schema || typeof schema !== "object")
|
|
937
|
+
return (text) => text;
|
|
938
|
+
/** Recorded value (lower case) → what goes in its place; null where it stays. */
|
|
939
|
+
const swaps = new Map();
|
|
940
|
+
for (const [key, p] of Object.entries(schema)) {
|
|
941
|
+
const recorded = recordedText(p?.sampleValue);
|
|
942
|
+
if (!recorded)
|
|
943
|
+
continue;
|
|
944
|
+
const now = this.shownValueOf(key, p);
|
|
945
|
+
const swap = now !== undefined && sameText(now, recorded) ? null : now !== undefined ? clipValue(now) : roleOf(key);
|
|
946
|
+
const k = recorded.toLowerCase();
|
|
947
|
+
// Two parameters recorded with one value: a change wins over "stays".
|
|
948
|
+
if (!swaps.has(k) || swaps.get(k) === null)
|
|
949
|
+
swaps.set(k, swap);
|
|
950
|
+
}
|
|
951
|
+
const found = [...swaps.entries()].filter(([, swap]) => swap !== null).map(([k]) => k);
|
|
952
|
+
if (found.length === 0)
|
|
953
|
+
return (text) => text;
|
|
954
|
+
found.sort((a, b) => b.length - a.length);
|
|
955
|
+
let pattern;
|
|
956
|
+
try {
|
|
957
|
+
pattern = new RegExp(`(?<![\\p{L}\\p{N}_])(?:${found.map(escapeRegExp).join("|")})(?![\\p{L}\\p{N}_])`, "giu");
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
return (text) => text;
|
|
961
|
+
}
|
|
962
|
+
return (text) => (text ? text.replace(pattern, (m) => swaps.get(m.toLowerCase()) ?? m) : text);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* This request's own values, for the direct directive's line that says the
|
|
966
|
+
* steps take them (B2 is11.md): what the reading took from the request.
|
|
967
|
+
* The values the request names in words come first — a name, a title: what
|
|
968
|
+
* a reader holds the note against — those unlike the recording's first;
|
|
969
|
+
* then the values worked out from the day ("tomorrow" as a date). Targets
|
|
970
|
+
* only: never a setting, never a value a step works out, never a mark.
|
|
971
|
+
* Undefined until the reading has settled, or when it named none.
|
|
972
|
+
*/
|
|
973
|
+
requestValues() {
|
|
974
|
+
if (!this.paramsSettled)
|
|
975
|
+
return undefined;
|
|
976
|
+
const schema = this.scenario.paramsObject;
|
|
977
|
+
if (!schema || typeof schema !== "object")
|
|
978
|
+
return undefined;
|
|
979
|
+
const items = [];
|
|
980
|
+
let order = 0;
|
|
981
|
+
for (const [key, p] of Object.entries(schema)) {
|
|
982
|
+
order += 1;
|
|
983
|
+
if (p?.kind === "setting")
|
|
984
|
+
continue;
|
|
985
|
+
const now = this.shownValueOf(key, p);
|
|
986
|
+
if (now === undefined)
|
|
987
|
+
continue;
|
|
988
|
+
const recorded = recordedText(p?.sampleValue);
|
|
989
|
+
items.push({
|
|
990
|
+
key,
|
|
991
|
+
text: clipValue(now),
|
|
992
|
+
// Named in the request's own words; an older service sends no `from`.
|
|
993
|
+
said: p?.from === undefined || p.from === "prompt",
|
|
994
|
+
differs: recorded !== undefined && !sameText(now, recorded),
|
|
995
|
+
order,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
items.sort((a, b) => Number(b.said) - Number(a.said) || Number(b.differs) - Number(a.differs) || a.order - b.order);
|
|
999
|
+
const parts = [];
|
|
1000
|
+
let chars = 0;
|
|
1001
|
+
for (const item of items) {
|
|
1002
|
+
if (parts.length >= SHOWN_VALUES)
|
|
1003
|
+
break;
|
|
1004
|
+
const part = `${item.key} ${JSON.stringify(item.text)}`;
|
|
1005
|
+
if (parts.length > 0 && chars + part.length > SHOWN_VALUES_CHARS)
|
|
1006
|
+
break;
|
|
1007
|
+
parts.push(part);
|
|
1008
|
+
chars += part.length + 2;
|
|
1009
|
+
}
|
|
1010
|
+
return parts.length > 0 ? parts.join(", ") : undefined;
|
|
1011
|
+
}
|
|
857
1012
|
/**
|
|
858
1013
|
* The directive injected via `UserPromptSubmit`'s `additionalContext`.
|
|
859
1014
|
*
|
|
@@ -880,9 +1035,14 @@ export class ScenarioReplayPlan {
|
|
|
880
1035
|
// A sub-task plan (armed mid-task) never tells the agent to stop calling
|
|
881
1036
|
// tools: the steps below are a part of the task it is already doing.
|
|
882
1037
|
const subTask = opts?.subTask ?? this.subTask;
|
|
1038
|
+
// The intent and each step's reasoning were written from the recording,
|
|
1039
|
+
// so they name its values ("set Olga up"); shown here, they say this
|
|
1040
|
+
// request's — or the value's role — never another request's (B2 is11.md).
|
|
1041
|
+
const fix = this.rewriter();
|
|
883
1042
|
// The intent is a paragraph the analyser wrote; it is context, not an
|
|
884
1043
|
// instruction, so it is clipped rather than dumped whole into the prompt.
|
|
885
|
-
const
|
|
1044
|
+
const written = fix(this.intent);
|
|
1045
|
+
const intent = written.length > 240 ? `${written.slice(0, 240)}…` : written;
|
|
886
1046
|
if (this.mode === "direct") {
|
|
887
1047
|
// The steps are not listed. The model does not call them, and what the
|
|
888
1048
|
// tool returns names every call and says whether the run was whole or
|
|
@@ -890,9 +1050,17 @@ export class ScenarioReplayPlan {
|
|
|
890
1050
|
// cost the agent up to 2,000 characters in the first call of every turn
|
|
891
1051
|
// a plan was armed for (docs/calculatedReplay.md, "How a bundle reads"),
|
|
892
1052
|
// and the tool's result shows an operator watching the session what ran.
|
|
1053
|
+
//
|
|
1054
|
+
// One line says where the steps' values come from, with this request's
|
|
1055
|
+
// own: a model that met another request's value in the note used to
|
|
1056
|
+
// stop to check, and its check retired the plan (B2 is11.md).
|
|
1057
|
+
const values = this.requestValues();
|
|
893
1058
|
return [
|
|
894
1059
|
"[BaseInstRunner calculated replay] A known-good tool sequence was recorded",
|
|
895
|
-
`for this
|
|
1060
|
+
`for a request like this one${intent ? ` (intent: ${intent})` : ""}.`,
|
|
1061
|
+
values
|
|
1062
|
+
? `Its steps take this request's values, not the recording's: ${values}.`
|
|
1063
|
+
: "Its steps take this request's values, not the recording's.",
|
|
896
1064
|
"",
|
|
897
1065
|
`Call ${directToolName} once, with no arguments, before any other tool. It runs the`,
|
|
898
1066
|
"sequence and returns what each call did.",
|
|
@@ -924,7 +1092,7 @@ export class ScenarioReplayPlan {
|
|
|
924
1092
|
return ` (only when ${t.text ?? "this request needs it"})`;
|
|
925
1093
|
};
|
|
926
1094
|
const listing = (withWhy, argsBefore) => planned.map((s, i) => {
|
|
927
|
-
const why = withWhy ? clipReasoning(s.reasoning) : "";
|
|
1095
|
+
const why = withWhy ? clipReasoning(fix(s.reasoning ?? "")) : "";
|
|
928
1096
|
const head = ` ${i + 1}. ${s.toolName}${onlyWhen(i)}${why ? ` — ${why}` : ""}`;
|
|
929
1097
|
const shown = i < argsBefore ? args[i] : undefined;
|
|
930
1098
|
return shown ? `${head}\n ${shown}` : head;
|
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
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reads-only — whether one call of the agent's only looks, and changes nothing.
|
|
3
|
+
*
|
|
4
|
+
* WHY THE RUNNER ASKS (B2 is11.md, 2026-11-11). A direct plan is announced
|
|
5
|
+
* with "call run_scenario before any other tool", and a first call that is
|
|
6
|
+
* not `run_scenario` used to retire it on the spot (control/server.ts). The
|
|
7
|
+
* rule is there for one danger: a model that has no `run_scenario` makes its
|
|
8
|
+
* own calls, and a divergence would then run every step, writes included,
|
|
9
|
+
* inside a hook nobody approves. But the call that retired the plan in is11
|
|
10
|
+
* was the agent reading its own notes file: it had seen another person's name
|
|
11
|
+
* in the note (Olga, the recording's; the request said Carlos) and checked
|
|
12
|
+
* first. The plan would have done the task right — its steps take the person
|
|
13
|
+
* from the request — and the turn cost a full agent run.
|
|
14
|
+
*
|
|
15
|
+
* So a call that only looks no longer retires a direct plan: it runs as the
|
|
16
|
+
* agent wrote it, and the plan waits, still announced, for `run_scenario`.
|
|
17
|
+
* The first call that may change something still retires it, running nothing
|
|
18
|
+
* (and nothing ever runs in a hook in its place): a plan run after the agent
|
|
19
|
+
* changed something itself could do the same work twice.
|
|
20
|
+
*
|
|
21
|
+
* WHEN IN DOUBT, IT IS A CHANGE. A call judged "only looks" that does change
|
|
22
|
+
* something leaves a plan armed that could then repeat that change; a change
|
|
23
|
+
* judged a change only ends a replay early, as every call did before. So:
|
|
24
|
+
*
|
|
25
|
+
* - Claude Code's built-ins by name: the reading ones only look, every other
|
|
26
|
+
* built-in (a sub-agent, an edit, an unknown new tool) is a change;
|
|
27
|
+
* - `Bash` by its command ({@link commandReadsOnly}), and `PowerShell` never;
|
|
28
|
+
* - an MCP tool by what its own server says of it (`annotations`), else by
|
|
29
|
+
* its name: a reading word and no writing word.
|
|
30
|
+
*
|
|
31
|
+
* The same rule as the service's `readsOnly` (correctionCheck.ts, AC13), kept
|
|
32
|
+
* in step by hand, stricter where a wrong "only looks" costs more here: a
|
|
33
|
+
* command substitution, a process substitution or an `awk` `system(` makes a
|
|
34
|
+
* command a change.
|
|
35
|
+
*/
|
|
36
|
+
import type { ToolAnnotations } from "../record/recorder.js";
|
|
37
|
+
/**
|
|
38
|
+
* Whether this call only looks. `toolInput` is the hook's `tool_input` (only a
|
|
39
|
+
* `Bash` call's `command` is read from it); `hints` is what the tool's own
|
|
40
|
+
* server listed for it, when it listed anything.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readsOnlyCall(toolName: string, toolInput?: unknown, hints?: ToolAnnotations): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Whether a shell command only looks: every part of it is a looking command,
|
|
45
|
+
* nothing is written to a file, and nothing runs code it does not show.
|
|
46
|
+
*/
|
|
47
|
+
export declare function commandReadsOnly(command: string): boolean;
|
|
48
|
+
//# sourceMappingURL=reads-only.d.ts.map
|