@agent-finops/core 0.9.6 → 0.9.7
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 +3 -3
- package/dist/actionPlanner.js +4 -1
- package/dist/actionVerification.d.ts +54 -54
- package/dist/activitySnapshot.d.ts +57 -57
- package/dist/agentEconomicsReceipt.d.ts +62 -62
- package/dist/analyze.js +59 -18
- package/dist/contextHealth.js +15 -4
- package/dist/cutList.d.ts +23 -0
- package/dist/cutList.js +268 -11
- package/dist/deadContext.js +10 -3
- package/dist/insights.js +41 -23
- package/dist/planMath.js +25 -4
- package/dist/projectIndexStore.d.ts +8 -8
- package/dist/qualitativeIndexCache.d.ts +8 -8
- package/dist/toolInvocations.js +9 -1
- package/dist/untrustedLabel.d.ts +70 -0
- package/dist/untrustedLabel.js +161 -0
- package/package.json +1 -1
package/dist/contextHealth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeUntrustedLabel, WITHHELD_FILE_LABEL, WITHHELD_PROJECT_LABEL } from "./untrustedLabel.js";
|
|
1
2
|
import { loadAgentInventory } from "./agentInventory.js";
|
|
2
3
|
import { computeDeadContext } from "./deadContext.js";
|
|
3
4
|
import { localAgentFormatSupports } from "./localAgentFormats/registry.js";
|
|
@@ -74,7 +75,9 @@ export function buildContextHealth(input = {}) {
|
|
|
74
75
|
if ((contextChurn.repeatedReadEvents ?? 0) > 0) {
|
|
75
76
|
const files = contextChurn.repeatedFiles
|
|
76
77
|
.slice(0, 3)
|
|
77
|
-
|
|
78
|
+
// Basenames, but a basename is still a name the user did not author,
|
|
79
|
+
// and this sentence is returned verbatim by the MCP context-health tool.
|
|
80
|
+
.map((file) => `${safeUntrustedLabel(file.file, WITHHELD_FILE_LABEL)} ×${file.readCount}`)
|
|
78
81
|
.join(", ");
|
|
79
82
|
evidence.push({
|
|
80
83
|
kind: "context_churn",
|
|
@@ -231,6 +234,9 @@ function contextDecision(input) {
|
|
|
231
234
|
const MIN_EXACT_COMPARISONS = 2;
|
|
232
235
|
const MIN_SESSION_TYPE_COMPARISONS = 2;
|
|
233
236
|
const MAX_DISPLAY_RATIO = 20;
|
|
237
|
+
function safeOptionalProject(value) {
|
|
238
|
+
return value === undefined ? undefined : safeUntrustedLabel(value, WITHHELD_PROJECT_LABEL);
|
|
239
|
+
}
|
|
234
240
|
function buildCurrentSession(sessions, now, activeWithinMinutes) {
|
|
235
241
|
const latest = latestContextSession(sessions);
|
|
236
242
|
if (!latest)
|
|
@@ -273,7 +279,9 @@ function buildCurrentSession(sessions, now, activeWithinMinutes) {
|
|
|
273
279
|
return {
|
|
274
280
|
status: ageMs <= activeWithinMinutes * 60_000 ? "active" : "recent",
|
|
275
281
|
agent: latest.agent,
|
|
276
|
-
project: latest.project
|
|
282
|
+
project: latest.project === undefined
|
|
283
|
+
? undefined
|
|
284
|
+
: safeUntrustedLabel(latest.project, WITHHELD_PROJECT_LABEL),
|
|
277
285
|
totalTokens: latest.totalTokens,
|
|
278
286
|
contextTokens: latest.contextTokens,
|
|
279
287
|
usageSource: latest.usageSource,
|
|
@@ -308,7 +316,7 @@ function contextSessions(calls) {
|
|
|
308
316
|
return {
|
|
309
317
|
key,
|
|
310
318
|
agent: latest.agent,
|
|
311
|
-
project: latest.project ?? ordered[0]?.project,
|
|
319
|
+
project: safeOptionalProject(latest.project ?? ordered[0]?.project),
|
|
312
320
|
lastActivityAt: latest.timestamp,
|
|
313
321
|
totalTokens: turnUsage?.totalTokens ?? 0,
|
|
314
322
|
contextTokens: turnUsage?.contextTokens ?? 0,
|
|
@@ -350,8 +358,11 @@ function buildContextChurn(latest, invocations) {
|
|
|
350
358
|
signal.sessionId &&
|
|
351
359
|
latest.key === `${signal.agent}:${signal.sessionId}`))
|
|
352
360
|
: undefined;
|
|
361
|
+
// Neutralized at the source in toolInvocations.ts; re-applied here because a
|
|
362
|
+
// signal can also arrive from a cache written by an earlier version, and
|
|
363
|
+
// because this array is handed to an agent verbatim by the MCP tool.
|
|
353
364
|
const repeatedFiles = (currentSignal?.repeatedFileReads ?? [])
|
|
354
|
-
.map((file) => ({ file: file.name, readCount: file.count }));
|
|
365
|
+
.map((file) => ({ file: safeUntrustedLabel(file.name, WITHHELD_FILE_LABEL), readCount: file.count }));
|
|
355
366
|
return {
|
|
356
367
|
currentSessionEvidence: !latest
|
|
357
368
|
? "no_current_session"
|
package/dist/cutList.d.ts
CHANGED
|
@@ -40,7 +40,30 @@ export type CutAction = {
|
|
|
40
40
|
* by two actions (see {@link buildRecommendedPlan}).
|
|
41
41
|
*/
|
|
42
42
|
recordIds: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Median DAY's summed input+cache tokens for this candidate, computed over
|
|
45
|
+
* calendar days (not over records — a project running two models emits two
|
|
46
|
+
* records per day, and calling that "per day" would halve the number).
|
|
47
|
+
*
|
|
48
|
+
* Exists so a grouped render can tell two members apart by the quantity that
|
|
49
|
+
* explains their dollars instead of by the rounded dollar alone. Optional and
|
|
50
|
+
* absent on candidates with no day-level evidence; every renderer MUST drop
|
|
51
|
+
* the figure rather than print a placeholder.
|
|
52
|
+
*/
|
|
53
|
+
medianDailyInputTokens?: number;
|
|
43
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Compact token count for prose: 2,140,000 -> "2.1M", 8,300 -> "8.3K".
|
|
57
|
+
* Lives in core because the cut-list guidance strings are built here, and is
|
|
58
|
+
* re-used by the renderers so one candidate's token magnitude reads the same
|
|
59
|
+
* on every surface.
|
|
60
|
+
*
|
|
61
|
+
* The ladder runs to T. It used to stop at B, so a fleet-scale window printed
|
|
62
|
+
* "4212.7B" — a number the reader has to count digits on to place, from a
|
|
63
|
+
* product whose whole claim is arithmetic you can read at a glance. Past T the
|
|
64
|
+
* mantissa gets thousands separators rather than a fourteenth silent digit.
|
|
65
|
+
*/
|
|
66
|
+
export declare function formatTokenCount(tokens: number): string;
|
|
44
67
|
/**
|
|
45
68
|
* A non-overlapping "recommended plan" plus the leftover overlapping
|
|
46
69
|
* opportunities. The recommended-plan total is the only savings number safe to
|
package/dist/cutList.js
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import { hasCallLevelProvenance, hasModeledWorkloadEvidence, hasPricedEvidence } from "./schema.js";
|
|
2
2
|
import { localAgentFormatSupports } from "./localAgentFormats/registry.js";
|
|
3
|
+
import { safeUntrustedLabel, WITHHELD_AGENT_LABEL, WITHHELD_MODEL_LABEL, WITHHELD_OPERATION_LABEL, WITHHELD_PROJECT_LABEL } from "./untrustedLabel.js";
|
|
4
|
+
/**
|
|
5
|
+
* Compact token count for prose: 2,140,000 -> "2.1M", 8,300 -> "8.3K".
|
|
6
|
+
* Lives in core because the cut-list guidance strings are built here, and is
|
|
7
|
+
* re-used by the renderers so one candidate's token magnitude reads the same
|
|
8
|
+
* on every surface.
|
|
9
|
+
*
|
|
10
|
+
* The ladder runs to T. It used to stop at B, so a fleet-scale window printed
|
|
11
|
+
* "4212.7B" — a number the reader has to count digits on to place, from a
|
|
12
|
+
* product whose whole claim is arithmetic you can read at a glance. Past T the
|
|
13
|
+
* mantissa gets thousands separators rather than a fourteenth silent digit.
|
|
14
|
+
*/
|
|
15
|
+
export function formatTokenCount(tokens) {
|
|
16
|
+
if (!Number.isFinite(tokens))
|
|
17
|
+
return "0";
|
|
18
|
+
const value = Math.max(0, tokens);
|
|
19
|
+
if (value >= 1_000_000_000_000)
|
|
20
|
+
return `${formatMantissa(value / 1_000_000_000_000)}T`;
|
|
21
|
+
if (value >= 1_000_000_000)
|
|
22
|
+
return `${(value / 1_000_000_000).toFixed(1)}B`;
|
|
23
|
+
if (value >= 1_000_000)
|
|
24
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
25
|
+
if (value >= 1_000)
|
|
26
|
+
return `${(value / 1_000).toFixed(1)}K`;
|
|
27
|
+
return String(Math.round(value));
|
|
28
|
+
}
|
|
29
|
+
/** Top of the ladder: keep one decimal until the integer part needs commas. */
|
|
30
|
+
function formatMantissa(value) {
|
|
31
|
+
return value >= 1_000
|
|
32
|
+
? Math.round(value).toLocaleString("en-US")
|
|
33
|
+
: value.toFixed(1);
|
|
34
|
+
}
|
|
3
35
|
/**
|
|
4
36
|
* Select a non-overlapping subset of cut actions, highest-savings first. An
|
|
5
37
|
* action is added only if none of its records were already claimed by a
|
|
@@ -126,10 +158,18 @@ function modelDowngradeActions(records) {
|
|
|
126
158
|
const affectedSpendUsd = roundMoney(sumRecords(groupRecords));
|
|
127
159
|
const windowSavings = affectedSpendUsd * (1 - rule.costRetained);
|
|
128
160
|
const monthlySavings = roundMoney(toMonthly(windowSavings, window));
|
|
161
|
+
// Both untrusted. The model gate is anchored but open-ended
|
|
162
|
+
// (`^claude-opus-4(?:[.-].*)?$` accepts anything after the dash), and
|
|
163
|
+
// `downgradeSafeOperation` is a SUBSTRING allowlist — "summary" anywhere
|
|
164
|
+
// in the label passes the whole label through. Neutralize before either
|
|
165
|
+
// reaches a title or an instruction. IDs keep the raw values: an id is
|
|
166
|
+
// identity, not display, and it is rendered through a blanking guard.
|
|
167
|
+
const modelLabel = safeUntrustedLabel(model, WITHHELD_MODEL_LABEL);
|
|
168
|
+
const operationLabel = safeUntrustedLabel(operation, WITHHELD_OPERATION_LABEL);
|
|
129
169
|
actions.push({
|
|
130
170
|
id: `downgrade-${slug(model)}-${slug(operation)}`,
|
|
131
|
-
title: `Move ${
|
|
132
|
-
action: `Route ${groupRecords.length} ${
|
|
171
|
+
title: `Move ${modelLabel} ${operationLabel} calls to ${target}`,
|
|
172
|
+
action: `Route ${groupRecords.length} ${operationLabel} call${groupRecords.length === 1 ? "" : "s"} from ${modelLabel} to ${target} (keep ${modelLabel} only when output is rejected).`,
|
|
133
173
|
estimatedMonthlySavingsUsd: monthlySavings,
|
|
134
174
|
affectedSpendUsd,
|
|
135
175
|
recordCount: groupRecords.length,
|
|
@@ -155,6 +195,30 @@ function contextTrimActions(records) {
|
|
|
155
195
|
: `connected::${operation}`;
|
|
156
196
|
byOperation.set(key, [...(byOperation.get(key) ?? []), record]);
|
|
157
197
|
}
|
|
198
|
+
// ONE denominator, and it is the set the sentence is already talking about:
|
|
199
|
+
// this agent's OWN flagged projects. It is both the share's denominator and
|
|
200
|
+
// the rank clause's population, so the percentage, the rank, and the entry's
|
|
201
|
+
// own dollars two lines below all reconcile, and the members of one fan-out
|
|
202
|
+
// sum to 100%.
|
|
203
|
+
//
|
|
204
|
+
// 0.9.7 shipped three denominators in one sentence — a machine-wide local
|
|
205
|
+
// total for the share, this agent's flagged projects for the rank, and the
|
|
206
|
+
// entry's own total underneath. Every figure was individually true, which is
|
|
207
|
+
// exactly why the sentence read as an arithmetic error. Never reintroduce a
|
|
208
|
+
// denominator the sentence does not name.
|
|
209
|
+
const flaggedSpendByAgent = new Map();
|
|
210
|
+
for (const [key, groupRecords] of byOperation) {
|
|
211
|
+
const [scope, agentId] = key.split("::");
|
|
212
|
+
if (scope !== "local" || !agentId)
|
|
213
|
+
continue;
|
|
214
|
+
// roundMoney, matching `affectedSpendUsd` exactly: comparing a rounded
|
|
215
|
+
// group against unrounded peers made a candidate outrank ITSELF and
|
|
216
|
+
// report "rank 2 of 8" for the largest project on the list.
|
|
217
|
+
flaggedSpendByAgent.set(agentId, [
|
|
218
|
+
...(flaggedSpendByAgent.get(agentId) ?? []),
|
|
219
|
+
roundMoney(sumRecords(groupRecords))
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
158
222
|
const actions = [];
|
|
159
223
|
for (const [key, groupRecords] of byOperation) {
|
|
160
224
|
const affectedSpendUsd = roundMoney(sumRecords(groupRecords));
|
|
@@ -164,7 +228,26 @@ function contextTrimActions(records) {
|
|
|
164
228
|
: key.replace(/^connected::/, "");
|
|
165
229
|
const count = groupRecords.length;
|
|
166
230
|
const agent = groupRecords[0]?.agentId ?? "coding-agent";
|
|
231
|
+
// `agent` stays raw as the map key and the id slug; only the DISPLAY
|
|
232
|
+
// form is neutralized. Bounded to the adapter enum for local rows in
|
|
233
|
+
// practice, but nothing in the type system enforces that.
|
|
234
|
+
const agentLabel = safeUntrustedLabel(agent, WITHHELD_AGENT_LABEL);
|
|
167
235
|
const project = groupRecords[0]?.projectId;
|
|
236
|
+
const evidence = sessionAggregates ? dailyContextEvidence(groupRecords) : null;
|
|
237
|
+
const flaggedSpend = flaggedSpendByAgent.get(agent) ?? [];
|
|
238
|
+
// The project label is UNTRUSTED — it is a directory name off the user's
|
|
239
|
+
// disk. Neutralize it ONCE, here, so the title, the guidance, and every
|
|
240
|
+
// surface that re-derives a label from the title all carry the same
|
|
241
|
+
// already-safe text and cannot disagree about it.
|
|
242
|
+
const projectLabel = project
|
|
243
|
+
? safeUntrustedLabel(project, WITHHELD_PROJECT_LABEL)
|
|
244
|
+
: "Unattributed";
|
|
245
|
+
// The connected path's operation label is a raw provider/adapter string
|
|
246
|
+
// with NO allowlist in front of it, and it lands in both the title and the
|
|
247
|
+
// instruction. It has to be neutralized here for the same reason the
|
|
248
|
+
// project label is: the report layer's prose sanitizer never blanks, and
|
|
249
|
+
// it is only safe to never blank because core neutralized first.
|
|
250
|
+
const operationLabel = safeUntrustedLabel(operation, WITHHELD_OPERATION_LABEL);
|
|
168
251
|
// Large token volume proves exposure, not that context is removable or
|
|
169
252
|
// what quality/cost delta a change would produce. Context remains an
|
|
170
253
|
// inspect-only action until matched before/after evidence exists.
|
|
@@ -174,11 +257,18 @@ function contextTrimActions(records) {
|
|
|
174
257
|
? `inspect-context-${slug(agent)}-${slug(project ?? "unattributed")}`
|
|
175
258
|
: `inspect-context-${slug(operation)}`,
|
|
176
259
|
title: sessionAggregates
|
|
177
|
-
? `Investigate cumulative context in ${
|
|
178
|
-
: `Inspect oversized context on ${
|
|
260
|
+
? `Investigate cumulative context in ${agentLabel} · ${projectLabel}`
|
|
261
|
+
: `Inspect oversized context on ${operationLabel}`,
|
|
179
262
|
action: sessionAggregates
|
|
180
|
-
?
|
|
181
|
-
|
|
263
|
+
? localContextTrimGuidance({
|
|
264
|
+
count,
|
|
265
|
+
agent: agentLabel,
|
|
266
|
+
projectLabel,
|
|
267
|
+
evidence,
|
|
268
|
+
groupSpendUsd: affectedSpendUsd,
|
|
269
|
+
flaggedSpend
|
|
270
|
+
})
|
|
271
|
+
: `${count} call-level ${operationLabel} record${count === 1 ? "" : "s"} exceeded 100k input tokens. Inspect retrieved chunks and prompt history, then run a matched before/after before claiming savings.`,
|
|
182
272
|
estimatedMonthlySavingsUsd: monthlySavings,
|
|
183
273
|
affectedSpendUsd,
|
|
184
274
|
recordCount: count,
|
|
@@ -186,11 +276,172 @@ function contextTrimActions(records) {
|
|
|
186
276
|
impactBasis: "observed_value_no_counterfactual",
|
|
187
277
|
recordIds: groupRecords.map((record) => record.id),
|
|
188
278
|
confidence: combinedConfidence(groupRecords.map((record) => record.costConfidence)),
|
|
189
|
-
kind: "context_trim"
|
|
279
|
+
kind: "context_trim",
|
|
280
|
+
...(evidence ? { medianDailyInputTokens: evidence.medianDailyInputTokens } : {})
|
|
190
281
|
});
|
|
191
282
|
}
|
|
192
283
|
return actions;
|
|
193
284
|
}
|
|
285
|
+
function dailyContextEvidence(records) {
|
|
286
|
+
if (records.length === 0)
|
|
287
|
+
return null;
|
|
288
|
+
const byDay = new Map();
|
|
289
|
+
for (const record of records) {
|
|
290
|
+
const day = record.timestamp.slice(0, 10);
|
|
291
|
+
const current = byDay.get(day) ?? { input: 0, output: 0 };
|
|
292
|
+
current.input += record.inputTokens;
|
|
293
|
+
current.output += record.outputTokens;
|
|
294
|
+
byDay.set(day, current);
|
|
295
|
+
}
|
|
296
|
+
const days = [...byDay.entries()].sort((left, right) => left[0].localeCompare(right[0]));
|
|
297
|
+
const first = days[0];
|
|
298
|
+
if (!first)
|
|
299
|
+
return null;
|
|
300
|
+
// ONE REAL DAY, not two independent medians. The sentence says "median day
|
|
301
|
+
// carried X input+cache tokens against Y output"; if X and Y came from
|
|
302
|
+
// different calendar days that sentence would describe a day that never
|
|
303
|
+
// happened. So: order the days by input+cache, take the middle OBSERVED day
|
|
304
|
+
// (the lower of the two middles on an even sample, never their average), and
|
|
305
|
+
// read every median figure off that one day.
|
|
306
|
+
const byInput = [...days].sort((left, right) => left[1].input - right[1].input || left[0].localeCompare(right[0]));
|
|
307
|
+
const medianDay = byInput[Math.floor((byInput.length - 1) / 2)];
|
|
308
|
+
const medianDailyInputTokens = medianDay[1].input;
|
|
309
|
+
const medianDailyOutputTokens = medianDay[1].output;
|
|
310
|
+
const peak = days.reduce((best, entry) => (entry[1].input > best[1].input ? entry : best), first);
|
|
311
|
+
const spendByModel = new Map();
|
|
312
|
+
for (const record of records) {
|
|
313
|
+
spendByModel.set(record.model, (spendByModel.get(record.model) ?? 0) + (record.amountUsd ?? 0));
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
activeDays: days.length,
|
|
317
|
+
medianDailyInputTokens,
|
|
318
|
+
medianDailyOutputTokens,
|
|
319
|
+
peakDay: peak[0],
|
|
320
|
+
peakDayInputTokens: peak[1].input,
|
|
321
|
+
peakOverMedian: medianDailyInputTokens > 0 ? peak[1].input / medianDailyInputTokens : 0,
|
|
322
|
+
inputPerOutput: medianDailyOutputTokens > 0
|
|
323
|
+
? medianDailyInputTokens / medianDailyOutputTokens
|
|
324
|
+
: null,
|
|
325
|
+
models: [...spendByModel.entries()]
|
|
326
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
327
|
+
.map(([model]) => model)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The guidance a local context candidate carries.
|
|
332
|
+
*
|
|
333
|
+
* Two hard constraints shape it:
|
|
334
|
+
*
|
|
335
|
+
* 1. TRUTH. Every clause states something OBSERVED — a median, a date, a
|
|
336
|
+
* ratio, a share of a single-basis total. Nothing predicts a reduction,
|
|
337
|
+
* nothing prices a change, and no clause is emitted when the evidence
|
|
338
|
+
* behind it is absent. Sharper means more specific about what was seen,
|
|
339
|
+
* never more confident about what would happen.
|
|
340
|
+
*
|
|
341
|
+
* 2. GEOMETRY. The grouped terminal/report render quotes this string from its
|
|
342
|
+
* LARGEST member and drops everything before the first ". " (see
|
|
343
|
+
* `groupedCutActionLines` / `rankCutCandidates` in @agent-finops/report).
|
|
344
|
+
* So sentence 1 carries only per-member counts, and sentence 2 onward
|
|
345
|
+
* NAMES the project it describes — otherwise one project's median would be
|
|
346
|
+
* read as the whole group's. For the same reason sentence 1 interpolates
|
|
347
|
+
* no free text: a label containing ". " would truncate the string at the
|
|
348
|
+
* wrong point.
|
|
349
|
+
*/
|
|
350
|
+
function localContextTrimGuidance(input) {
|
|
351
|
+
const { count, evidence } = input;
|
|
352
|
+
// Idempotent: `contextTrimActions` already neutralized the label for the
|
|
353
|
+
// title, and re-running the check on an already-neutral label is a no-op.
|
|
354
|
+
// Repeating it here keeps the guarantee attached to the function that does
|
|
355
|
+
// the interpolating, not to one of its callers.
|
|
356
|
+
const projectLabel = safeUntrustedLabel(input.projectLabel, WITHHELD_PROJECT_LABEL);
|
|
357
|
+
const plural = count === 1 ? "" : "s";
|
|
358
|
+
if (!evidence) {
|
|
359
|
+
// No day-level evidence to quote. Stay honest and short rather than
|
|
360
|
+
// reciting a checklist the product cannot perform.
|
|
361
|
+
return `${count} day + agent + model + project aggregate${plural} each carried at least 100k summed input/cache tokens. ` +
|
|
362
|
+
`Inspect the heaviest sessions in ${projectLabel} before proposing one reversible change.`;
|
|
363
|
+
}
|
|
364
|
+
const dayPlural = evidence.activeDays === 1 ? "" : "s";
|
|
365
|
+
const sentences = [
|
|
366
|
+
`${count} day + agent + model + project aggregate${plural} over ${evidence.activeDays} active day${dayPlural}.`
|
|
367
|
+
];
|
|
368
|
+
const ratio = evidence.inputPerOutput === null ? null : formatRatio(evidence.inputPerOutput);
|
|
369
|
+
sentences.push(evidence.medianDailyOutputTokens > 0
|
|
370
|
+
? `${projectLabel} — median day carried ${formatTokenCount(evidence.medianDailyInputTokens)} input+cache tokens ` +
|
|
371
|
+
`against ${formatTokenCount(evidence.medianDailyOutputTokens)} output${ratio ? ` (${ratio}:1)` : ""}.`
|
|
372
|
+
: `${projectLabel} — median day carried ${formatTokenCount(evidence.medianDailyInputTokens)} input+cache tokens ` +
|
|
373
|
+
"with no output tokens recorded that day.");
|
|
374
|
+
// Only when there is a real lead. A uniform project gets no false one.
|
|
375
|
+
if (evidence.peakOverMedian >= 2) {
|
|
376
|
+
sentences.push(`Heaviest day ${evidence.peakDay} carried ${formatTokenCount(evidence.peakDayInputTokens)}, ` +
|
|
377
|
+
`${formatRatio(evidence.peakOverMedian)}× the median day; dates are each session's last activity.`);
|
|
378
|
+
}
|
|
379
|
+
const share = concentrationClause(input);
|
|
380
|
+
if (share)
|
|
381
|
+
sentences.push(share);
|
|
382
|
+
if (evidence.models.length > 1) {
|
|
383
|
+
// Model ids come off the same untrusted logs the project name does.
|
|
384
|
+
const shown = evidence.models
|
|
385
|
+
.slice(0, 3)
|
|
386
|
+
.map((model) => safeUntrustedLabel(model, WITHHELD_MODEL_LABEL));
|
|
387
|
+
const rest = evidence.models.length - shown.length;
|
|
388
|
+
sentences.push(`${evidence.models.length} models ran there: ${shown.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}.`);
|
|
389
|
+
}
|
|
390
|
+
sentences.push(evidence.peakOverMedian >= 2
|
|
391
|
+
? `Inspect the sessions behind ${evidence.peakDay} before proposing one reversible change.`
|
|
392
|
+
: `Inspect the heaviest sessions in ${projectLabel} before proposing one reversible change.`);
|
|
393
|
+
return sentences.join(" ");
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* "…holds 76% of the flagged claude-code value observed in this window (rank 1
|
|
397
|
+
* of 8 flagged projects)." An accounting fact about ONE denominator — not a
|
|
398
|
+
* savings claim, not a projection.
|
|
399
|
+
*
|
|
400
|
+
* The denominator is this agent's own flagged total, which is also the rank
|
|
401
|
+
* clause's population and the sum of the per-project dollars the same readout
|
|
402
|
+
* prints two lines below. So the percentage, the rank, and the entry's own
|
|
403
|
+
* total reconcile, and the members of one fan-out sum to 100%.
|
|
404
|
+
*
|
|
405
|
+
* 0.9.7 divided by a machine-wide local total instead: `--full` printed a
|
|
406
|
+
* by-project table saying 83%, this sentence said 46%, and the entry's own
|
|
407
|
+
* dollars implied 76% — three true numbers that read as an arithmetic error.
|
|
408
|
+
* No clamp is needed now and none belongs here: the numerator is one member of
|
|
409
|
+
* the set in the denominator, so a ratio above 1 would be a real bug worth
|
|
410
|
+
* seeing rather than a cosmetic one worth hiding.
|
|
411
|
+
*/
|
|
412
|
+
function concentrationClause(input) {
|
|
413
|
+
const { agent, groupSpendUsd, flaggedSpend } = input;
|
|
414
|
+
const flaggedTotalUsd = flaggedSpend.reduce((total, spend) => total + spend, 0);
|
|
415
|
+
if (!(flaggedTotalUsd > 0) || !(groupSpendUsd > 0))
|
|
416
|
+
return null;
|
|
417
|
+
const ratio = groupSpendUsd / flaggedTotalUsd;
|
|
418
|
+
if (!Number.isFinite(ratio) || ratio <= 0)
|
|
419
|
+
return null;
|
|
420
|
+
const percent = Math.round(ratio * 100);
|
|
421
|
+
const rank = flaggedSpend.filter((spend) => spend > groupSpendUsd).length + 1;
|
|
422
|
+
const total = flaggedSpend.length;
|
|
423
|
+
// A ceiling, mirroring the "under 1%" floor. The ordinary solo-dev shape is
|
|
424
|
+
// one main repo and two small side projects: at 99.5%–99.9% the rounded
|
|
425
|
+
// figure prints "100%" on the same screen as an across-line that lists two
|
|
426
|
+
// more flagged projects WITH DOLLARS — which reads as exactly the
|
|
427
|
+
// arithmetic error this sentence's denominator was fixed to kill. "over 99%"
|
|
428
|
+
// is the same fact without the contradiction. A lone flagged project keeps
|
|
429
|
+
// the literal 100%, because there is nothing beside it to contradict.
|
|
430
|
+
const share = percent < 1
|
|
431
|
+
? "under 1%"
|
|
432
|
+
: percent >= 100 && total > 1 ? "over 99%" : `${percent}%`;
|
|
433
|
+
const rankClause = total > 1 ? ` (rank ${rank} of ${total} flagged projects)` : "";
|
|
434
|
+
return `That project holds ${share} of the flagged ${agent} value observed in this window${rankClause}.`;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* "519", "4.4" — integers once the ratio is big enough that a decimal is noise.
|
|
438
|
+
* Shared by the input:output ratio and the "N× the median day" multiple so the
|
|
439
|
+
* two never disagree about precision. Separated above 999, because a bare
|
|
440
|
+
* "1904762:1" is a digit-counting exercise, not a figure.
|
|
441
|
+
*/
|
|
442
|
+
function formatRatio(value) {
|
|
443
|
+
return value >= 10 ? Math.round(value).toLocaleString("en-US") : value.toFixed(1);
|
|
444
|
+
}
|
|
194
445
|
function cacheActions(records) {
|
|
195
446
|
const window = windowDays(records);
|
|
196
447
|
const counts = new Map();
|
|
@@ -226,10 +477,13 @@ function cacheActions(records) {
|
|
|
226
477
|
const affectedSpendUsd = roundMoney(sumRecords(groupRecords));
|
|
227
478
|
const windowSavings = sumRecords(avoidableRecords);
|
|
228
479
|
const monthlySavings = roundMoney(toMonthly(windowSavings, window));
|
|
480
|
+
// No allowlist stands in front of this one at all — any operation label
|
|
481
|
+
// with a stable fingerprint reaches the title and the instruction.
|
|
482
|
+
const operationLabel = safeUntrustedLabel(operation, WITHHELD_OPERATION_LABEL);
|
|
229
483
|
actions.push({
|
|
230
484
|
id: `cache-${slug(operation)}-${stableSuffix(key)}`,
|
|
231
|
-
title: `Cache repeated ${
|
|
232
|
-
action: `Keep the earliest ${
|
|
485
|
+
title: `Cache repeated ${operationLabel} calls`,
|
|
486
|
+
action: `Keep the earliest ${operationLabel} call as the canonical miss and cache the ${avoidableRecords.length} subsequent call${avoidableRecords.length === 1 ? "" : "s"} with the same adapter-provided input fingerprint.`,
|
|
233
487
|
estimatedMonthlySavingsUsd: monthlySavings,
|
|
234
488
|
affectedSpendUsd,
|
|
235
489
|
recordCount: groupRecords.length,
|
|
@@ -274,10 +528,13 @@ function batchActions(records) {
|
|
|
274
528
|
const retainedCost = batchCostRetainedByProvider[groupRecords[0].source.provider];
|
|
275
529
|
const windowSavings = affectedSpendUsd * (1 - retainedCost);
|
|
276
530
|
const monthlySavings = roundMoney(toMonthly(windowSavings, window));
|
|
531
|
+
// `batchSafeOperation` is a SUBSTRING allowlist: "summar" anywhere in the
|
|
532
|
+
// label admits the whole label, injected prose included.
|
|
533
|
+
const operationLabel = safeUntrustedLabel(operation, WITHHELD_OPERATION_LABEL);
|
|
277
534
|
actions.push({
|
|
278
535
|
id: `batch-${slug(operation)}-${stableSuffix(key)}`,
|
|
279
|
-
title: `Move ${
|
|
280
|
-
action: `Submit ${groupRecords.length} ${
|
|
536
|
+
title: `Move ${operationLabel} calls to the Batch API`,
|
|
537
|
+
action: `Submit ${groupRecords.length} ${operationLabel} call${groupRecords.length === 1 ? "" : "s"} through the provider's Batch API (flat 50% off; results within 24h, fine for offline work).`,
|
|
281
538
|
estimatedMonthlySavingsUsd: monthlySavings,
|
|
282
539
|
affectedSpendUsd,
|
|
283
540
|
recordCount: groupRecords.length,
|
package/dist/deadContext.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadAgentInventory } from "./agentInventory.js";
|
|
2
|
+
import { safeUntrustedLabel, WITHHELD_ENTITY_LABEL, WITHHELD_FILE_LABEL } from "./untrustedLabel.js";
|
|
2
3
|
import { findPricingRule } from "./modelPricing.js";
|
|
3
4
|
import { loadToolInvocations } from "./toolInvocations.js";
|
|
4
5
|
/**
|
|
@@ -97,15 +98,21 @@ export function computeDeadContext(items, invocations, config) {
|
|
|
97
98
|
}
|
|
98
99
|
dead.push({
|
|
99
100
|
kind: item.kind,
|
|
100
|
-
|
|
101
|
+
// Skill, subagent, slash-command, hook and MCP SERVER names, read off
|
|
102
|
+
// disk. They are printed by name on the readout and in the artifact.
|
|
103
|
+
name: safeUntrustedLabel(item.name, WITHHELD_ENTITY_LABEL),
|
|
101
104
|
scope: item.scope,
|
|
102
105
|
activation: item.activation,
|
|
106
|
+
// The STRUCTURED siblings travel with the name to every surface the name
|
|
107
|
+
// does, including the Apply artifact. Neutralizing the name and leaving
|
|
108
|
+
// the path beside it raw is the same inversion Blocker A was.
|
|
109
|
+
// `host` is the InventoryHost enum, not free text — bounded by the type.
|
|
103
110
|
host: item.host,
|
|
104
111
|
invocationTracking: item.invocationTracking,
|
|
105
112
|
alwaysLoadedTokens: item.alwaysLoadedTokens,
|
|
106
113
|
weightConfidence: item.weightConfidence,
|
|
107
|
-
path: item.path,
|
|
108
|
-
ownerDirs: item.ownerDirs
|
|
114
|
+
path: item.path === undefined ? undefined : safeUntrustedLabel(item.path, WITHHELD_FILE_LABEL),
|
|
115
|
+
ownerDirs: item.ownerDirs?.map((dir) => safeUntrustedLabel(dir, WITHHELD_FILE_LABEL))
|
|
109
116
|
});
|
|
110
117
|
}
|
|
111
118
|
dead.sort((a, b) => b.alwaysLoadedTokens - a.alwaysLoadedTokens);
|
package/dist/insights.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasCallLevelProvenance, hasPricedEvidence, spendComparisonKey, spendInsightSchema } from "./schema.js";
|
|
2
|
+
import { safeUntrustedLabel, safeUntrustedLabels, WITHHELD_ENTITY_LABEL, WITHHELD_OPERATION_LABEL } from "./untrustedLabel.js";
|
|
2
3
|
const confidenceRank = {
|
|
3
4
|
verified: 0,
|
|
4
5
|
estimated: 1,
|
|
@@ -32,7 +33,10 @@ function spikeInsights(records, summary) {
|
|
|
32
33
|
const topProject = topBreakdown(currentRecords, (record) => record.projectId);
|
|
33
34
|
const topModels = breakdown(currentRecords, (record) => record.model).slice(0, 2).map((entry) => entry.key);
|
|
34
35
|
const deltaUsd = roundMoney(anomaly.currentAmountUsd - anomaly.previousAmountUsd);
|
|
35
|
-
|
|
36
|
+
// Ownership lead is an agent/project/client id off the records — it lands
|
|
37
|
+
// mid-sentence in `summary`, so it is neutralized at the interpolation
|
|
38
|
+
// point like every other untrusted fragment.
|
|
39
|
+
const likelyOwner = safeEntity(topAgent?.key ?? topProject?.key ?? topClient?.key ?? "an unassigned owner");
|
|
36
40
|
const cohortSuffix = stableSuffix(anomaly.comparisonKey ?? "legacy");
|
|
37
41
|
const isProviderBilledCost = currentRecords.length > 0 && currentRecords.every((record) => record.usageGranularity === "billing_bucket" &&
|
|
38
42
|
record.costConfidence === "verified");
|
|
@@ -50,14 +54,14 @@ function spikeInsights(records, summary) {
|
|
|
50
54
|
{ label: `Previous cohort ${isProviderBilledCost ? "spend" : "value"}`, value: formatUsd(anomaly.previousAmountUsd) },
|
|
51
55
|
{ label: `Current cohort ${isProviderBilledCost ? "spend" : "value"}`, value: formatUsd(anomaly.currentAmountUsd) },
|
|
52
56
|
{ label: `${evidenceLabel} increase`, value: formatUsd(deltaUsd), detail: `${formatMultiplier(anomaly.multiplier)} day-over-day multiplier` },
|
|
53
|
-
topAgent ? { label: "Ownership lead", value: topAgent.key, detail: `${formatUsd(topAgent.amountUsd)} across ${topAgent.recordCount} cohort records` } : undefined,
|
|
54
|
-
topClient ? { label: "Client concentration", value: topClient.key, detail: `${formatUsd(topClient.amountUsd)} on spike day` } : undefined,
|
|
55
|
-
topModels.length > 0 ? { label: "Dominant models", value: topModels.join(", ") } : undefined
|
|
57
|
+
topAgent ? { label: "Ownership lead", value: safeEntity(topAgent.key), detail: `${formatUsd(topAgent.amountUsd)} across ${topAgent.recordCount} cohort records` } : undefined,
|
|
58
|
+
topClient ? { label: "Client concentration", value: safeEntity(topClient.key), detail: `${formatUsd(topClient.amountUsd)} on spike day` } : undefined,
|
|
59
|
+
topModels.length > 0 ? { label: "Dominant models", value: safeUntrustedLabels(topModels).join(", ") } : undefined
|
|
56
60
|
]),
|
|
57
|
-
affectedClients: keysFrom(currentRecords, (record) => record.clientId),
|
|
58
|
-
affectedProjects: keysFrom(currentRecords, (record) => record.projectId),
|
|
59
|
-
affectedAgents: keysFrom(currentRecords, (record) => record.agentId),
|
|
60
|
-
affectedModels: keysFrom(currentRecords, (record) => record.model),
|
|
61
|
+
affectedClients: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.clientId)),
|
|
62
|
+
affectedProjects: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.projectId)),
|
|
63
|
+
affectedAgents: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.agentId)),
|
|
64
|
+
affectedModels: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.model)),
|
|
61
65
|
estimatedImpactUsd: deltaUsd,
|
|
62
66
|
confidence: anomaly.confidence,
|
|
63
67
|
recommendedAction: `Review and reconcile the provider-cohort records from ${anomaly.key}, confirm the accountable owner, and obtain run-level evidence before diagnosing behavior or changing a policy.`,
|
|
@@ -81,24 +85,27 @@ function agentCostDriverInsights(records, summary) {
|
|
|
81
85
|
const topModel = topBreakdown(agentRecords, (record) => record.model);
|
|
82
86
|
const hasRunLevelEvidence = agentRecords.length > 0 && agentRecords.every(hasCallLevelProvenance);
|
|
83
87
|
return [{
|
|
84
|
-
id
|
|
88
|
+
// The id is a STRUCTURED field beside the neutralized title, and it is
|
|
89
|
+
// rendered (`Canonical candidate ID: ...`). Slug the neutralized form, not
|
|
90
|
+
// the raw key.
|
|
91
|
+
id: `agent-spend-concentration-${slug(safeEntity(topAgent.key))}`,
|
|
85
92
|
kind: "optimization_opportunity",
|
|
86
93
|
severity: "medium",
|
|
87
|
-
title: `${topAgent.key} spend concentration needs owner and budget review`,
|
|
88
|
-
summary: `${topAgent.key} is attached to ${formatPercent(share)} of tracked spend. Concentration alone does not prove abnormal behavior or an avoidable dollar amount${hasRunLevelEvidence ? "." : "; the evidence is aggregate rather than run-level."}`,
|
|
94
|
+
title: `${safeEntity(topAgent.key)} spend concentration needs owner and budget review`,
|
|
95
|
+
summary: `${safeEntity(topAgent.key)} is attached to ${formatPercent(share)} of tracked spend. Concentration alone does not prove abnormal behavior or an avoidable dollar amount${hasRunLevelEvidence ? "." : "; the evidence is aggregate rather than run-level."}`,
|
|
89
96
|
evidence: compactEvidence([
|
|
90
97
|
{ label: "Attributed spend", value: formatUsd(topAgent.amountUsd), detail: `${topAgent.recordCount} ${hasRunLevelEvidence ? "call-level" : "aggregate"} record${topAgent.recordCount === 1 ? "" : "s"}` },
|
|
91
98
|
{ label: "Share of tracked spend", value: formatPercent(share) },
|
|
92
|
-
topModel ? { label: "Dominant model or billing label", value: topModel.key, detail: `${formatUsd(topModel.amountUsd)} in this concentration` } : undefined,
|
|
93
|
-
topOperation ? { label: "Operation label", value: topOperation.key, detail: hasRunLevelEvidence ? "Call-level attribution" : "Not verified as one call or run" } : undefined
|
|
99
|
+
topModel ? { label: "Dominant model or billing label", value: safeEntity(topModel.key), detail: `${formatUsd(topModel.amountUsd)} in this concentration` } : undefined,
|
|
100
|
+
topOperation ? { label: "Operation label", value: safeEntity(topOperation.key), detail: hasRunLevelEvidence ? "Call-level attribution" : "Not verified as one call or run" } : undefined
|
|
94
101
|
]),
|
|
95
|
-
affectedClients: keysFrom(agentRecords, (record) => record.clientId),
|
|
96
|
-
affectedProjects: keysFrom(agentRecords, (record) => record.projectId),
|
|
97
|
-
affectedAgents: [topAgent.key],
|
|
98
|
-
affectedModels: keysFrom(agentRecords, (record) => record.model),
|
|
102
|
+
affectedClients: safeUntrustedLabels(keysFrom(agentRecords, (record) => record.clientId)),
|
|
103
|
+
affectedProjects: safeUntrustedLabels(keysFrom(agentRecords, (record) => record.projectId)),
|
|
104
|
+
affectedAgents: [safeEntity(topAgent.key)],
|
|
105
|
+
affectedModels: safeUntrustedLabels(keysFrom(agentRecords, (record) => record.model)),
|
|
99
106
|
estimatedImpactUsd: 0,
|
|
100
107
|
confidence: topAgent.confidence,
|
|
101
|
-
recommendedAction: `Confirm who owns ${topAgent.key}, reconcile the spend to its approved budget, and collect behavioral evidence before setting a cap or savings target.`,
|
|
108
|
+
recommendedAction: `Confirm who owns ${safeEntity(topAgent.key)}, reconcile the spend to its approved budget, and collect behavioral evidence before setting a cap or savings target.`,
|
|
102
109
|
verificationNeeded: "Confirm the budget owner and expected range; concentration alone is not behavioral evidence."
|
|
103
110
|
}];
|
|
104
111
|
}
|
|
@@ -113,7 +120,7 @@ function contextBloatInsights(records) {
|
|
|
113
120
|
const scopedRecords = topOperation
|
|
114
121
|
? highInputRecords.filter((record) => record.operation === topOperation.key)
|
|
115
122
|
: highInputRecords;
|
|
116
|
-
const operationLabel = topOperation?.key ?? "large-context calls";
|
|
123
|
+
const operationLabel = safeUntrustedLabel(topOperation?.key ?? "large-context calls", WITHHELD_OPERATION_LABEL);
|
|
117
124
|
const totalInputTokens = scopedRecords.reduce((total, record) => total + record.inputTokens, 0);
|
|
118
125
|
const scopedSpend = roundMoney(sumRecords(scopedRecords));
|
|
119
126
|
if (scopedSpend < 20) {
|
|
@@ -131,16 +138,27 @@ function contextBloatInsights(records) {
|
|
|
131
138
|
{ label: "Spend attached to large context", value: formatUsd(scopedSpend) },
|
|
132
139
|
{ label: "Dominant operation", value: operationLabel }
|
|
133
140
|
],
|
|
134
|
-
affectedClients: keysFrom(scopedRecords, (record) => record.clientId),
|
|
135
|
-
affectedProjects: keysFrom(scopedRecords, (record) => record.projectId),
|
|
136
|
-
affectedAgents: keysFrom(scopedRecords, (record) => record.agentId),
|
|
137
|
-
affectedModels: keysFrom(scopedRecords, (record) => record.model),
|
|
141
|
+
affectedClients: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.clientId)),
|
|
142
|
+
affectedProjects: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.projectId)),
|
|
143
|
+
affectedAgents: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.agentId)),
|
|
144
|
+
affectedModels: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.model)),
|
|
138
145
|
estimatedImpactUsd: 0,
|
|
139
146
|
confidence: combinedConfidence(scopedRecords.map((record) => record.costConfidence)),
|
|
140
147
|
recommendedAction: `Inspect representative ${operationLabel} prompts locally and run a matched before/after with the same acceptance criteria before proposing one reversible context change.`,
|
|
141
148
|
verificationNeeded: "Measure token and quality deltas on matched calls; no savings counterfactual is present yet."
|
|
142
149
|
}];
|
|
143
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* A breakdown key rendered for a HUMAN. The same slot holds a client, a
|
|
153
|
+
* project, an agent, a model or an operation depending on which grouping won,
|
|
154
|
+
* so it takes the dimension-neutral marker.
|
|
155
|
+
*
|
|
156
|
+
* Display only. The raw key is still what `records.filter(...)` matches on —
|
|
157
|
+
* rewriting a matching key would silently empty the cohort behind the finding.
|
|
158
|
+
*/
|
|
159
|
+
function safeEntity(value) {
|
|
160
|
+
return safeUntrustedLabel(value, WITHHELD_ENTITY_LABEL);
|
|
161
|
+
}
|
|
144
162
|
function topBreakdown(records, select) {
|
|
145
163
|
return breakdown(records, select)[0];
|
|
146
164
|
}
|
package/dist/planMath.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import { safeUntrustedLabel, WITHHELD_ENTITY_LABEL, WITHHELD_PLAN_LABEL } from "./untrustedLabel.js";
|
|
2
|
+
/**
|
|
3
|
+
* The plan label and the limit signal are read out of the agent's own local
|
|
4
|
+
* config files, so both are untrusted text that lands mid-sentence in a
|
|
5
|
+
* headline the readout, the report and `doctor` all print verbatim.
|
|
6
|
+
*/
|
|
7
|
+
function safePlanLabel(value) {
|
|
8
|
+
return safeUntrustedLabel(value, WITHHELD_PLAN_LABEL);
|
|
9
|
+
}
|
|
10
|
+
function safeLimitSignal(value) {
|
|
11
|
+
return safeUntrustedLabel(value, WITHHELD_ENTITY_LABEL);
|
|
12
|
+
}
|
|
1
13
|
export const subscriptionPlans = [
|
|
2
14
|
{ id: "claude-pro", provider: "anthropic", agent: "claude-code", name: "Claude Pro", monthlyUsd: 20, coversUpToUsd: 50 },
|
|
3
15
|
{ id: "claude-max-5x", provider: "anthropic", agent: "claude-code", name: "Claude Max 5x", monthlyUsd: 100, coversUpToUsd: 250 },
|
|
@@ -61,21 +73,21 @@ export function computePlanChecks(records, detectedPlans = []) {
|
|
|
61
73
|
const nextTier = subscriptionPlans.find((plan) => plan.agent === agent && plan.coversUpToUsd > detectedKnown.coversUpToUsd);
|
|
62
74
|
// A local limit signal upgrades "might hit limits" to hard evidence.
|
|
63
75
|
const evidence = detected?.limitSignal
|
|
64
|
-
? `local metadata reports ${detected.limitSignal}`
|
|
76
|
+
? `local metadata reports ${safeLimitSignal(detected.limitSignal)}`
|
|
65
77
|
: `if the provider reports active rate limits`;
|
|
66
78
|
upgradeHint = nextTier
|
|
67
79
|
? `API-equivalent projection exceeds the rough ${detectedKnown.name} comparison threshold (~$${detectedKnown.coversUpToUsd}/mo); ${evidence}. ${nextTier.name} ($${nextTier.monthlyUsd}/mo) is the next listed tier, but verify account limits before changing plans; trimming context (below) may buy headroom.`
|
|
68
80
|
: `API-equivalent projection exceeds the rough ${detectedKnown.name} comparison threshold (~$${detectedKnown.coversUpToUsd}/mo); verify account limits before changing plans. Trimming context (below) may buy headroom.`;
|
|
69
81
|
}
|
|
70
82
|
else if (detected?.limitSignal) {
|
|
71
|
-
upgradeHint = `local metadata reports ${detected.limitSignal}; verify the live provider window. Trimming context (below) may buy headroom.`;
|
|
83
|
+
upgradeHint = `local metadata reports ${safeLimitSignal(detected.limitSignal)}; verify the live provider window. Trimming context (below) may buy headroom.`;
|
|
72
84
|
}
|
|
73
85
|
}
|
|
74
86
|
else if (detected) {
|
|
75
87
|
// Detected a plan we can't price (e.g. an unrecognized tier): state the
|
|
76
88
|
// fact, then fall back to suggestion math without pretending certainty.
|
|
77
89
|
headline =
|
|
78
|
-
`${agent}: ~${formatUsd(monthly)}/mo at API rates (${basis}) — compared with ${detected.planLabel} ` +
|
|
90
|
+
`${agent}: ~${formatUsd(monthly)}/mo at API rates (${basis}) — compared with ${safePlanLabel(detected.planLabel)} ` +
|
|
79
91
|
`(label detected locally; price not in our table)` +
|
|
80
92
|
(suggested ? `; reference listed plan: ${suggested.name} ($${suggested.monthlyUsd}/mo).` : `.`);
|
|
81
93
|
}
|
|
@@ -100,7 +112,16 @@ export function computePlanChecks(records, detectedPlans = []) {
|
|
|
100
112
|
suggestedPlan: detectedKnown ?? suggested,
|
|
101
113
|
monthlySavingsVsApiUsd: effectiveSavings,
|
|
102
114
|
valueMultiple,
|
|
103
|
-
|
|
115
|
+
// The STRUCTURED sibling of the headline. Neutralizing the sentence and
|
|
116
|
+
// shipping the raw label beside it in the same object is the inversion
|
|
117
|
+
// that let a hostile name reach an agent while the human saw a redaction.
|
|
118
|
+
detectedPlan: detected === undefined ? undefined : {
|
|
119
|
+
...detected,
|
|
120
|
+
planLabel: safePlanLabel(detected.planLabel),
|
|
121
|
+
...(detected.limitSignal === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { limitSignal: safeLimitSignal(detected.limitSignal) })
|
|
124
|
+
},
|
|
104
125
|
upgradeHint,
|
|
105
126
|
headline
|
|
106
127
|
});
|