tokentracker-cli 1.5.1 → 1.5.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.
Files changed (130) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-mB7HZSGE.js → AchievementBadge-uuDGO9Mq.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DLzeq_eo.js → AchievementsPage-BCTVT7Jo.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DrXQyDns.js → AchievementsSection-B6fJSJSr.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-WrgKRybi.js → BadgeDetailModal-FjUN__9G.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-DFRaixVK.js → BotAnimated-B-KqY_Wr.js} +1 -1
  6. package/dashboard/dist/assets/{Card-bFq2tlO8.js → Card-DS8T8piP.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-gwhGxavy.js → CloudCheckoutPage-_0UjsFPu.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-BD4iBYeZ.js → CloudPage-BstgVBaV.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-C_KlPl0U.js → CommandPalette-DtVsYZgc.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-Cl1Bgv8M.js → CommunityStatsModal-DTpEJqV3.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-NWoYevYC.js → ConfirmModal-Lfdh2vr7.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-D2-D2cfA.js → Controls-DpwQ7pkq.js} +1 -1
  13. package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +1 -0
  14. package/dashboard/dist/assets/{DateRangePopover-8EMVXuH6.js → DateRangePopover-CrWgrzT2.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-DbP8d6bM.js → DevicePage-DbK5KWJS.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-CEstgLa1.js → DialogClose-Dd2B_o03.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-DP5TLLbz.js → DialogTitle-3JQBVha3.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-Uvi3yMum.js → ElectricLogo-LYAqG8pG.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-Dpzihg6f.js → FadeIn-DhDvUor7.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-DllqemKr.js → HeaderGithubStar-9f1XmgYC.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-Dc-TAH6l.js → HoverTooltip-BXhHAkP7.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-jd1D5ZKX.js → IpCheckPage-ChPdDeMl.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-CB1ZVw-a.js → LandingPage-DnJ7Y9zw.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-Bt2cRRmE.js → LeaderboardPage-BF9isLzu.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-BVlyVOFf.js → LeaderboardProBadge-BM4JKgkC.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-CwQFRJg_.js → LeaderboardProfileModal-DfZABOpN.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CeqcTbUG.js → LeaderboardProfilePage-BaZ38iT5.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-B2_HoCVD.js → LocalOnlyNotice-DvhlYoqQ.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-nw9JOc2d.js → LoginCard-g_Gbnzhs.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-BHNLTK1D.js → LoginModal-DkJrI6b7.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-LI2GZKhM.js → LoginPage-CqCKSo0n.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-CK6sPeI0.js → PetPage-DbF7ROsj.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-BsXchEVa.js → PopoverPopup-4WuH41ZT.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-vgCEy0dh.js → ProviderIcon-6j0A2KD9.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-B4CPj2vX.js → ResetPasswordPage-DBd-iwuM.js} +1 -1
  38. package/dashboard/dist/assets/{Select-BkAy6P2r.js → Select-BBEpLeiG.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-C0RNqDrD.js → SelectItemText-Cn_U_rrO.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-B-_w-Z-a.js → SelfHostPage-2WZc6Rml.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +1 -0
  42. package/dashboard/dist/assets/{SessionsPage-CmWXBqD2.js → SessionsPage-CWS0iQ1L.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-CA70zP-t.js → SettingsPage-rXNr96i-.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CNamhfzU.js → SkillsPage-CudTvbZL.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-CXrVBSk6.js → TokenGalaxy-BRKrbTsJ.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-CR4MqElX.js → TooltipPopup-CsLKXhWC.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-DQfcrEve.js → TrendMonitor-8onUTbkk.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-BBXxtZbP.js → WidgetsPage-CcwE0lz8.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-C8i2KlMF.js → WrappedPage-Cj9LLYqP.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +45 -0
  51. package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +1 -0
  52. package/dashboard/dist/assets/{agent-logos-CxQb8vir.js → agent-logos-ChBA1nAH.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-D6zkcLVt.js → arrow-left-Bb5mfoPc.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-BGb-XVn_.js → arrow-right-D1Ti2XUI.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-XTStagDJ.js → arrow-up-right-B_CjA_y8.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-Cz3NGZiS.js → calendar-days-DdfrAqie.js} +1 -1
  57. package/dashboard/dist/assets/{check-1z_vmHPD.js → check-29zGw1MK.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-CkSBY3bE.js → chevron-down-DAzlFX7g.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-pD5C4r_S.js → cloud-checkout-C2SeCjFM.js} +1 -1
  60. package/dashboard/dist/assets/{composite-BtDzx3a2.js → composite-DIf735Hq.js} +1 -1
  61. package/dashboard/dist/assets/{copy-Ctv_wxuD.js → copy-BOuOM0HQ.js} +1 -1
  62. package/dashboard/dist/assets/{download-DKbKMGSR.js → download-50tQRpGe.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-g9cU017t.js → external-link-BhlQClVe.js} +1 -1
  64. package/dashboard/dist/assets/{flame-DZq3kXUt.js → flame-CeGfA-0L.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-CXQfx0PC.js → format-tokens-Cq7YiAkJ.js} +1 -1
  66. package/dashboard/dist/assets/{gift-CKlKjGP6.js → gift-Do-UFhiT.js} +1 -1
  67. package/dashboard/dist/assets/{icons-xPGMnBjl.js → icons-C6EZ_HmP.js} +1 -1
  68. package/dashboard/dist/assets/{index-fVbP5wOv.js → index-BFrbVqsm.js} +1 -1
  69. package/dashboard/dist/assets/{index-CDBOKcDh.js → index-BWhbBAd9.js} +1 -1
  70. package/dashboard/dist/assets/{index-Dl0oFLNr.js → index-D9kwYWLX.js} +1 -1
  71. package/dashboard/dist/assets/{index-BwOjMtB2.js → index-c0crpe9Z.js} +1 -1
  72. package/dashboard/dist/assets/{info-D9xNIuGh.js → info-B4MXmsNE.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-Bk5wYt2n.js → link-2-DuWCslzl.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-CcN3pHWb.js → loader-circle-C2I3NZAO.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-CYcy1ici.js → local-usage-transfer-BQasvsUF.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-DbpEzkJe.js → modifiers.esm-DbeNS6FT.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-WkWv1qIS.js → monitor-smartphone-C6Y4ktsc.js} +1 -1
  78. package/dashboard/dist/assets/{play-2yZGWMes.js → play-CwgcDJC2.js} +1 -1
  79. package/dashboard/dist/assets/{plus-mOF8tG1F.js → plus-B796wsUO.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-7gaOvgMp.js → provider-display-Cw26RFDm.js} +1 -1
  81. package/dashboard/dist/assets/{react-CdPiPDhk.js → react-d7Bk1-_K.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-C7FJLAlh.js → safePolygon-CDE0dNv_.js} +1 -1
  83. package/dashboard/dist/assets/{search-C63nCrjo.js → search-DtKvsdRs.js} +1 -1
  84. package/dashboard/dist/assets/{server-BaG1rnuv.js → server-gAURHUyz.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-D_4-Cm6L.js → shield-check-DFVdDTwh.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-DfbzwpDv.js → skills-api-2rdPUrJI.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-D-v8FRX5.js → sortable.esm-DFlfwnJr.js} +1 -1
  88. package/dashboard/dist/assets/{store-Dv_FYrXK.js → store-BKPCk_de.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-FQi6MaYt.js → terminal-BtWFakvS.js} +1 -1
  90. package/dashboard/dist/assets/{trash-TbK96dEz.js → trash-bpr9zTAM.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-DGoWVZuk.js → use-community-stats-DMnGgLne.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-Dg6uSaYt.js → use-latest-request-guard-BSBlJJHe.js} +1 -1
  93. package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +1 -0
  94. package/dashboard/dist/assets/{use-native-settings-DIk0AF3b.js → use-native-settings-OthqiF6g.js} +1 -1
  95. package/dashboard/dist/assets/{use-ordered-list-CUqV3Zgl.js → use-ordered-list-DpAtr0R-.js} +1 -1
  96. package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-D_-w22c8.js → use-transform-BRz9dz_2.js} +1 -1
  98. package/dashboard/dist/assets/use-usage-limits-CwmhudAX.js +1 -0
  99. package/dashboard/dist/assets/{useCurrency-B_AuLCsL.js → useCurrency-CAA-tQ27.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-DfxvIif8.js → usePositioner-Cj2DPmwM.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-bbO13V4s.js → useTokenFormat-Bf5-f7Hx.js} +1 -1
  102. package/dashboard/dist/assets/{zap-k07-EXTK.js → zap-B8Fg6PaC.js} +1 -1
  103. package/dashboard/dist/index.html +6 -2
  104. package/dashboard/dist/ip-check.html +2 -2
  105. package/dashboard/dist/leaderboard.html +2 -2
  106. package/dashboard/dist/share.html +2 -2
  107. package/package.json +1 -1
  108. package/src/commands/serve.js +22 -13
  109. package/src/lib/codex-rollout-parser.js +43 -6
  110. package/src/lib/codex-usage-record.js +21 -0
  111. package/src/lib/deepseek-status.js +157 -0
  112. package/src/lib/local-api.js +44 -4
  113. package/src/lib/pricing/curated-overrides.json +94 -0
  114. package/src/lib/pricing/seed-snapshot.json +1 -1
  115. package/src/lib/provider-status.js +15 -2
  116. package/src/lib/qoder-limits.js +95 -23
  117. package/src/lib/rollout.js +68 -14
  118. package/src/lib/session-analytics.js +10 -3
  119. package/src/lib/usage-limits.js +187 -43
  120. package/src/lib/workbuddy-limits.js +292 -27
  121. package/src/lib/zcode-limits.js +457 -42
  122. package/dashboard/dist/assets/DashboardPage-vITMnPXx.js +0 -1
  123. package/dashboard/dist/assets/DialogDescription-0wcygxtH.js +0 -1
  124. package/dashboard/dist/assets/LimitsPage-Bp4ZORmR.js +0 -6
  125. package/dashboard/dist/assets/ServiceStatusPage-5YevsGGD.js +0 -1
  126. package/dashboard/dist/assets/__vite-browser-external-BEEzY7mq.js +0 -45
  127. package/dashboard/dist/assets/__vite-browser-external-Br-KAXh6.css +0 -1
  128. package/dashboard/dist/assets/use-limits-display-prefs-Djbdp7fi.js +0 -1
  129. package/dashboard/dist/assets/use-reduced-motion-DMCok-vZ.js +0 -1
  130. package/dashboard/dist/assets/use-usage-limits-h7aGsIQr.js +0 -1
@@ -34,6 +34,7 @@ const { readCodexServiceTier, isPriorityServiceTier, isUltrafastServiceTier } =
34
34
  const {
35
35
  createUsageRecordState,
36
36
  extractTokenUsageRecord,
37
+ isCodexTotalOnlyResetSentinel,
37
38
  noteLineAfterUsageRecord,
38
39
  noteTokenCount,
39
40
  noteUsageRecord,
@@ -72,6 +73,11 @@ const CODEX_RESUME_INVALIDATED = "TOKENTRACKER_CODEX_RESUME_INVALIDATED";
72
73
  // input (including cache), not the accumulated session or day totals.
73
74
  const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272_000;
74
75
 
76
+ // Session token summaries use the same fork-prefix rules as rollout.js; the
77
+ // reset-sentinel predicate is shared verbatim from codex-usage-record.js.
78
+ // Context/performance callers retain their existing event view.
79
+ const LEDGER_FORK_REPLAY_GAP_MS = 500;
80
+
75
81
  // ---------------------------------------------------------------------------
76
82
  // Timezone helpers
77
83
  // ---------------------------------------------------------------------------
@@ -461,7 +467,7 @@ function formatToolDisplayName(name) {
461
467
  // Usage normalization
462
468
  // ---------------------------------------------------------------------------
463
469
 
464
- function normalizeUsage(u) {
470
+ function normalizeUsage(u, { preserveReportedTotal = false } = {}) {
465
471
  const out = {};
466
472
  for (const k of [
467
473
  "input_tokens",
@@ -477,7 +483,7 @@ function normalizeUsage(u) {
477
483
  // Codex reports input inclusive of cached_input_tokens; keep our schema
478
484
  // convention: non-cached input and cached input tracked separately.
479
485
  out.input_tokens = Math.max(0, out.input_tokens - out.cached_input_tokens);
480
- out.total_tokens =
486
+ if (!preserveReportedTotal) out.total_tokens =
481
487
  out.input_tokens +
482
488
  out.cached_input_tokens +
483
489
  out.cache_creation_input_tokens +
@@ -596,6 +602,7 @@ async function parseCodexRolloutFile(filePath, {
596
602
  sourceHandle = null,
597
603
  collectBreakdowns = true,
598
604
  collectModelUsage = false,
605
+ ledgerUsage = false,
599
606
  onObject = null,
600
607
  onUsage = null,
601
608
  } = {}) {
@@ -638,6 +645,12 @@ async function parseCodexRolloutFile(filePath, {
638
645
  let provider = isResuming ? resumeState.provider || null : null;
639
646
  let cliVersion = isResuming ? resumeState.cliVersion || null : null;
640
647
  let forkedFromId = isResuming ? resumeState.forkedFromId || null : null;
648
+ const ledgerRolloutDate = path.basename(primaryFilePath).match(/^rollout-(\d{4}-\d{2}-\d{2})T/)?.[1] || null;
649
+ let ledgerCurrentDate = null;
650
+ // Same gating as rollout.js: the replay prefix only ever sits at the head of
651
+ // a freshly-forked file, so only a scan from offset 0 can see it.
652
+ let ledgerReplayActive = initialOffset === 0;
653
+ let ledgerPreviousTokenMs = null;
641
654
  let parentThreadId = isResuming ? resumeState.parentThreadId || null : null;
642
655
  let threadSource = isResuming ? resumeState.threadSource || null : null;
643
656
  let agentNickname = isResuming ? resumeState.agentNickname || null : null;
@@ -1050,6 +1063,9 @@ async function parseCodexRolloutFile(filePath, {
1050
1063
  contentHasher,
1051
1064
  sourceHandle,
1052
1065
  })) {
1066
+ if (ledgerUsage && obj?.type === "turn_context" && typeof obj?.payload?.current_date === "string") {
1067
+ ledgerCurrentDate = obj.payload.current_date.trim().match(/^(\d{4}-\d{2}-\d{2})/)?.[1] || null;
1068
+ }
1053
1069
  const modelEvent = applyCodexModelEvent(modelAttributionState, obj);
1054
1070
  const attributedModel = currentCodexModel(modelAttributionState);
1055
1071
  model = attributedModel || model;
@@ -1114,7 +1130,7 @@ async function parseCodexRolloutFile(filePath, {
1114
1130
  }
1115
1131
 
1116
1132
  if (usageRecord) {
1117
- const delta = normalizeUsage(usageRecord.usage);
1133
+ const delta = normalizeUsage(usageRecord.usage, { preserveReportedTotal: ledgerUsage });
1118
1134
  noteUsageRecord(
1119
1135
  usageRecordState,
1120
1136
  // Without a response_id a compaction cannot be deduplicated.
@@ -1147,15 +1163,36 @@ async function parseCodexRolloutFile(filePath, {
1147
1163
  );
1148
1164
  }
1149
1165
  const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
1150
- const delta = rawDelta ? normalizeUsage(rawDelta) : null;
1166
+ const delta = rawDelta && !(ledgerUsage && isCodexTotalOnlyResetSentinel(lastUsage, totalUsage))
1167
+ ? normalizeUsage(rawDelta, { preserveReportedTotal: ledgerUsage }) : null;
1151
1168
  // Timing consumers receive canonical, deduplicated usage below. A null
1152
1169
  // delta is a snapshot, not another model response.
1153
1170
  if (compaction) countCompactionRecord(compaction);
1154
1171
  const isNewResumeEvent = noteTokenEvent(ts, lastUsage, totalUsage);
1155
1172
  if (inRequestedRange) {
1156
1173
  const eventSessionId = sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath;
1157
- const eventKey = `${eventSessionId}:${ts}:${usageEventSignature(lastUsage, totalUsage)}`;
1158
- if (!isNewResumeEvent || (seenTokenEvents && seenTokenEvents.has(eventKey))) {
1174
+ const eventKey = ledgerUsage ? `${eventSessionId}:${ts}`
1175
+ : `${eventSessionId}:${ts}:${usageEventSignature(lastUsage, totalUsage)}`;
1176
+ const alreadySeen = seenTokenEvents && seenTokenEvents.has(eventKey);
1177
+ let replayed = false;
1178
+ if (ledgerUsage && forkedFromId && delta?.total_tokens > 0) {
1179
+ if (ledgerReplayActive) {
1180
+ const tokenMs = Date.parse(ts);
1181
+ if (!Number.isFinite(tokenMs) || (ledgerPreviousTokenMs !== null && tokenMs < ledgerPreviousTokenMs)) {
1182
+ ledgerReplayActive = false;
1183
+ } else {
1184
+ if (ledgerPreviousTokenMs !== null && tokenMs - ledgerPreviousTokenMs >= LEDGER_FORK_REPLAY_GAP_MS) ledgerReplayActive = false;
1185
+ replayed = ledgerReplayActive && ledgerPreviousTokenMs !== null
1186
+ && tokenMs - ledgerPreviousTokenMs < LEDGER_FORK_REPLAY_GAP_MS;
1187
+ ledgerPreviousTokenMs = tokenMs;
1188
+ }
1189
+ }
1190
+ replayed ||= Boolean(ledgerRolloutDate && ledgerCurrentDate && ledgerCurrentDate < ledgerRolloutDate);
1191
+ }
1192
+ // Remember skipped inherited rows too: another readable copy of the
1193
+ // same session must not reintroduce its prefix after the first live turn.
1194
+ if (ledgerUsage && seenTokenEvents && delta?.total_tokens > 0) seenTokenEvents.add(eventKey);
1195
+ if (!isNewResumeEvent || alreadySeen || replayed) {
1159
1196
  if (typeof onUsage === "function") onUsage({ timestamp: ts, delta: null, rawUsage: lastUsage, model: attributedModel });
1160
1197
  attributeTurn(null);
1161
1198
  } else {
@@ -176,6 +176,26 @@ function formatRecordOnlyWarning(count) {
176
176
  return `${count} Codex session(s) report usage only via token_usage_record; not counted yet (see #652)`;
177
177
  }
178
178
 
179
+ // A reset sentinel is a token_count whose total drops to exact zero while the
180
+ // previous total was positive and every component of both snapshots is zero —
181
+ // Codex restarting a session's cumulative counters, not a real response. Both
182
+ // the hourly ledger (rollout.js) and the session ledger mode
183
+ // (codex-rollout-parser.js) must reject it identically, so the predicate lives
184
+ // here as the single copy of the rule.
185
+ function isCodexTotalOnlyResetSentinel(lastUsage, totalUsage) {
186
+ const componentTotal = (usage) => [
187
+ usage?.input_tokens,
188
+ usage?.cached_input_tokens,
189
+ usage?.cache_creation_input_tokens ?? usage?.cache_write_input_tokens,
190
+ usage?.output_tokens,
191
+ usage?.reasoning_output_tokens,
192
+ ].reduce((sum, value) => sum + Math.max(0, Number(value) || 0), 0);
193
+ return Number(lastUsage?.total_tokens || 0) > 0
194
+ && Number(totalUsage?.total_tokens || 0) === 0
195
+ && componentTotal(lastUsage) === 0
196
+ && componentTotal(totalUsage) === 0;
197
+ }
198
+
179
199
  module.exports = {
180
200
  MAX_COMPACTION_RESPONSE_IDS,
181
201
  createCompactionResponseIds,
@@ -184,6 +204,7 @@ module.exports = {
184
204
  awaitsCompactedLine,
185
205
  createUsageRecordState,
186
206
  extractTokenUsageRecord,
207
+ isCodexTotalOnlyResetSentinel,
187
208
  isCodexTurnEndEvent,
188
209
  isRecordOnly,
189
210
  noteLineAfterUsageRecord,
@@ -0,0 +1,157 @@
1
+ // DeepSeek service-status probe.
2
+ //
3
+ // status.deepseek.com is a Flashcat(快猫)-hosted Next.js app. Unlike Statuspage.io
4
+ // it exposes no public JSON API and sends no CORS headers, so a browser cannot
5
+ // read it directly. The page state is, however, inlined in the server render as
6
+ // React Server Component "flight" data: the client script pushes JSON chunks via
7
+ // self.__next_f.push([1, "<json string>"]). We concatenate those chunks and read
8
+ // the single machine-readable signal — the live `active_changes` array.
9
+ //
10
+ // Semantics (Flashcat change status -> Statuspage.io indicator):
11
+ // operational -> none
12
+ // degraded / degraded_performance / maintenance -> minor
13
+ // partial_outage -> major
14
+ // major_outage / full_outage -> critical
15
+ // An empty active_changes array means everything is operational. Anything we
16
+ // cannot parse throws, so the caller falls back instead of inventing "all good".
17
+
18
+ const DEEPSEEK_STATUS_URL = "https://status.deepseek.com/";
19
+
20
+ const RSC_PUSH_RE = /self\.__next_f\.push\(\[1,("(?:[^"\\]|\\.)*")\]\)/g;
21
+
22
+ const CHANGE_STATUS_TO_INDICATOR = {
23
+ operational: "none",
24
+ degraded: "minor",
25
+ degraded_performance: "minor",
26
+ maintenance: "minor",
27
+ under_maintenance: "minor",
28
+ partial_outage: "major",
29
+ major_outage: "critical",
30
+ full_outage: "critical",
31
+ outage: "critical",
32
+ };
33
+
34
+ const INDICATOR_RANK = { none: 0, minor: 1, major: 2, critical: 3 };
35
+
36
+ function extractFlight(html) {
37
+ if (typeof html !== "string" || html.length === 0) {
38
+ throw new Error("DeepSeek status: expected an HTML document");
39
+ }
40
+ let flight = "";
41
+ let chunks = 0;
42
+ RSC_PUSH_RE.lastIndex = 0;
43
+ let match;
44
+ while ((match = RSC_PUSH_RE.exec(html))) {
45
+ try {
46
+ flight += JSON.parse(match[1]);
47
+ } catch (_error) {
48
+ throw new Error("DeepSeek status: malformed RSC chunk");
49
+ }
50
+ chunks += 1;
51
+ }
52
+ if (chunks === 0) {
53
+ throw new Error("DeepSeek status: no RSC data found");
54
+ }
55
+ return flight;
56
+ }
57
+
58
+ // Return the balanced JSON array that starts at `start` (a '[' in flight).
59
+ function sliceJsonArray(flight, start) {
60
+ let depth = 0;
61
+ let inString = false;
62
+ let escaped = false;
63
+ for (let i = start; i < flight.length; i += 1) {
64
+ const ch = flight[i];
65
+ if (inString) {
66
+ if (escaped) escaped = false;
67
+ else if (ch === "\\") escaped = true;
68
+ else if (ch === '"') inString = false;
69
+ continue;
70
+ }
71
+ if (ch === '"') inString = true;
72
+ else if (ch === "[") depth += 1;
73
+ else if (ch === "]") {
74
+ depth -= 1;
75
+ if (depth === 0) return flight.slice(start, i + 1);
76
+ }
77
+ }
78
+ throw new Error("DeepSeek status: unterminated active_changes array");
79
+ }
80
+
81
+ function readUpdatedAt(flight) {
82
+ const match = /"initialDataUpdatedAt":(\d{10,})/.exec(flight);
83
+ if (!match) return undefined;
84
+ const ms = Number(match[1]);
85
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
86
+ }
87
+
88
+ /**
89
+ * Parse the status.deepseek.com HTML into a Statuspage.io-shaped payload:
90
+ * { status: { indicator, description }, page: { updated_at } }
91
+ * Throws when the page structure is not understood, so callers fall back
92
+ * instead of reporting a false "operational".
93
+ */
94
+ function parseDeepSeekStatusPage(html) {
95
+ const flight = extractFlight(html);
96
+ const marker = '"active_changes":';
97
+ const markerIndex = flight.indexOf(marker);
98
+ if (markerIndex === -1) {
99
+ throw new Error("DeepSeek status: active_changes not found");
100
+ }
101
+ // The value must be an array. If upstream ever reports null / an object /
102
+ // anything else, refuse to guess a healthier-looking array further along the
103
+ // document — throwing keeps the caller on its last-good / "unknown" path
104
+ // instead of inventing a false "operational".
105
+ let arrayStart = markerIndex + marker.length;
106
+ while (arrayStart < flight.length && /\s/.test(flight[arrayStart])) arrayStart += 1;
107
+ if (flight[arrayStart] !== "[") {
108
+ throw new Error("DeepSeek status: active_changes is not an array");
109
+ }
110
+ let changes;
111
+ try {
112
+ changes = JSON.parse(sliceJsonArray(flight, arrayStart));
113
+ } catch (error) {
114
+ throw new Error("DeepSeek status: cannot parse active_changes (" + error.message + ")");
115
+ }
116
+ if (!Array.isArray(changes)) {
117
+ throw new Error("DeepSeek status: active_changes is not an array");
118
+ }
119
+
120
+ const updatedAt = readUpdatedAt(flight);
121
+ if (changes.length === 0) {
122
+ return { status: { indicator: "none", description: "" }, page: { updated_at: updatedAt } };
123
+ }
124
+
125
+ let worstIndicator = "none";
126
+ let worstRank = -1;
127
+ let descriptions = [];
128
+ for (const change of changes) {
129
+ if (!change || typeof change !== "object") continue;
130
+ const rawStatus = String(change.status || change.change_status || change.impact || "").toLowerCase();
131
+ const indicator = CHANGE_STATUS_TO_INDICATOR[rawStatus] || "minor";
132
+ const rank = INDICATOR_RANK[indicator];
133
+ const description = typeof change.name === "string" ? change.name
134
+ : typeof change.title === "string" ? change.title
135
+ : typeof change.description === "string" ? change.description
136
+ : "";
137
+ if (rank > worstRank) {
138
+ worstRank = rank;
139
+ worstIndicator = indicator;
140
+ descriptions = description ? [description] : [];
141
+ } else if (rank === worstRank && description) {
142
+ descriptions.push(description);
143
+ }
144
+ }
145
+ // A non-empty array with no readable change is still "something is wrong".
146
+ if (worstRank < 0) worstIndicator = "minor";
147
+
148
+ return {
149
+ status: { indicator: worstIndicator, description: descriptions.join("; ") },
150
+ page: { updated_at: updatedAt },
151
+ };
152
+ }
153
+
154
+ module.exports = {
155
+ parseDeepSeekStatusPage,
156
+ DEEPSEEK_STATUS_URL,
157
+ };
@@ -20,6 +20,7 @@ const { createCloudDeviceTokenStore } = require("./cloud-device-token");
20
20
  const { parseBearerToken } = require("./bearer-token");
21
21
  const { writeJsonAtomicSync } = require("./atomic-json");
22
22
  const { createLocalUsageTransfer, MAX_BYTES: LOCAL_USAGE_MAX_BYTES } = require("./local-usage-transfer");
23
+ const { fetchProviderServiceStatus } = require("./provider-status");
23
24
 
24
25
  const SYNC_TIMEOUT_MS = 120_000;
25
26
  // A failed account-view request should not stall every dashboard refresh
@@ -1677,6 +1678,32 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
1677
1678
 
1678
1679
  return async function handleLocalApi(req, res, url) {
1679
1680
  const p = url.pathname;
1681
+ // status.deepseek.com has no public JSON API or CORS, so the dashboard
1682
+ // cannot probe it like the other providers. Relay it through the local app:
1683
+ // fetchProviderServiceStatus("deepseek") fetches + parses the server-rendered
1684
+ // page (with a 5-minute cache and 6s timeout in provider-status.js) and we
1685
+ // return a Statuspage.io-shaped payload the existing dashboard parser reads.
1686
+ if (p === "/api/deepseek-status") {
1687
+ const method = String(req.method || "GET").toUpperCase();
1688
+ if (method !== "GET" && method !== "HEAD") {
1689
+ json(res, { error: "Method Not Allowed" }, 405);
1690
+ return true;
1691
+ }
1692
+ try {
1693
+ const reading = await fetchProviderServiceStatus("deepseek");
1694
+ if (!reading) {
1695
+ json(res, { error: "DeepSeek status unavailable" }, 502);
1696
+ return true;
1697
+ }
1698
+ json(res, {
1699
+ page: { updated_at: reading.updated_at },
1700
+ status: { indicator: reading.indicator, description: reading.description || "" },
1701
+ });
1702
+ } catch (error) {
1703
+ json(res, { error: `DeepSeek status probe error: ${error?.message || error}` }, 502);
1704
+ }
1705
+ return true;
1706
+ }
1680
1707
  if (p === "/api/local-usage") {
1681
1708
  if (!isAuthorizedLocalMutation(req)) { json(res, { code: "unauthorized" }, 401); return true; }
1682
1709
  try {
@@ -2718,7 +2745,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2718
2745
  const src = row.source || "unknown";
2719
2746
  const mdl = row.model || "unknown";
2720
2747
  if (!bySource.has(src))
2721
- bySource.set(src, { source: src, source_scope: getSourceScope(src), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, total_cost_usd: "0" }, models: new Map() });
2748
+ bySource.set(src, { source: src, source_scope: getSourceScope(src), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, conversation_count: 0, total_cost_usd: "0" }, models: new Map() });
2722
2749
  const sa = bySource.get(src);
2723
2750
  sa.totals.total_tokens += row.total_tokens || 0;
2724
2751
  sa.totals.billable_total_tokens += row.billable_total_tokens ?? row.total_tokens ?? 0;
@@ -2727,8 +2754,12 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2727
2754
  sa.totals.cached_input_tokens += row.cached_input_tokens || 0;
2728
2755
  sa.totals.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
2729
2756
  sa.totals.reasoning_output_tokens += row.reasoning_output_tokens || 0;
2757
+ // Request-only sources (#948: credit-billed transcripts that report no
2758
+ // tokens) carry their activity solely as conversation_count — thread it
2759
+ // through so the dashboard can surface "0 tokens / N requests".
2760
+ sa.totals.conversation_count += row.conversation_count || 0;
2730
2761
  if (!sa.models.has(mdl))
2731
- sa.models.set(mdl, { model: mdl, model_id: mdl, pricing: getModelPricingInfo(mdl, { source: src }), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, total_cost_usd: "0" } });
2762
+ sa.models.set(mdl, { model: mdl, model_id: mdl, pricing: getModelPricingInfo(mdl, { source: src }), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, conversation_count: 0, total_cost_usd: "0" } });
2732
2763
  const ma = sa.models.get(mdl);
2733
2764
  const reportedCost = ["grok", "cline"].includes(src) && Number(row.total_cost_usd) > 0;
2734
2765
  const costSource = reportedCost ? "provider_reported" : "model_pricing";
@@ -2740,6 +2771,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2740
2771
  ma.totals.cached_input_tokens += row.cached_input_tokens || 0;
2741
2772
  ma.totals.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
2742
2773
  ma.totals.reasoning_output_tokens += row.reasoning_output_tokens || 0;
2774
+ ma.totals.conversation_count += row.conversation_count || 0;
2743
2775
  // Price each row, exactly as aggregateByDay does for the hero (#653).
2744
2776
  // computeRowCost is not linear for models with a Fast tier or
2745
2777
  // long-context rates: priority_* / long_context_* mark a subset of the
@@ -3672,7 +3704,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
3672
3704
 
3673
3705
  // --- usage-limits ---
3674
3706
  if (p === "/functions/tokentracker-usage-limits") {
3675
- const { getUsageLimits, resetUsageLimitsCache } = require("./usage-limits");
3707
+ const { getUsageLimits, resetUsageLimitsCache, normalizeHiddenProviders } = require("./usage-limits");
3676
3708
  try {
3677
3709
  // Devin quota is opt-in (Settings > Usage & Limits > Providers). An
3678
3710
  // explicit devin=1 without local authentication is rejected before any
@@ -3683,7 +3715,14 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
3683
3715
  const localAuthorized = isAuthorizedLocalMutation(req);
3684
3716
  const devinParam = url.searchParams.get("devin");
3685
3717
  const devinEnabled = devinParam === "1" || devinParam === "true";
3686
- if (devinEnabled && !localAuthorized) {
3718
+ // Provider visibility gate (#956): hidden=<csv> names provider rows to
3719
+ // skip entirely — no upstream call, no credential read. Like devin=1,
3720
+ // an explicit list is honored only together with the loopback auth
3721
+ // header (letting an unauthenticated caller scope the fetch would let
3722
+ // it pick exactly which credentials to leave alone). Unknown ids are
3723
+ // dropped by the normalizer, so the gate only ever narrows.
3724
+ const hiddenProviders = normalizeHiddenProviders(url.searchParams.get("hidden"));
3725
+ if ((devinEnabled || hiddenProviders) && !localAuthorized) {
3687
3726
  json(res, { error: "Unauthorized" }, 401);
3688
3727
  return true;
3689
3728
  }
@@ -3701,6 +3740,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
3701
3740
  // cooldown) — an explicit user refresh should hit upstream.
3702
3741
  forceRefresh,
3703
3742
  devinEnabled,
3743
+ hiddenProviders,
3704
3744
  });
3705
3745
  json(res, data);
3706
3746
  } catch (e) {
@@ -75,6 +75,27 @@
75
75
  "cache_write": 6.25,
76
76
  "note": "Opus 4.8 not yet in LiteLLM. Pin to the standard Opus tier (same as 4.6/4.7); remove once LiteLLM carries it."
77
77
  },
78
+ "claude-opus-4-5": {
79
+ "input": 5,
80
+ "output": 25,
81
+ "cache_read": 0.5,
82
+ "cache_write": 6.25,
83
+ "note": "Canonical undated Opus 4.5 alias (LiteLLM bare key, sourced from https://platform.claude.com/docs/en/about-claude/pricing, cached 2026-10-31): standard Opus tier $5/$25 per MTok, cache_read 0.1x, cache_write 1.25x — same rates as claude-opus-4-5-20250414. Previously LiteLLM-only; promoted so cloud account views price the bare id (issue #929)."
84
+ },
85
+ "claude-opus-4-6": {
86
+ "input": 5,
87
+ "output": 25,
88
+ "cache_read": 0.5,
89
+ "cache_write": 6.25,
90
+ "note": "Opus 4.6 standard tier (LiteLLM entry sourced from https://platform.claude.com/docs/en/about-claude/pricing, cached 2026-10-31): $5/$25 per MTok, cache_read 0.1x, cache_write 1.25x. The edge table pins the same rate; curated pins it so local billing and -thinking variants no longer depend on LiteLLM fuzzy matching (issue #929)."
91
+ },
92
+ "claude-sonnet-4-6": {
93
+ "input": 3,
94
+ "output": 15,
95
+ "cache_read": 0.3,
96
+ "cache_write": 3.75,
97
+ "note": "Sonnet 4.6 standard tier (LiteLLM entry sourced from https://platform.claude.com/docs/en/about-claude/pricing, cached 2026-10-31): $3/$15 per MTok, cache_read 0.1x, cache_write 1.25x. The edge table pins the same rate; curated pins it so local billing no longer depends on the LiteLLM cache (issue #929)."
98
+ },
78
99
  "claude-sonnet-5": {
79
100
  "input": 2,
80
101
  "output": 10,
@@ -137,6 +158,13 @@
137
158
  "priority_multiplier": 2,
138
159
  "note": "Official Standard USD/MTok rates verified 2026-09-24: https://developers.openai.com/api/docs/models/gpt-6-sol. Same rules as gpt-6-astra: requests above 272K input tokens use 2x input/cache and 1.5x output for the full request, applied only to observed long-context request subsets; Fast mode is published as 2x the applicable rates (priority_multiplier). Reasoning-effort suffixes use the same base rates."
139
160
  },
161
+ "gpt-6-luna": {
162
+ "input": 0.1,
163
+ "output": 0.5,
164
+ "cache_read": 0.01,
165
+ "cache_write": 0.125,
166
+ "note": "GPT-6 Luna lightweight tier, Standard USD/MTok (LiteLLM entry sourced from https://developers.openai.com/api/docs/pricing, cached 2026-10-31): $0.10/$0.50 per MTok, $0.01 cache read, $0.125 cache write. Distinct from gpt-5.6-luna. Previously LiteLLM-only, so cloud account views showed it unpriced; promoted so cloud prices it and curated/edge parity guards the key (issue #929)."
167
+ },
140
168
  "gpt-5.6-sol": {
141
169
  "input": 4,
142
170
  "output": 20,
@@ -158,6 +186,42 @@
158
186
  "cache_write": 0.25,
159
187
  "note": "GPT-5.6 lightweight tier. Official short-context rates: $0.20/$1.20 per MTok (previous curated values $1/$6 overcounted 5x). See gpt-5.6-sol."
160
188
  },
189
+ "gpt-5.5": {
190
+ "input": 5,
191
+ "output": 30,
192
+ "cache_read": 0.5,
193
+ "note": "Official Standard USD/MTok rates (LiteLLM entry sourced from https://developers.openai.com/api/docs/pricing, cached 2026-10-31): $5/$30 per MTok, $0.50 cache read, no published cache-write rate. The largest production LiteLLM-only model — codex's primary SKU ($1,118 of cloud account usage showed unpriced between 2026-01-01 and 2026-10-31); promoted so cloud account views price it and curated/edge parity guards the key (issue #929)."
194
+ },
195
+ "gpt-5.4": {
196
+ "input": 2.5,
197
+ "output": 15,
198
+ "cache_read": 0.25,
199
+ "note": "Official Standard USD/MTok rates (LiteLLM entry sourced from https://developers.openai.com/api/docs/pricing, cached 2026-10-31): $2.50/$15 per MTok, $0.25 cache read. The edge table has pinned this rate since before v1.5.1; curated pins the same value so local billing no longer depends on the LiteLLM cache (issue #929). -pro/-nano/-mini keep their own SKUs."
200
+ },
201
+ "gpt-5.4-pro": {
202
+ "input": 30,
203
+ "output": 180,
204
+ "cache_read": 3,
205
+ "note": "Official Pro USD/MTok rates, matching the edge table's gpt-5.4-pro entry ($30/$180, $3 cache read; https://developers.openai.com/api/docs/pricing). Companion pin for the gpt-5.4 promotion: without a curated Pro entry, the curated contained match captures provider-qualified dated snapshots (openai/gpt-5.4-pro-2026-03-05) at the base $2.50/$15 rate before the LiteLLM reverse lookup reaches the Pro SKU — 12x under local vs edge (issue #929)."
206
+ },
207
+ "gpt-5.4-mini": {
208
+ "input": 0.75,
209
+ "output": 4.5,
210
+ "cache_read": 0.075,
211
+ "note": "Official Standard USD/MTok rates (LiteLLM entry sourced from https://developers.openai.com/api/docs/pricing, cached 2026-10-31): $0.75/$4.50 per MTok, $0.075 cache read. Same promotion rationale as gpt-5.4 (issue #929)."
212
+ },
213
+ "gpt-5.2": {
214
+ "input": 1.75,
215
+ "output": 14,
216
+ "cache_read": 0.175,
217
+ "note": "Official Standard USD/MTok rates (LiteLLM entry sourced from https://developers.openai.com/api/docs/pricing, cached 2026-10-31): $1.75/$14 per MTok, $0.175 cache read. Same promotion rationale as gpt-5.4 (issue #929); -pro stays on its own 10x LiteLLM SKU."
218
+ },
219
+ "gpt-5.3-codex-spark": {
220
+ "input": 1.75,
221
+ "output": 14,
222
+ "cache_read": 0.175,
223
+ "note": "Codex spark capacity tier; not a separate LiteLLM key. Local (LiteLLM reverse-substring) and the edge matcher both bill it at the base gpt-5.3-codex rate ($1.75/$14, $0.175 cache read, https://developers.openai.com/api/docs/pricing); the exact entry pins that resolution on both sides (issue #929)."
224
+ },
161
225
  "kiro-agent": {
162
226
  "input": 3,
163
227
  "output": 15,
@@ -265,6 +329,12 @@
265
329
  "cache_write": 0,
266
330
  "note": "Google Gemini 4 Argon introductory list rate (through 2026-11-30) matching Cursor Other Models pool: $2/M input, $0.10/M cached input, $10/M output. Effort suffixes (-high/-medium/-low/-minimal) are stripped by normalizeCursorModel. Sources: https://cursor.com/docs/models-and-pricing and https://ai.google.dev/gemini-api/docs/pricing"
267
331
  },
332
+ "gemini-3-flash-preview": {
333
+ "input": 0.5,
334
+ "output": 3,
335
+ "cache_read": 0.05,
336
+ "note": "LiteLLM entry (sourced from https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing, cached 2026-10-31): $0.50/$3 per MTok, $0.05 cache read, no published cache-write rate. Same rate the edge table already pins; promoted so curated/edge parity guards the key (issue #929)."
337
+ },
268
338
  "MiniMax-M2.7": {
269
339
  "input": 0.3,
270
340
  "output": 1.2,
@@ -579,6 +649,30 @@
579
649
  "output": 0,
580
650
  "cache_read": 0
581
651
  },
652
+ "mimo-v2.5": {
653
+ "input": 0.4,
654
+ "output": 2,
655
+ "cache_read": 0.08,
656
+ "note": "Xiaomi MiMo v2.5 (queue rows report the bare id; LiteLLM keys are provider-qualified). Rates match the deepinfra entry — the lexicographically smallest provider key the matcher deterministically picks: $0.40/$2 per MTok, $0.08 cache read, no published cache-write rate. The edge table pinned this exact rate after the kimi-k2.6/mimo profile-modal incident; curated pins the same value so local billing no longer depends on LiteLLM prefix-strip (issue #929)."
657
+ },
658
+ "mimo-v2.5-pro": {
659
+ "input": 1,
660
+ "output": 3,
661
+ "cache_read": 0.2,
662
+ "note": "Xiaomi MiMo v2.5 Pro (deepinfra is the lexicographically smallest LiteLLM provider key, matching the edge table): $1/$3 per MTok, $0.20 cache read. Exact entry required alongside mimo-v2.5 so the contained-exact fallback cannot bill -pro at the base v2.5 rate (issue #929)."
663
+ },
664
+ "muse-spark-1.2-contributor": {
665
+ "input": 0.1,
666
+ "output": 0.2,
667
+ "cache_read": 0.002,
668
+ "note": "Paid Muse contributor SKU. LiteLLM provider-qualified entries agree on $0.10/$0.20 per MTok, $0.002 cache read (meta and opencode_go both list these rates, cached 2026-10-31). Previously LiteLLM prefix-strip-only, so cloud account views showed it unpriced; promoted so cloud prices it and curated/edge parity guards the key (issue #929). The -free SKU below stays $0."
669
+ },
670
+ "muse-spark-1.3-contributor": {
671
+ "input": 0.1,
672
+ "output": 0.2,
673
+ "cache_read": 0.002,
674
+ "note": "See muse-spark-1.2-contributor — same paid contributor SKU at the same verified rates (issue #929)."
675
+ },
582
676
  "muse-spark-1.2-contributor-free": {
583
677
  "input": 0,
584
678
  "output": 0,