sprag-cli 3.55.0 → 3.56.0

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/src/route-scan.js CHANGED
@@ -25,7 +25,8 @@ import { collectSessionRecords } from './session-records.js';
25
25
  import {
26
26
  collectSubagentRuns, indexRuns, exactRunsForEpisode, fallbackRunsForEpisode,
27
27
  } from './subagent-records.js';
28
- import { estimateCost, modelRank, isRecognizedModelId, TIER_TARGET_RANK, tierForRank } from './cost.js';
28
+ import { modelRank, isRecognizedModelId, TIER_TARGET_RANK, tierForRank } from './cost.js';
29
+ import { sessionCost } from './claude-price.js';
29
30
  import { learnProfileMapping, resetModelAliasCache } from './model-alias.js';
30
31
  import { modelRuleBaseText } from './model-rules.js';
31
32
 
@@ -115,7 +116,8 @@ const CATEGORIES = [
115
116
  label: '탐색·조회 (파일/값 찾기)',
116
117
  labelEn: 'lookup (finding files/values)',
117
118
  agent: 'haiku-explore',
118
- kw: [[/grep|검색|search|find/i, 2], [/찾아|어디|위치|목록|살펴/i, 1]],
119
+ kw: [[/grep|검색|search|find/i, 2], [/찾아|어디|위치|목록|살펴/i, 1],
120
+ [/정의(?:돼|되어)\s*있|어느\s*파일|어떤\s*파일에|\bwhich\s+file\b|\bwhere\s+(?:is|are)\b[^?\n]*\bdefined\b/i, 1]],
119
121
  },
120
122
  {
121
123
  id: 'read',
@@ -333,9 +335,13 @@ export function worthDelegating(tier, rank) {
333
335
  * Approximation, deliberately stated as one: it holds token counts constant,
334
336
  * which a cheaper model would not reproduce exactly. Directionally right and
335
337
  * enough to rank rules by value, so it is rendered as "~$X".
338
+ *
339
+ * Signed: a run that moved to a pricier model is a loss and comes back
340
+ * negative. null means a side has no price (a gateway session whose /model/info
341
+ * table lacks the model), which is unpriced, not zero.
336
342
  */
337
343
  export function runSaving(run, mainModel) {
338
- if (!run.model || !mainModel) return 0;
344
+ if (!run.model || !mainModel) return null;
339
345
  const totals = {
340
346
  input: run.input,
341
347
  cacheCreation: run.cacheCreation,
@@ -344,9 +350,10 @@ export function runSaving(run, mainModel) {
344
350
  ephemeral1h: run.ephemeral1h,
345
351
  output: run.out,
346
352
  };
347
- const actual = estimateCost(totals, run.model).actual;
348
- const counterfactual = estimateCost(totals, mainModel).actual;
349
- return Math.max(0, counterfactual - actual);
353
+ const actual = sessionCost(totals, run.model);
354
+ const counterfactual = sessionCost(totals, mainModel);
355
+ if (!actual || !counterfactual) return null;
356
+ return counterfactual.actual - actual.actual;
350
357
  }
351
358
 
352
359
  const clamp = (v, [lo, hi]) => Math.min(hi, Math.max(lo, v));
@@ -549,7 +556,7 @@ export async function runRouteScan({ days = 14 } = {}) {
549
556
  d.runs += 1;
550
557
  if (isFailedRun(run)) d.errRuns += 1;
551
558
  d.outTokens += run.out || 0;
552
- d.savedUsd += saved;
559
+ if (Number.isFinite(saved)) d.savedUsd += saved; // unpriced runs add nothing
553
560
  delegatedStats.set(key, d);
554
561
  };
555
562
  // Ledger events feed the statusline's weekly/monthly "Routing saved"
@@ -759,7 +766,8 @@ export async function runRouteScan({ days = 14 } = {}) {
759
766
  // profile-map.json's `modelAliases` to bring these runs back in.
760
767
  if (!baseline || !isRecognizedModelId(baseline)) continue;
761
768
  const usd = runSaving(e.run, baseline);
762
- if (usd <= 0) continue;
769
+ // A loss is recorded too; only an unpriced run (null) has nothing to record.
770
+ if (!Number.isFinite(usd)) continue;
763
771
  priced.push({
764
772
  key: e.run.path,
765
773
  ts: e.run.endedAt ?? e.run.startedAt ?? Date.now(),
@@ -29,6 +29,9 @@ import { userDataDir } from './paths.js';
29
29
  const WEEK_MS = 7 * 24 * 3600 * 1000;
30
30
  const MONTH_MS = 30 * 24 * 3600 * 1000;
31
31
 
32
+ /** Stored USD is kept to 4 decimals so re-scans compare equal and files stay small. */
33
+ export const roundUsd = (usd) => Math.round(Number(usd) * 10000) / 10000;
34
+
32
35
  export function ledgerPath() {
33
36
  return join(userDataDir(), 'delegation-ledger.json');
34
37
  }
@@ -55,8 +58,9 @@ export function loadLedger() {
55
58
  * Upsert saving events. `events` is an array of
56
59
  * { key, ts, usd, rule, from, to } where `key` is the run's transcript path
57
60
  * (unique per subagent run) and `from`/`to` name the models the routing
58
- * decision moved between. Zero-saving runs are skipped — they carry no
59
- * information the totals care about.
61
+ * decision moved between. `usd` is signed: a run on a pricier model is a loss
62
+ * and is recorded as a negative amount so it reduces the totals. Only
63
+ * non-finite amounts (unpriced runs) are skipped.
60
64
  */
61
65
  export function recordDelegationEvents(events) {
62
66
  if (!Array.isArray(events) || events.length === 0) return;
@@ -64,9 +68,9 @@ export function recordDelegationEvents(events) {
64
68
  data.version = LEDGER_VERSION;
65
69
  let changed = false;
66
70
  for (const e of events) {
67
- if (!e || !e.key || !(Number(e.usd) > 0) || !Number.isFinite(e.ts)) continue;
71
+ if (!e || !e.key || typeof e.usd !== 'number' || !Number.isFinite(e.usd) || !Number.isFinite(e.ts)) continue;
68
72
  const prev = data.events[e.key];
69
- const usd = Math.round(Number(e.usd) * 10000) / 10000;
73
+ const usd = roundUsd(e.usd);
70
74
  if (prev && prev.ts === e.ts && prev.usd === usd) continue;
71
75
  data.events[e.key] = {
72
76
  ts: e.ts,
@@ -115,8 +119,8 @@ export function delegationSavedTotals(now = Date.now()) {
115
119
  const byPair = new Map();
116
120
  try {
117
121
  for (const e of Object.values(loadLedger().events)) {
118
- const usd = Number(e.usd) || 0;
119
- if (usd <= 0) continue;
122
+ const usd = Number(e.usd);
123
+ if (!Number.isFinite(usd)) continue;
120
124
  totals.total += usd;
121
125
  if (Number.isFinite(e.ts)) {
122
126
  if (now - e.ts <= WEEK_MS) totals.week += usd;