@mnemom/mnemom 0.17.38-next.1 → 0.17.38-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent-run.d.ts +10 -2
- package/dist/commands/agent-run.js +18 -7
- package/dist/commands/scan.d.ts +1 -1
- package/dist/commands/scan.js +7 -14
- package/dist/index.js +1 -0
- package/dist/lib/runner/executor-client.d.ts +6 -0
- package/dist/lib/runner/executor-client.js +6 -0
- package/dist/lib/runner/job.d.ts +12 -3
- package/dist/lib/runner/job.js +30 -2
- package/dist/lib/runner/loop.d.ts +7 -2
- package/dist/lib/runner/loop.js +15 -3
- package/dist/lib/runner/options.d.ts +7 -0
- package/dist/lib/runner/options.js +11 -0
- package/dist/lib/scan/caps.d.ts +1 -35
- package/dist/lib/scan/caps.js +2 -67
- package/dist/lib/scan/cost-per-pr.d.ts +1 -93
- package/dist/lib/scan/cost-per-pr.js +2 -101
- package/dist/lib/scan/credential.d.ts +2 -26
- package/dist/lib/scan/credential.js +5 -58
- package/dist/lib/scan/failure.d.ts +1 -10
- package/dist/lib/scan/failure.js +2 -43
- package/dist/lib/scan/pr-source.d.ts +1 -57
- package/dist/lib/scan/pr-source.js +2 -132
- package/dist/lib/scan/report-email.d.ts +7 -104
- package/dist/lib/scan/report-email.js +25 -97
- package/dist/lib/scan/scan.d.ts +1 -69
- package/dist/lib/scan/scan.js +2 -195
- package/dist/lib/scan/share.d.ts +3 -1
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +1 -6
- package/dist/lib/scan/spend/ai-tool-detect.js +2 -18
- package/dist/lib/scan/spend/anthropic-usage.d.ts +1 -76
- package/dist/lib/scan/spend/anthropic-usage.js +2 -269
- package/dist/lib/scan/spend/prices.d.ts +1 -33
- package/dist/lib/scan/spend/prices.js +2 -57
- package/dist/lib/scan/spend/spend-estimate.d.ts +1 -30
- package/dist/lib/scan/spend/spend-estimate.js +2 -70
- package/dist/lib/scan/spend/spend-tiers.d.ts +1 -16
- package/dist/lib/scan/spend/spend-tiers.js +6 -26
- package/dist/lib/scan/spend/token-rate.d.ts +1 -30
- package/dist/lib/scan/spend/token-rate.js +2 -54
- package/dist/vendor/durable-contract/index.js +19 -2
- package/dist/vendor/shared/durable-contract/cli-surface.js +2 -1
- package/dist/vendor/shared/durable-contract/codes.d.ts +12 -0
- package/dist/vendor/shared/durable-contract/codes.js +11 -0
- package/dist/vendor/shared/durable-contract/constants.d.ts +7 -1
- package/dist/vendor/shared/durable-contract/constants.js +7 -1
- package/dist/vendor/shared/durable-contract/doors.d.ts +9 -1
- package/dist/vendor/shared/durable-contract/doors.js +13 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +1 -1
- package/dist/vendor/shared/durable-contract/schema.d.ts +9 -0
- package/dist/vendor/shared/durable-contract/schema.js +11 -0
- package/dist/vendor/shared/org-scan-core/caps.d.ts +35 -0
- package/dist/vendor/shared/org-scan-core/caps.js +68 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.d.ts +93 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.js +101 -0
- package/dist/vendor/shared/org-scan-core/credential.d.ts +43 -0
- package/dist/vendor/shared/org-scan-core/credential.js +74 -0
- package/dist/vendor/shared/org-scan-core/failure.d.ts +10 -0
- package/dist/vendor/shared/org-scan-core/failure.js +43 -0
- package/dist/vendor/shared/org-scan-core/index.d.ts +18 -0
- package/dist/vendor/shared/org-scan-core/index.js +19 -0
- package/dist/vendor/shared/org-scan-core/pr-source.d.ts +61 -0
- package/dist/vendor/shared/org-scan-core/pr-source.js +138 -0
- package/dist/vendor/shared/org-scan-core/report-build.d.ts +120 -0
- package/dist/vendor/shared/org-scan-core/report-build.js +119 -0
- package/dist/vendor/shared/org-scan-core/scan.d.ts +75 -0
- package/dist/vendor/shared/org-scan-core/scan.js +202 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.d.ts +6 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.js +18 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.d.ts +76 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.js +270 -0
- package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.js +1 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.d.ts +33 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.js +58 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.js +71 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.d.ts +16 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.js +26 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.js +55 -0
- package/package.json +1 -1
- /package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.d.ts +0 -0
|
@@ -1,269 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// context_window, and cost_report) with the server-only parts (egress allowlist) dropped.
|
|
4
|
-
//
|
|
5
|
-
// MNE-9706: the reads take either credential, as the server's credential-source.ts does: the pasted Admin
|
|
6
|
-
// key (x-api-key) or a short-lived WIF access token (Authorization: Bearer, see anthropic-wif.ts). Either
|
|
7
|
-
// one is sent ONLY to the two report paths below; walk() refuses any other path (the fence, design T18:
|
|
8
|
-
// Anthropic's WIF scope for these reports is org:admin, so the fence has to live here).
|
|
9
|
-
//
|
|
10
|
-
// The key is held in memory for the length of the two reads. It is never stored, logged, written to disk,
|
|
11
|
-
// echoed, put in argv, a URL or an error message, and is sent only to api.anthropic.com (a constant, not
|
|
12
|
-
// configurable) in the x-api-key header. Every failure surfaces as a fixed sentence: the thrown value, the
|
|
13
|
-
// response body and the key never reach the terminal.
|
|
14
|
-
export const ANTHROPIC_ADMIN_ORIGIN = "https://api.anthropic.com";
|
|
15
|
-
export const ADMIN_KEY_ENV = "ANTHROPIC_ADMIN_KEY";
|
|
16
|
-
const ANTHROPIC_VERSION = "2023-06-01";
|
|
17
|
-
const USAGE_PATH = "/v1/organizations/usage_report/messages";
|
|
18
|
-
const COST_PATH = "/v1/organizations/cost_report";
|
|
19
|
-
/** The only paths a usage credential is ever sent to (design T18: fenced to report endpoints). */
|
|
20
|
-
export const REPORT_PATHS = [USAGE_PATH, COST_PATH];
|
|
21
|
-
/** The fence, as one exported check so it is directly testable. Throws AnthropicUsageError for any other path. */
|
|
22
|
-
export function assertReportPath(path) {
|
|
23
|
-
if (!REPORT_PATHS.includes(path))
|
|
24
|
-
throw new AnthropicUsageError("malformed");
|
|
25
|
-
}
|
|
26
|
-
export const DEFAULT_MAX_PAGES = 20;
|
|
27
|
-
/** One shared deadline for a request including its retries (not N x timeout). */
|
|
28
|
-
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
29
|
-
export const DEFAULT_MAX_RETRIES = 2;
|
|
30
|
-
const MAX_RETRY_WAIT_MS = 5_000;
|
|
31
|
-
const ADMIN_KEY_RE = /^sk-ant-admin[A-Za-z0-9_-]{8,}$/;
|
|
32
|
-
/** Shape check only; the key is never echoed, even on a mismatch. */
|
|
33
|
-
export const isAdminKeyShape = (k) => typeof k === "string" && ADMIN_KEY_RE.test(k.trim());
|
|
34
|
-
const FAILURE_TEXT = {
|
|
35
|
-
key_invalid: "The Anthropic admin key is not an Admin API key (it should start with sk-ant-admin).",
|
|
36
|
-
key_refused: "Anthropic refused the admin key (401/403).",
|
|
37
|
-
token_refused: "Anthropic refused the workload identity token for the usage reports (401/403). The federation rule needs scope org:admin.",
|
|
38
|
-
rate_limited: "Anthropic rate limit reached.",
|
|
39
|
-
timeout: "Anthropic did not answer in time.",
|
|
40
|
-
network: "Anthropic was unreachable.",
|
|
41
|
-
upstream: "Anthropic usage could not be read.",
|
|
42
|
-
malformed: "Anthropic returned usage in a shape this CLI does not recognise.",
|
|
43
|
-
too_large: "Anthropic usage for this period is too large to read completely.",
|
|
44
|
-
};
|
|
45
|
-
/** Fixed sentence for a failure reason. */
|
|
46
|
-
export const usageFailureText = (r) => FAILURE_TEXT[r];
|
|
47
|
-
/** Carries only a reason tag. Its message is the fixed sentence: nothing upstream can reach it. */
|
|
48
|
-
export class AnthropicUsageError extends Error {
|
|
49
|
-
reason;
|
|
50
|
-
constructor(reason) {
|
|
51
|
-
super(FAILURE_TEXT[reason]);
|
|
52
|
-
this.reason = reason;
|
|
53
|
-
this.name = "AnthropicUsageError";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const bad = () => {
|
|
57
|
-
throw new AnthropicUsageError("malformed");
|
|
58
|
-
};
|
|
59
|
-
const rec = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : bad();
|
|
60
|
-
function reqNum(r, k) {
|
|
61
|
-
const v = r[k];
|
|
62
|
-
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : bad();
|
|
63
|
-
}
|
|
64
|
-
/** Optional counter inside an optional object: absent is 0, present-but-wrong is an error. */
|
|
65
|
-
function optNum(o, k) {
|
|
66
|
-
if (o === undefined || o === null)
|
|
67
|
-
return 0;
|
|
68
|
-
if (typeof o !== "object")
|
|
69
|
-
return bad();
|
|
70
|
-
const v = o[k];
|
|
71
|
-
if (v === undefined || v === null)
|
|
72
|
-
return 0;
|
|
73
|
-
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : bad();
|
|
74
|
-
}
|
|
75
|
-
export function parseUsageRow(raw) {
|
|
76
|
-
const r = rec(raw);
|
|
77
|
-
const cw = r.context_window;
|
|
78
|
-
if (cw !== null && cw !== undefined && cw !== "0-200k" && cw !== "200k-1M")
|
|
79
|
-
bad();
|
|
80
|
-
return {
|
|
81
|
-
tokens: reqNum(r, "uncached_input_tokens") +
|
|
82
|
-
reqNum(r, "cache_read_input_tokens") +
|
|
83
|
-
reqNum(r, "output_tokens") +
|
|
84
|
-
optNum(r.cache_creation, "ephemeral_1h_input_tokens") +
|
|
85
|
-
optNum(r.cache_creation, "ephemeral_5m_input_tokens"),
|
|
86
|
-
contextWindow: (cw ?? null),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
const CENT = /^-?\d+(\.\d+)?$/;
|
|
90
|
-
export function parseCostRow(raw) {
|
|
91
|
-
const r = rec(raw);
|
|
92
|
-
if (typeof r.amount !== "string" || !CENT.test(r.amount))
|
|
93
|
-
bad();
|
|
94
|
-
if (typeof r.currency !== "string" || r.currency === "")
|
|
95
|
-
bad();
|
|
96
|
-
return { cents: Number(r.amount), currency: r.currency };
|
|
97
|
-
}
|
|
98
|
-
const DAY_MS = 86_400_000;
|
|
99
|
-
/** Buckets are UTC-aligned: floor so a mid-day start reads the whole bucket it falls in. */
|
|
100
|
-
export function alignStartingAt(startingAt) {
|
|
101
|
-
const t = Date.parse(startingAt);
|
|
102
|
-
if (!Number.isFinite(t))
|
|
103
|
-
return startingAt;
|
|
104
|
-
return new Date(Math.floor(t / DAY_MS) * DAY_MS).toISOString().replace(".000Z", "Z");
|
|
105
|
-
}
|
|
106
|
-
const backoff = (attempt) => Math.min(250 * 2 ** (attempt - 1), MAX_RETRY_WAIT_MS);
|
|
107
|
-
function retryWait(res, attempt) {
|
|
108
|
-
const ra = Number(res.headers.get("retry-after"));
|
|
109
|
-
return Number.isFinite(ra) && ra > 0 ? ra * 1000 : backoff(attempt);
|
|
110
|
-
}
|
|
111
|
-
class Reader {
|
|
112
|
-
auth;
|
|
113
|
-
fetchImpl;
|
|
114
|
-
sleep;
|
|
115
|
-
maxPages;
|
|
116
|
-
timeoutMs;
|
|
117
|
-
maxRetries;
|
|
118
|
-
constructor(auth, deps) {
|
|
119
|
-
this.auth = auth;
|
|
120
|
-
this.fetchImpl = deps.fetchImpl ?? ((...a) => fetch(...a));
|
|
121
|
-
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
122
|
-
this.maxPages = deps.maxPages ?? DEFAULT_MAX_PAGES;
|
|
123
|
-
this.timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
124
|
-
this.maxRetries = deps.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
125
|
-
}
|
|
126
|
-
async walk(path, base, parse, callMaxPages) {
|
|
127
|
-
// The fence: the credential goes to the two report paths and nowhere else.
|
|
128
|
-
assertReportPath(path);
|
|
129
|
-
const ceiling = typeof callMaxPages === "number" && Number.isFinite(callMaxPages) && callMaxPages >= 1
|
|
130
|
-
? Math.min(this.maxPages, Math.floor(callMaxPages))
|
|
131
|
-
: this.maxPages;
|
|
132
|
-
const rows = [];
|
|
133
|
-
let page = null;
|
|
134
|
-
let pages = 0;
|
|
135
|
-
for (;;) {
|
|
136
|
-
const params = new URLSearchParams(base);
|
|
137
|
-
if (page)
|
|
138
|
-
params.set("page", page);
|
|
139
|
-
const body = await this.request(`${ANTHROPIC_ADMIN_ORIGIN}${path}?${params.toString()}`);
|
|
140
|
-
pages += 1;
|
|
141
|
-
if (!Array.isArray(body.data))
|
|
142
|
-
bad();
|
|
143
|
-
for (const raw of body.data) {
|
|
144
|
-
const b = rec(raw);
|
|
145
|
-
if (typeof b.starting_at !== "string" || typeof b.ending_at !== "string")
|
|
146
|
-
bad();
|
|
147
|
-
if (!Array.isArray(b.results))
|
|
148
|
-
bad();
|
|
149
|
-
for (const r of b.results)
|
|
150
|
-
rows.push(parse(r));
|
|
151
|
-
}
|
|
152
|
-
const more = body.has_more;
|
|
153
|
-
if (more !== undefined && more !== null && typeof more !== "boolean")
|
|
154
|
-
bad();
|
|
155
|
-
if (more !== true)
|
|
156
|
-
return { rows, truncated: false, pages };
|
|
157
|
-
if (typeof body.next_page !== "string" || body.next_page === "")
|
|
158
|
-
bad();
|
|
159
|
-
if (pages >= ceiling)
|
|
160
|
-
return { rows, truncated: true, pages };
|
|
161
|
-
page = body.next_page;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
async request(url) {
|
|
165
|
-
const deadline = AbortSignal.timeout(this.timeoutMs);
|
|
166
|
-
for (let attempt = 0;;) {
|
|
167
|
-
let res;
|
|
168
|
-
try {
|
|
169
|
-
res = await this.fetchImpl(url, {
|
|
170
|
-
method: "GET",
|
|
171
|
-
headers: {
|
|
172
|
-
[this.auth.header]: this.auth.value,
|
|
173
|
-
"anthropic-version": ANTHROPIC_VERSION,
|
|
174
|
-
accept: "application/json",
|
|
175
|
-
},
|
|
176
|
-
// A redirect would carry the x-api-key header to wherever it points: refuse to follow one.
|
|
177
|
-
redirect: "error",
|
|
178
|
-
signal: deadline,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
// Never surface the thrown value: it can carry the request.
|
|
183
|
-
if (deadline.aborted)
|
|
184
|
-
throw new AnthropicUsageError("timeout");
|
|
185
|
-
if (attempt++ < this.maxRetries) {
|
|
186
|
-
await this.sleep(backoff(attempt));
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
throw new AnthropicUsageError("network");
|
|
190
|
-
}
|
|
191
|
-
if (res.ok) {
|
|
192
|
-
let json;
|
|
193
|
-
try {
|
|
194
|
-
json = await res.json();
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
throw new AnthropicUsageError("upstream");
|
|
198
|
-
}
|
|
199
|
-
return rec(json);
|
|
200
|
-
}
|
|
201
|
-
await res.body?.cancel().catch(() => { });
|
|
202
|
-
if (res.status === 401 || res.status === 403)
|
|
203
|
-
throw new AnthropicUsageError(this.auth.header === "x-api-key" ? "key_refused" : "token_refused");
|
|
204
|
-
const retryable = res.status === 429 || res.status >= 500;
|
|
205
|
-
const wait = retryable ? retryWait(res, attempt + 1) : 0;
|
|
206
|
-
if (retryable &&
|
|
207
|
-
!deadline.aborted &&
|
|
208
|
-
wait <= MAX_RETRY_WAIT_MS &&
|
|
209
|
-
attempt++ < this.maxRetries) {
|
|
210
|
-
await this.sleep(wait);
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
throw new AnthropicUsageError(res.status === 429 ? "rate_limited" : "upstream");
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
const validDay = (d) => /^\d{4}-\d{2}-\d{2}$/.test(d) && Number.isFinite(Date.parse(`${d}T00:00:00Z`));
|
|
218
|
-
/**
|
|
219
|
-
* The same two reads mnemom-api made: usage grouped by context_window (the exact >200k share) and cost
|
|
220
|
-
* (total spend). The key is shape-checked before any call so a regular API key never leaves the machine.
|
|
221
|
-
* Throws AnthropicUsageError only. A walk stopped by the page cap is an error, never a short total.
|
|
222
|
-
*/
|
|
223
|
-
export async function fetchMeasuredUsage(key, period, deps = {}) {
|
|
224
|
-
const k = key?.trim();
|
|
225
|
-
if (!k || !isAdminKeyShape(k))
|
|
226
|
-
throw new AnthropicUsageError("key_invalid");
|
|
227
|
-
return fetchMeasuredUsageWithAuth({ header: "x-api-key", value: k }, period, deps);
|
|
228
|
-
}
|
|
229
|
-
/** A WIF access token as the bearer for the two report reads. Printable ASCII only, else key_invalid. */
|
|
230
|
-
export function bearerAuth(token) {
|
|
231
|
-
if (!/^[\x21-\x7e]{8,4096}$/.test(token))
|
|
232
|
-
throw new AnthropicUsageError("key_invalid");
|
|
233
|
-
return { header: "authorization", value: `Bearer ${token}` };
|
|
234
|
-
}
|
|
235
|
-
/** The two reads with an already-resolved credential (pasted key or WIF bearer). Throws AnthropicUsageError only. */
|
|
236
|
-
export async function fetchMeasuredUsageWithAuth(auth, period, deps = {}) {
|
|
237
|
-
if (!validDay(period.start) || !validDay(period.end))
|
|
238
|
-
throw new AnthropicUsageError("malformed");
|
|
239
|
-
const startingAt = alignStartingAt(`${period.start}T00:00:00Z`);
|
|
240
|
-
const endingAt = new Date(Date.parse(`${period.end}T00:00:00Z`) + DAY_MS).toISOString();
|
|
241
|
-
const reader = new Reader(auth, deps);
|
|
242
|
-
const up = new URLSearchParams({
|
|
243
|
-
starting_at: startingAt,
|
|
244
|
-
ending_at: endingAt,
|
|
245
|
-
bucket_width: "1d",
|
|
246
|
-
});
|
|
247
|
-
up.append("group_by[]", "context_window");
|
|
248
|
-
const usage = await reader.walk(USAGE_PATH, up, parseUsageRow);
|
|
249
|
-
if (usage.truncated)
|
|
250
|
-
throw new AnthropicUsageError("too_large");
|
|
251
|
-
const cp = new URLSearchParams({ starting_at: startingAt, ending_at: endingAt });
|
|
252
|
-
const cost = await reader.walk(COST_PATH, cp, parseCostRow);
|
|
253
|
-
if (cost.truncated)
|
|
254
|
-
throw new AnthropicUsageError("too_large");
|
|
255
|
-
let total = 0;
|
|
256
|
-
let over = 0;
|
|
257
|
-
for (const r of usage.rows) {
|
|
258
|
-
total += r.tokens;
|
|
259
|
-
if (r.contextWindow === "200k-1M")
|
|
260
|
-
over += r.tokens;
|
|
261
|
-
}
|
|
262
|
-
let cents = 0;
|
|
263
|
-
for (const r of cost.rows) {
|
|
264
|
-
if (r.currency !== "USD")
|
|
265
|
-
throw new AnthropicUsageError("malformed");
|
|
266
|
-
cents += r.cents;
|
|
267
|
-
}
|
|
268
|
-
return { usd: Math.round(cents) / 100, over200kShare: total > 0 ? over / total : null };
|
|
269
|
-
}
|
|
1
|
+
// Moved to shared/org-scan-core (MNE-9752); vendored at cli/src/vendor/shared/org-scan-core. Re-exported so CLI imports stay put.
|
|
2
|
+
export * from "../../../vendor/shared/org-scan-core/spend/anthropic-usage.js";
|
|
@@ -1,33 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export interface TokenPrice {
|
|
3
|
-
/** USD per 1M input tokens. */
|
|
4
|
-
inputPerMTok: number;
|
|
5
|
-
/** USD per 1M output tokens. */
|
|
6
|
-
outputPerMTok: number;
|
|
7
|
-
model: string;
|
|
8
|
-
}
|
|
9
|
-
export interface PriceBand {
|
|
10
|
-
low: TokenPrice;
|
|
11
|
-
high: TokenPrice;
|
|
12
|
-
/** Page the prices were read from. */
|
|
13
|
-
source: string;
|
|
14
|
-
}
|
|
15
|
-
export declare const PRICES_AS_OF = "2026-10-28";
|
|
16
|
-
export declare const TOOL_PRICES: Record<Exclude<AiTool, "copilot">, PriceBand>;
|
|
17
|
-
/**
|
|
18
|
-
* GitHub Copilot bills per premium request, not per token
|
|
19
|
-
* (https://docs.github.com/en/copilot/concepts/billing/copilot-requests),
|
|
20
|
-
* so it has no token price of its own. Its agent routes to the other vendors' models, so its band is
|
|
21
|
-
* the union of the other three: cheapest low to dearest high. An assumption, stated here once.
|
|
22
|
-
*/
|
|
23
|
-
export declare function copilotBand(): PriceBand;
|
|
24
|
-
/**
|
|
25
|
-
* Share of billed tokens that are input (the rest output). Agent loops re-read large context and
|
|
26
|
-
* write little, so input dominates. An ASSUMPTION used both to turn our known run cost into
|
|
27
|
-
* tokens-per-line and to price those tokens for each tool; it cancels to first order when the
|
|
28
|
-
* tool's input/output price ratio matches ours, and is the main source of the range's width.
|
|
29
|
-
*/
|
|
30
|
-
export declare const INPUT_SHARE = 0.9;
|
|
31
|
-
export declare function bandFor(tool: AiTool): PriceBand;
|
|
32
|
-
/** USD per token at the assumed input/output mix. */
|
|
33
|
-
export declare function blendedUsdPerToken(p: TokenPrice): number;
|
|
1
|
+
export * from "../../../vendor/shared/org-scan-core/spend/prices.js";
|
|
@@ -1,57 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// USD per million tokens (input / output), standard (non-batch) list price. A band is the cheapest
|
|
4
|
-
// and the dearest model a tool's coding agent plausibly runs on, so the estimate is a RANGE.
|
|
5
|
-
// Re-read the pages and bump PRICES_AS_OF when they move; a stale price widens nothing silently,
|
|
6
|
-
// it just shifts the range, which is why the date ships with the estimate's method text.
|
|
7
|
-
export const PRICES_AS_OF = "2026-10-28";
|
|
8
|
-
export const TOOL_PRICES = {
|
|
9
|
-
claude_code: {
|
|
10
|
-
source: "https://platform.claude.com/docs/en/about-claude/pricing",
|
|
11
|
-
low: { model: "Claude Sonnet 5.5", inputPerMTok: 2, outputPerMTok: 10 },
|
|
12
|
-
high: { model: "Claude Opus 5", inputPerMTok: 5, outputPerMTok: 25 },
|
|
13
|
-
},
|
|
14
|
-
codex: {
|
|
15
|
-
source: "https://developers.openai.com/api/docs/pricing",
|
|
16
|
-
low: { model: "gpt-5.3-codex (standard)", inputPerMTok: 1.75, outputPerMTok: 14 },
|
|
17
|
-
high: { model: "gpt-5.3-codex (fast)", inputPerMTok: 3.5, outputPerMTok: 28 },
|
|
18
|
-
},
|
|
19
|
-
gemini: {
|
|
20
|
-
source: "https://ai.google.dev/gemini-api/docs/pricing",
|
|
21
|
-
// Gemini 3.8 Flash is $0.75/$3.75 through 2026-12-31 and $1.50/$7.50 from 2027-01-01; the
|
|
22
|
-
// post-promotion price is used so the lower bound does not expire in two months.
|
|
23
|
-
low: { model: "Gemini 3.8 Flash (from 2027-01-01)", inputPerMTok: 1.5, outputPerMTok: 7.5 },
|
|
24
|
-
high: { model: "Gemini 3.1 Pro (prompts up to 200k)", inputPerMTok: 2, outputPerMTok: 12 },
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* GitHub Copilot bills per premium request, not per token
|
|
29
|
-
* (https://docs.github.com/en/copilot/concepts/billing/copilot-requests),
|
|
30
|
-
* so it has no token price of its own. Its agent routes to the other vendors' models, so its band is
|
|
31
|
-
* the union of the other three: cheapest low to dearest high. An assumption, stated here once.
|
|
32
|
-
*/
|
|
33
|
-
export function copilotBand() {
|
|
34
|
-
const bands = Object.values(TOOL_PRICES);
|
|
35
|
-
const blend = (p) => p.inputPerMTok * INPUT_SHARE + p.outputPerMTok * (1 - INPUT_SHARE);
|
|
36
|
-
const low = bands.map((b) => b.low).sort((a, b) => blend(a) - blend(b))[0];
|
|
37
|
-
const high = bands.map((b) => b.high).sort((a, b) => blend(b) - blend(a))[0];
|
|
38
|
-
return {
|
|
39
|
-
low,
|
|
40
|
-
high,
|
|
41
|
-
source: "union of the claude_code, codex and gemini bands (per-request billing)",
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Share of billed tokens that are input (the rest output). Agent loops re-read large context and
|
|
46
|
-
* write little, so input dominates. An ASSUMPTION used both to turn our known run cost into
|
|
47
|
-
* tokens-per-line and to price those tokens for each tool; it cancels to first order when the
|
|
48
|
-
* tool's input/output price ratio matches ours, and is the main source of the range's width.
|
|
49
|
-
*/
|
|
50
|
-
export const INPUT_SHARE = 0.9;
|
|
51
|
-
export function bandFor(tool) {
|
|
52
|
-
return tool === "copilot" ? copilotBand() : TOOL_PRICES[tool];
|
|
53
|
-
}
|
|
54
|
-
/** USD per token at the assumed input/output mix. */
|
|
55
|
-
export function blendedUsdPerToken(p) {
|
|
56
|
-
return (p.inputPerMTok * INPUT_SHARE + p.outputPerMTok * (1 - INPUT_SHARE)) / 1_000_000;
|
|
57
|
-
}
|
|
1
|
+
// Moved to shared/org-scan-core (MNE-9752); vendored at cli/src/vendor/shared/org-scan-core. Re-exported so CLI imports stay put.
|
|
2
|
+
export * from "../../../vendor/shared/org-scan-core/spend/prices.js";
|
|
@@ -1,30 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { type TokenRate } from "./token-rate.js";
|
|
3
|
-
/** AI-assisted PRs needed before a range is worth printing. */
|
|
4
|
-
export declare const MIN_AI_PRS = 10;
|
|
5
|
-
/** Per-PR input. `markText` is commit messages / PR body, read for tool marks and dropped. */
|
|
6
|
-
export interface SpendPrInput {
|
|
7
|
-
additions: number;
|
|
8
|
-
deletions: number;
|
|
9
|
-
markText: string;
|
|
10
|
-
/** Tools already detected inside the PR source (so the body never leaves it). Wins over markText. */
|
|
11
|
-
tools?: AiTool[];
|
|
12
|
-
}
|
|
13
|
-
export type NoSpendReason = "too_few_ai_prs" | "no_rate" | "invalid_input";
|
|
14
|
-
export type SpendEstimate = {
|
|
15
|
-
kind: "range";
|
|
16
|
-
lowUsd: number;
|
|
17
|
-
highUsd: number;
|
|
18
|
-
aiPrs: number;
|
|
19
|
-
scannedPrs: number;
|
|
20
|
-
byTool: Partial<Record<AiTool, number>>;
|
|
21
|
-
/** One line, labelled estimate, passes claim-lint. */
|
|
22
|
-
line: string;
|
|
23
|
-
} | {
|
|
24
|
-
kind: "none";
|
|
25
|
-
reason: NoSpendReason;
|
|
26
|
-
aiPrs: number;
|
|
27
|
-
scannedPrs: number;
|
|
28
|
-
line: string;
|
|
29
|
-
};
|
|
30
|
-
export declare function estimateSpend(prs: unknown, rate?: TokenRate | null): SpendEstimate;
|
|
1
|
+
export * from "../../../vendor/shared/org-scan-core/spend/spend-estimate.js";
|
|
@@ -1,70 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
// No line is produced when there are too few AI-assisted PRs to price (MIN_AI_PRS). That is a
|
|
5
|
-
// data-sufficiency threshold on the evidence behind the range, nothing to do with how large a
|
|
6
|
-
// reporting cohort is. Default-OFF behind ORG_SCAN_MODE via estimateSpendIfEnabled.
|
|
7
|
-
import { AI_TOOLS, detectAiTools } from "./ai-tool-detect.js";
|
|
8
|
-
import { PRICES_AS_OF, bandFor, blendedUsdPerToken } from "./prices.js";
|
|
9
|
-
import { DERIVED_TOKEN_RATE } from "./token-rate.js";
|
|
10
|
-
/** AI-assisted PRs needed before a range is worth printing. */
|
|
11
|
-
export const MIN_AI_PRS = 10;
|
|
12
|
-
/** Fixed strings only: a failure never carries input or an error message into the output. */
|
|
13
|
-
const NONE_LINES = {
|
|
14
|
-
too_few_ai_prs: "No spend estimate: too few AI-assisted PRs in this scan to price a range.",
|
|
15
|
-
no_rate: "No spend estimate: the cost reference data is not sufficient to price a range.",
|
|
16
|
-
invalid_input: "No spend estimate: the PR counts could not be read.",
|
|
17
|
-
};
|
|
18
|
-
const isCount = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
19
|
-
function none(reason, aiPrs, scannedPrs) {
|
|
20
|
-
return { kind: "none", reason, aiPrs, scannedPrs, line: NONE_LINES[reason] };
|
|
21
|
-
}
|
|
22
|
-
function validPr(p) {
|
|
23
|
-
if (typeof p !== "object" || p === null)
|
|
24
|
-
return false;
|
|
25
|
-
const o = p;
|
|
26
|
-
return (isCount(o.additions) &&
|
|
27
|
-
isCount(o.deletions) &&
|
|
28
|
-
typeof o.markText === "string" &&
|
|
29
|
-
(o.tools === undefined ||
|
|
30
|
-
(Array.isArray(o.tools) && o.tools.every((t) => AI_TOOLS.includes(t)))));
|
|
31
|
-
}
|
|
32
|
-
/** Two significant figures so the range does not claim precision it lacks ($2.4, not $2; 0 stays $0). */
|
|
33
|
-
function usd(n) {
|
|
34
|
-
return `$${n.toLocaleString("en-US", { maximumSignificantDigits: 2 })}`;
|
|
35
|
-
}
|
|
36
|
-
export function estimateSpend(prs, rate = DERIVED_TOKEN_RATE) {
|
|
37
|
-
if (!Array.isArray(prs))
|
|
38
|
-
return none("invalid_input", 0, 0);
|
|
39
|
-
const scanned = prs.length;
|
|
40
|
-
if (!prs.every(validPr))
|
|
41
|
-
return none("invalid_input", 0, scanned);
|
|
42
|
-
let aiPrs = 0;
|
|
43
|
-
let low = 0;
|
|
44
|
-
let high = 0;
|
|
45
|
-
const byTool = {};
|
|
46
|
-
for (const p of prs) {
|
|
47
|
-
const tools = p.tools ?? detectAiTools(p.markText);
|
|
48
|
-
if (tools.length === 0)
|
|
49
|
-
continue;
|
|
50
|
-
aiPrs += 1;
|
|
51
|
-
for (const t of tools)
|
|
52
|
-
byTool[t] = (byTool[t] ?? 0) + 1;
|
|
53
|
-
if (!rate)
|
|
54
|
-
continue;
|
|
55
|
-
const lines = p.additions + p.deletions;
|
|
56
|
-
// A PR carrying several marks is priced once, across the widest band of the tools named.
|
|
57
|
-
const bands = tools.map(bandFor);
|
|
58
|
-
const lo = Math.min(...bands.map((b) => blendedUsdPerToken(b.low)));
|
|
59
|
-
const hi = Math.max(...bands.map((b) => blendedUsdPerToken(b.high)));
|
|
60
|
-
low += lines * rate.low * lo;
|
|
61
|
-
high += lines * rate.high * hi;
|
|
62
|
-
}
|
|
63
|
-
if (aiPrs < MIN_AI_PRS)
|
|
64
|
-
return none("too_few_ai_prs", aiPrs, scanned);
|
|
65
|
-
if (!rate)
|
|
66
|
-
return none("no_rate", aiPrs, scanned);
|
|
67
|
-
const line = `Estimate, AI tool spend: ${usd(low)} to ${usd(high)} across ${aiPrs} AI-assisted PRs ` +
|
|
68
|
-
`(public list prices as of ${PRICES_AS_OF} times a tokens-per-line rate from our own agents' PRs; not billing data).`;
|
|
69
|
-
return { kind: "range", lowUsd: low, highUsd: high, aiPrs, scannedPrs: scanned, byTool, line };
|
|
70
|
-
}
|
|
1
|
+
// Moved to shared/org-scan-core (MNE-9752); vendored at cli/src/vendor/shared/org-scan-core. Re-exported so CLI imports stay put.
|
|
2
|
+
export * from "../../../vendor/shared/org-scan-core/spend/spend-estimate.js";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { SpendEstimate } from "./spend-estimate.js";
|
|
2
1
|
export declare const ORG_SCAN_ENV = "MNEMOM_ORG_SCAN_MODE";
|
|
3
2
|
/** Default OFF: only the exact string "true" turns the tiers on. */
|
|
4
3
|
export declare const isSpendTiersEnabled: (env?: NodeJS.ProcessEnv) => boolean;
|
|
@@ -8,18 +7,4 @@ export declare const SPEND_NO_TERMINAL_MSG = "Pass --monthly-spend <usd> (there
|
|
|
8
7
|
export declare const MAX_MONTHLY_SPEND_USD = 10000000;
|
|
9
8
|
/** Dollars, or null if the text is not a positive, plausible amount. */
|
|
10
9
|
export declare function parseMonthlySpendUsd(raw: string): number | null;
|
|
11
|
-
export type SpendTier
|
|
12
|
-
export interface SpendSelection {
|
|
13
|
-
/** null when the evidence is too thin: print nothing. */
|
|
14
|
-
tier: SpendTier | null;
|
|
15
|
-
line: string | null;
|
|
16
|
-
/** May this line leave the machine in a share payload? Never for a self-reported figure. */
|
|
17
|
-
shareable: boolean;
|
|
18
|
-
}
|
|
19
|
-
export declare function selectSpend(i: {
|
|
20
|
-
measuredUsd?: number;
|
|
21
|
-
/** Fraction of measured tokens from requests over 200k context; shown only with a measured figure. */
|
|
22
|
-
over200kShare?: number | null;
|
|
23
|
-
selfReportedUsd?: number | null;
|
|
24
|
-
estimate: SpendEstimate;
|
|
25
|
-
}): SpendSelection;
|
|
10
|
+
export { selectSpend, type SpendTier, type SpendSelection, } from "../../../vendor/shared/org-scan-core/spend/spend-select.js";
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// MNE-9298: the CLI side of the spend tiers (env flag, typed figure). selectSpend is shared (MNE-9752).
|
|
2
|
+
// Tier 1 measured Anthropic usage the customer connected (a number handed in by the caller).
|
|
3
|
+
// Tier 2 a dollar figure the CTO gives; self-reported, labelled so, never sent or saved.
|
|
4
|
+
// Tier 3 the PR-size estimate range, only when the evidence behind it is enough.
|
|
5
|
+
// With none of the three there is NO line: a number is never invented to fill the gap.
|
|
1
6
|
export const ORG_SCAN_ENV = "MNEMOM_ORG_SCAN_MODE";
|
|
2
7
|
/** Default OFF: only the exact string "true" turns the tiers on. */
|
|
3
8
|
export const isSpendTiersEnabled = (env = process.env) => env[ORG_SCAN_ENV] === "true";
|
|
@@ -14,29 +19,4 @@ export function parseMonthlySpendUsd(raw) {
|
|
|
14
19
|
const n = Number(t.replace(/[$,\s]/g, ""));
|
|
15
20
|
return Number.isFinite(n) && n > 0 && n <= MAX_MONTHLY_SPEND_USD ? n : null;
|
|
16
21
|
}
|
|
17
|
-
|
|
18
|
-
const isUsd = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
19
|
-
const pct = (f) => `${Math.round(f * 100)}%`;
|
|
20
|
-
const isShare = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 1;
|
|
21
|
-
export function selectSpend(i) {
|
|
22
|
-
if (isUsd(i.measuredUsd)) {
|
|
23
|
-
return {
|
|
24
|
-
tier: 1,
|
|
25
|
-
line: `Tier 1, measured: ${usd(i.measuredUsd)} of Anthropic usage over the period${isShare(i.over200kShare)
|
|
26
|
-
? ` (${pct(i.over200kShare)} of tokens in requests over 200k context)`
|
|
27
|
-
: ""}.`,
|
|
28
|
-
shareable: true,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
if (isUsd(i.selfReportedUsd)) {
|
|
32
|
-
return {
|
|
33
|
-
tier: 2,
|
|
34
|
-
line: `Tier 2, self-reported: about ${usd(i.selfReportedUsd)} a month (given by you; not verified, not sent, not saved).`,
|
|
35
|
-
shareable: false,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
if (i.estimate.kind === "range") {
|
|
39
|
-
return { tier: 3, line: `Tier 3, ${i.estimate.line}`, shareable: true };
|
|
40
|
-
}
|
|
41
|
-
return { tier: null, line: null, shareable: false };
|
|
42
|
-
}
|
|
22
|
+
export { selectSpend, } from "../../../vendor/shared/org-scan-core/spend/spend-select.js";
|
|
@@ -1,30 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const MIN_CORPUS_ROWS = 5;
|
|
3
|
-
export interface CostRowShape {
|
|
4
|
-
repo: string;
|
|
5
|
-
pr: number;
|
|
6
|
-
additions: number;
|
|
7
|
-
deletions: number;
|
|
8
|
-
cost_usd: number;
|
|
9
|
-
runs: number;
|
|
10
|
-
models: string[];
|
|
11
|
-
}
|
|
12
|
-
export interface TokenRate {
|
|
13
|
-
/** Price-equivalent tokens per changed (added + deleted) line: 25th percentile. */
|
|
14
|
-
low: number;
|
|
15
|
-
median: number;
|
|
16
|
-
/** 75th percentile. */
|
|
17
|
-
high: number;
|
|
18
|
-
rows: number;
|
|
19
|
-
}
|
|
20
|
-
/** Linear-interpolated quantile of a sorted array. */
|
|
21
|
-
export declare function quantile(sorted: number[], q: number): number;
|
|
22
|
-
/**
|
|
23
|
-
* Derive the rate from corpus rows: per-row tokens/line, then the 25th, 50th and 75th percentile.
|
|
24
|
-
* Rows that fail validation are dropped (not repaired); fewer than MIN_CORPUS_ROWS valid rows
|
|
25
|
-
* returns null, never a rate built on a handful.
|
|
26
|
-
*/
|
|
27
|
-
export declare function deriveTokenRate(rows: unknown): TokenRate | null;
|
|
28
|
-
export declare const CORPUS_AS_OF: string;
|
|
29
|
-
/** The shipped rate: derived once from the committed fixture. */
|
|
30
|
-
export declare const DERIVED_TOKEN_RATE: TokenRate | null;
|
|
1
|
+
export * from "../../../vendor/shared/org-scan-core/spend/token-rate.js";
|
|
@@ -1,54 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// Nothing here is a typed-in rate: change the fixture and the rate moves, and token-rate.test.ts pins
|
|
4
|
-
// the derivation. Tokens are price-equivalent: cost / (our model's blended list price per token).
|
|
5
|
-
import rawCorpus from "./cost-corpus.v1.js";
|
|
6
|
-
import { TOOL_PRICES, blendedUsdPerToken } from "./prices.js";
|
|
7
|
-
/** Rows needed before the rate is trusted at all (data sufficiency, not a group size). */
|
|
8
|
-
export const MIN_CORPUS_ROWS = 5;
|
|
9
|
-
/** Our agents ran on Sonnet-family models in every corpus row; price their tokens as that. */
|
|
10
|
-
const BASIS_USD_PER_TOKEN = blendedUsdPerToken(TOOL_PRICES.claude_code.low);
|
|
11
|
-
const isCount = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
12
|
-
function validRow(r) {
|
|
13
|
-
if (typeof r !== "object" || r === null)
|
|
14
|
-
return false;
|
|
15
|
-
const o = r;
|
|
16
|
-
return (isCount(o.additions) &&
|
|
17
|
-
isCount(o.deletions) &&
|
|
18
|
-
o.additions + o.deletions > 0 &&
|
|
19
|
-
isCount(o.cost_usd) &&
|
|
20
|
-
o.cost_usd > 0);
|
|
21
|
-
}
|
|
22
|
-
/** Linear-interpolated quantile of a sorted array. */
|
|
23
|
-
export function quantile(sorted, q) {
|
|
24
|
-
if (sorted.length === 0)
|
|
25
|
-
return NaN;
|
|
26
|
-
const pos = (sorted.length - 1) * q;
|
|
27
|
-
const lo = Math.floor(pos);
|
|
28
|
-
const hi = Math.ceil(pos);
|
|
29
|
-
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Derive the rate from corpus rows: per-row tokens/line, then the 25th, 50th and 75th percentile.
|
|
33
|
-
* Rows that fail validation are dropped (not repaired); fewer than MIN_CORPUS_ROWS valid rows
|
|
34
|
-
* returns null, never a rate built on a handful.
|
|
35
|
-
*/
|
|
36
|
-
export function deriveTokenRate(rows) {
|
|
37
|
-
if (!Array.isArray(rows))
|
|
38
|
-
return null;
|
|
39
|
-
const per = rows
|
|
40
|
-
.filter(validRow)
|
|
41
|
-
.map((r) => r.cost_usd / (r.additions + r.deletions) / BASIS_USD_PER_TOKEN)
|
|
42
|
-
.sort((a, b) => a - b);
|
|
43
|
-
if (per.length < MIN_CORPUS_ROWS)
|
|
44
|
-
return null;
|
|
45
|
-
return {
|
|
46
|
-
low: quantile(per, 0.25),
|
|
47
|
-
median: quantile(per, 0.5),
|
|
48
|
-
high: quantile(per, 0.75),
|
|
49
|
-
rows: per.length,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
export const CORPUS_AS_OF = rawCorpus.as_of;
|
|
53
|
-
/** The shipped rate: derived once from the committed fixture. */
|
|
54
|
-
export const DERIVED_TOKEN_RATE = deriveTokenRate(rawCorpus.rows);
|
|
1
|
+
// Moved to shared/org-scan-core (MNE-9752); vendored at cli/src/vendor/shared/org-scan-core. Re-exported so CLI imports stay put.
|
|
2
|
+
export * from "../../../vendor/shared/org-scan-core/spend/token-rate.js";
|