ccc-notifier 0.4.0 → 0.6.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.
@@ -2,24 +2,22 @@
2
2
  import {
3
3
  isWSL
4
4
  } from "./chunk-DGXUSPS4.js";
5
- import {
6
- makeFullDashboardState,
7
- writeFullDashboardStateAtomic
8
- } from "./chunk-ZHIZZ6V5.js";
9
- import {
10
- waitForDataLock
11
- } from "./chunk-J6Y3RMMC.js";
12
5
  import {
13
6
  formatJPY,
14
7
  formatTokens,
15
8
  formatUSD,
16
9
  modelDisplayName
17
10
  } from "./chunk-J5QAYTFE.js";
11
+ import {
12
+ makeFullDashboardState,
13
+ waitForDataLock,
14
+ writeFullDashboardStateAtomic
15
+ } from "./chunk-27SJELD2.js";
18
16
  import {
19
17
  paths,
20
18
  readConfig,
21
19
  readTurns
22
- } from "./chunk-26CISNOE.js";
20
+ } from "./chunk-OOAC5ULQ.js";
23
21
 
24
22
  // src/dashboard.ts
25
23
  import { spawn, spawnSync } from "child_process";
@@ -128,6 +126,13 @@ function dateKeyOf(d) {
128
126
  const day = String(d.getDate()).padStart(2, "0");
129
127
  return `${y}-${m}-${day}`;
130
128
  }
129
+ function inclusiveLocalDateSpan(min, max) {
130
+ const toUtcDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
131
+ const minDay = toUtcDay(min);
132
+ const maxDay = toUtcDay(max);
133
+ if (!Number.isFinite(minDay) || !Number.isFinite(maxDay) || maxDay < minDay) return null;
134
+ return Math.floor((maxDay - minDay) / 864e5) + 1;
135
+ }
131
136
  function fmtLocalDateTime(iso) {
132
137
  const d = new Date(iso);
133
138
  if (Number.isNaN(d.getTime())) return iso;
@@ -214,6 +219,7 @@ function buildTurnEmbed(rec, map) {
214
219
  const primary = rec.models[0] ?? "unknown";
215
220
  const extra = rec.models.length > 1 ? ` +${rec.models.length - 1}` : "";
216
221
  const saMark = rec.subagents ? " +SA" : "";
222
+ const codexActivityMark = rec.source === "codex" && rec.subagentActivity ? " +SA(\u672A\u96C6\u8A08)" : "";
217
223
  let sa = null;
218
224
  if (rec.subagents) {
219
225
  const saModels = Object.keys(rec.subagents.costByModel).map((m) => modelDisplayName(m));
@@ -231,7 +237,7 @@ function buildTurnEmbed(rec, map) {
231
237
  p: basename(rec.project) || rec.project || "(unknown)",
232
238
  pf: rec.project ?? "",
233
239
  br: rec.gitBranch,
234
- md: modelDisplayName(primary) + extra + saMark,
240
+ md: modelDisplayName(primary) + extra + saMark + codexActivityMark,
235
241
  mr: rec.models,
236
242
  ti: formatTokens(turnInputTokens(rec)),
237
243
  to: formatTokens(turnOutputTokens(rec)),
@@ -243,6 +249,14 @@ function buildTurnEmbed(rec, map) {
243
249
  sa
244
250
  };
245
251
  if (rec.source === "codex") out.sc = "codex";
252
+ if (rec.source === "codex" && rec.subagentActivity) {
253
+ out.ca = {
254
+ started: rec.subagentActivity.started,
255
+ stopped: rec.subagentActivity.stopped,
256
+ agentTypes: [...rec.subagentActivity.agentTypes],
257
+ usageStatus: rec.subagentActivity.usageStatus
258
+ };
259
+ }
246
260
  return out;
247
261
  }
248
262
  function computeKpis(turns) {
@@ -672,14 +686,28 @@ var APP_JS = `<script>
672
686
  lastRenderedGran = GRAN;
673
687
  }
674
688
 
689
+ // \u9031\u5225hover\u3060\u3051\u306F\u3001\u305D\u306E\u9031\u306E\u5185\u8A33\u3092\u91D1\u984D\u306E\u5927\u304D\u3044\u9806\u306B\u8AAD\u3080\u3002\u7A4D\u307F\u4E0A\u3052\u9806\u30FB\u8272\u306F
690
+ // slotOrder\u306E\u307E\u307E\u56FA\u5B9A\u3057\u3001\u540C\u984D\u3082slotOrder\u3067\u6C7A\u5B9A\u7684\u306B\u4E26\u3079\u308B\u3002
691
+ function tooltipSlotOrder(bucket, gran){
692
+ var order = slotOrder.slice();
693
+ if(gran !== 'week') return order;
694
+ var rank = {}; for(var i=0;i<slotOrder.length;i++){ rank[slotOrder[i]] = i; }
695
+ order.sort(function(a, b){
696
+ var diff = (bucket.bs[b] || 0) - (bucket.bs[a] || 0);
697
+ return diff !== 0 ? diff : rank[a] - rank[b];
698
+ });
699
+ return order;
700
+ }
701
+
675
702
  function showTip(evt, bucket){
676
703
  if(!tip) return;
677
704
  clearNode(tip);
678
705
  var h = document.createElement('div'); h.className = 'tip-title';
679
706
  h.textContent = periodText(bucket.key, GRAN) + ' \u5408\u8A08 ' + formatUSD(bucket.total) + ' \xB7 ' + bucket.turns + ' \u30BF\u30FC\u30F3';
680
707
  tip.appendChild(h);
681
- for(var j=0;j<slotOrder.length;j++){
682
- var slot = slotOrder[j];
708
+ var tipOrder = tooltipSlotOrder(bucket, GRAN);
709
+ for(var j=0;j<tipOrder.length;j++){
710
+ var slot = tipOrder[j];
683
711
  var v = bucket.bs[slot] || 0;
684
712
  if(v <= 0) continue;
685
713
  var row = document.createElement('div'); row.className = 'tip-row';
@@ -816,12 +844,13 @@ var APP_JS = `<script>
816
844
  sub.textContent = ' ' + mk + (mk === curMonth ? '(\u4ECA\u6708)' : '');
817
845
  h.appendChild(sub);
818
846
  budgetEl.appendChild(h);
819
- // \u671F\u9593\u9650\u5B9A\u7248\u306F\u57CB\u3081\u8FBC\u307E\u306A\u3044\u5168\u5C65\u6B74\u304B\u3089\u5F53\u6708\u3092\u96C6\u8A08\u3057\u305F\u56FA\u5B9A\u5024\u3002
847
+ // \u671F\u9593\u9650\u5B9A\u7248\u306F\u3001\u4FDD\u5B58\u6E08\u307F\u5C65\u6B74\u306E\u5F53\u6708\u5206\u3092\u96C6\u8A08\u5BFE\u8C61\u304B\u3089\u843D\u3068\u3055\u306A\u3044\u305F\u3081
848
+ // \u57CB\u3081\u8FBC\u307E\u306A\u3044\u5168\u5C65\u6B74\u304B\u3089\u56FA\u5B9A\u5024\u3092\u4F5C\u308B\u3002
820
849
  // \u5168\u5C65\u6B74\u7248\u306E\u307F\u5F93\u6765\u3069\u304A\u308A\u9078\u629E\u6708\u3078\u9023\u52D5\u3059\u308B\u3002\u3044\u305A\u308C\u3082\u30BD\u30FC\u30B9\u30D5\u30A3\u30EB\u30BF\u975E\u9023\u52D5\u3002
821
850
  if(BUDGET_FIXED || HAS_CODEX){
822
851
  var srcNote = document.createElement('p'); srcNote.className = 'note';
823
852
  srcNote.textContent = BUDGET_FIXED
824
- ? '\u4ECA\u6708\u30FB\u5168\u5C65\u6B74\u304B\u3089\u6B63\u78BA\u306B\u96C6\u8A08(\u5168\u30BD\u30FC\u30B9\u5408\u7B97) / exact current month from full history'
853
+ ? '\u4ECA\u6708\u30FB\u4FDD\u5B58\u6E08\u307F\u5C65\u6B74\u3092\u5168\u4EF6\u96C6\u8A08(\u5168\u30BD\u30FC\u30B9\u5408\u7B97) / current month from all recorded history'
825
854
  : '\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources';
826
855
  budgetEl.appendChild(srcNote);
827
856
  }
@@ -943,6 +972,12 @@ var APP_JS = `<script>
943
972
  saLine.textContent = '\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: ' + (t.sa.usd||'') + '(' + (t.sa.jpy||'') + ')\xB7 ' + (t.sa.models||'') + ' \xB7 API\u30B3\u30FC\u30EB ' + (t.sa.apiCalls||0);
944
973
  dtd.appendChild(saLine);
945
974
  }
975
+ if(t.ca){
976
+ var caLine = document.createElement('div'); caLine.className = 'detail-meta';
977
+ caLine.textContent = 'Codex\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: \u5229\u7528\u3042\u308A\u30FB\u6599\u91D1\u672A\u96C6\u8A08 \xB7 \u958B\u59CB ' +
978
+ (t.ca.started||0) + ' \xB7 \u7D42\u4E86 ' + (t.ca.stopped||0) + ' \xB7 \u7A2E\u5225 ' + ((t.ca.agentTypes||[]).join(', ')||'unknown');
979
+ dtd.appendChild(caLine);
980
+ }
946
981
  var pre = document.createElement('div'); pre.className = 'detail-prompt';
947
982
  pre.textContent = full.length ? full : '(\u30D7\u30ED\u30F3\u30D7\u30C8\u306A\u3057)';
948
983
  dtd.appendChild(pre); dtr.appendChild(dtd);
@@ -1047,7 +1082,7 @@ function budgetCard(budgetUSD, month, fallbackRate, hasCodex, fixedCurrentMonth)
1047
1082
  const width = Math.max(0, Math.min(100, pct));
1048
1083
  const level = pct >= 100 ? "over" : pct >= 70 ? "warn" : "ok";
1049
1084
  const budgetJpy = budgetUSD * fallbackRate;
1050
- return `<section class="card" id="cccn-budget"><h2>\u6708\u4E88\u7B97 / Monthly budget<span class="stat-sub"> \u4ECA\u6708(\u66A6\u6708)</span></h2>` + (fixedCurrentMonth ? `<p class="note">\u4ECA\u6708\u30FB\u5168\u5C65\u6B74\u304B\u3089\u6B63\u78BA\u306B\u96C6\u8A08(\u5168\u30BD\u30FC\u30B9\u5408\u7B97) / exact current month from full history</p>` : hasCodex ? `<p class="note">\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources</p>` : "") + `<div class="budget-bar"><div class="budget-fill lvl-${level}" style="width:${width.toFixed(1)}%"></div></div><div class="budget-foot"><span>\u4ECA\u6708 <b>${esc(formatUSD(month.usd))}</b> / ${esc(formatUSD(budgetUSD))} <span class="muted">(${esc(formatJPY(month.jpy))} / ${esc(formatJPY(budgetJpy))})</span></span><span class="budget-pct lvl-${level}">${pct.toFixed(1)}% used</span></div></section>`;
1085
+ return `<section class="card" id="cccn-budget"><h2>\u6708\u4E88\u7B97 / Monthly budget<span class="stat-sub"> \u4ECA\u6708(\u66A6\u6708)</span></h2>` + (fixedCurrentMonth ? `<p class="note">\u4ECA\u6708\u30FB\u4FDD\u5B58\u6E08\u307F\u5C65\u6B74\u3092\u5168\u4EF6\u96C6\u8A08(\u5168\u30BD\u30FC\u30B9\u5408\u7B97) / current month from all recorded history</p>` : hasCodex ? `<p class="note">\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources</p>` : "") + `<div class="budget-bar"><div class="budget-fill lvl-${level}" style="width:${width.toFixed(1)}%"></div></div><div class="budget-foot"><span>\u4ECA\u6708 <b>${esc(formatUSD(month.usd))}</b> / ${esc(formatUSD(budgetUSD))} <span class="muted">(${esc(formatJPY(month.jpy))} / ${esc(formatJPY(budgetJpy))})</span></span><span class="budget-pct lvl-${level}">${pct.toFixed(1)}% used</span></div></section>`;
1051
1086
  }
1052
1087
  function escapeJsonForScript(json) {
1053
1088
  return json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
@@ -1056,7 +1091,7 @@ function renderDashboard(turns, opts, fullHistory) {
1056
1091
  const version = readVersion();
1057
1092
  const generatedAtIso = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1058
1093
  const generatedAt = fmtLocalDateTime(generatedAtIso);
1059
- const period = opts.days === null ? "\u5168\u671F\u9593" : `\u76F4\u8FD1 ${opts.days} \u65E5\u9593`;
1094
+ let period = opts.days === null ? "\u5168\u671F\u9593" : `\u76F4\u8FD1 ${opts.days} \u65E5\u9593`;
1060
1095
  const limited = opts.days !== null;
1061
1096
  const variant = opts.variant ?? "custom";
1062
1097
  const totalLabel = limited ? "\u5BFE\u8C61\u671F\u9593\u5408\u8A08" : "\u901A\u7B97";
@@ -1081,13 +1116,17 @@ function renderDashboard(turns, opts, fullHistory) {
1081
1116
  }
1082
1117
  let minMs = Infinity;
1083
1118
  let maxMs = -Infinity;
1084
- for (const t of turnsEmbed) {
1085
- if (t.t > 0) {
1086
- if (t.t < minMs) minMs = t.t;
1087
- if (t.t > maxMs) maxMs = t.t;
1088
- }
1119
+ for (const turn of turns) {
1120
+ const ms = Date.parse(turn.ts);
1121
+ if (!Number.isFinite(ms)) continue;
1122
+ if (ms < minMs) minMs = ms;
1123
+ if (ms > maxMs) maxMs = ms;
1089
1124
  }
1090
1125
  const rangeText = Number.isFinite(minMs) && Number.isFinite(maxMs) ? `${dateKeyOf(new Date(minMs))} \u301C ${dateKeyOf(new Date(maxMs))}` : "\u2014";
1126
+ const embeddedDaySpan = Number.isFinite(minMs) && Number.isFinite(maxMs) ? inclusiveLocalDateSpan(new Date(minMs), new Date(maxMs)) : null;
1127
+ const shortRecentHistory = opts.days !== null && embeddedDaySpan !== null && embeddedDaySpan < opts.days;
1128
+ if (shortRecentHistory) period = `\u5C65\u6B74 ${embeddedDaySpan} \u65E5\u5206`;
1129
+ const recentVariantLabel = shortRecentHistory ? `\u5C65\u6B74 ${embeddedDaySpan} \u65E5\u5206 / Recent` : `\u76F4\u8FD1 ${opts.days} \u65E5\u7248 / Recent`;
1091
1130
  const embed = {
1092
1131
  version,
1093
1132
  generatedAt,
@@ -1104,8 +1143,8 @@ function renderDashboard(turns, opts, fullHistory) {
1104
1143
  const reloadSec = Number.isFinite(opts.autoReloadSec) && opts.autoReloadSec > 0 ? Math.floor(opts.autoReloadSec) : 0;
1105
1144
  const refreshMeta = reloadSec > 0 ? `<meta http-equiv="refresh" content="${reloadSec}">` : "";
1106
1145
  const updateTrigger = anyCodex ? "Claude Code / Codex \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642" : "Claude Code \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642";
1107
- const autoUpdateFoot = reloadSec > 0 ? variant === "full" ? `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u30D5\u30A1\u30A4\u30EB\u3092\u518D\u8AAD\u8FBC(\u5168\u5C65\u6B74\u7248\u306E\u751F\u6210\u306F\u30ED\u30FC\u30AB\u30EB\u65E5\u3054\u3068\u3001\u307E\u305F\u306F\u624B\u52D5 dashboard --all)</div>` : `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u81EA\u52D5\u66F4\u65B0(\u6700\u65B0\u5316\u306F ${updateTrigger})</div>` : "";
1108
- const variantNav = variant === "recent" ? `<div class="sub"><strong>\u76F4\u8FD1 ${opts.days} \u65E5\u7248 / Recent</strong> \xB7 ${opts.peerAvailable ? `<a href="report-all.html">\u5168\u5C65\u6B74\u7248\u3078 / Full history</a>` : `<span aria-disabled="true">\u5168\u5C65\u6B74\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard --all \u3067\u751F\u6210\uFF09 / Full history not generated</span>`}</div>` : variant === "full" ? `<div class="sub"><strong>\u5168\u5C65\u6B74\u7248 / Full history</strong> \xB7 ${opts.peerAvailable ? `<a href="report.html">\u76F4\u8FD1\u7248\u3078 / Recent</a>` : `<span aria-disabled="true">\u76F4\u8FD1\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard \u3067\u751F\u6210\uFF09 / Recent not generated</span>`}</div><div class="sub muted">\u6700\u7D42\u751F\u6210 ${esc(generatedAt)}\u3002\u30ED\u30FC\u30AB\u30EB\u65E5\u306E\u6700\u521D\u306E\u6B63\u5E38\u306A\u30BF\u30FC\u30F3\u6642\u3001\u307E\u305F\u306F\u624B\u52D5\u306E dashboard --all \u3067\u66F4\u65B0\u3055\u308C\u307E\u3059\u3002</div>` : "";
1146
+ const autoUpdateFoot = reloadSec > 0 ? variant === "full" ? `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u30D5\u30A1\u30A4\u30EB\u3092\u518D\u8AAD\u8FBC(\u5168\u5C65\u6B74\u7248\u306E\u751F\u6210\u306F\u6B63\u5E38\u306A sweep \u5B8C\u4E86\u6642\u3001\u30ED\u30FC\u30AB\u30EB\u65E5\u3054\u3068\u3001\u307E\u305F\u306F\u624B\u52D5 dashboard --all)</div>` : `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u81EA\u52D5\u66F4\u65B0(\u6700\u65B0\u5316\u306F ${updateTrigger}\u3001\u307E\u305F\u306F\u6B63\u5E38\u306A sweep \u5B8C\u4E86\u6642)</div>` : "";
1147
+ const variantNav = variant === "recent" ? `<div class="sub"><strong>${recentVariantLabel}</strong> \xB7 ${opts.peerAvailable ? `<a href="report-all.html">\u5168\u5C65\u6B74\u7248\u3078 / Full history</a>` : `<span aria-disabled="true">\u5168\u5C65\u6B74\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard --all \u3067\u751F\u6210\uFF09 / Full history not generated</span>`}</div>` : variant === "full" ? `<div class="sub"><strong>\u5168\u5C65\u6B74\u7248 / Full history</strong> \xB7 ${opts.peerAvailable ? `<a href="report.html">\u76F4\u8FD1\u7248\u3078 / Recent</a>` : `<span aria-disabled="true">\u76F4\u8FD1\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard \u3067\u751F\u6210\uFF09 / Recent not generated</span>`}</div><div class="sub muted">\u6700\u7D42\u751F\u6210 ${esc(generatedAt)}\u3002\u6B63\u5E38\u306A sweep \u5B8C\u4E86\u6642\u3001\u30ED\u30FC\u30AB\u30EB\u65E5\u306E\u6700\u521D\u306E\u6B63\u5E38\u306A\u30BF\u30FC\u30F3\u6642\u3001\u307E\u305F\u306F\u624B\u52D5\u306E dashboard --all \u3067\u66F4\u65B0\u3055\u308C\u307E\u3059\u3002</div>` : "";
1109
1148
  const head = `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark">` + refreshMeta + `<title>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</title>` + STYLE + `</head><body><div class="wrap">`;
1110
1149
  const foot = autoUpdateFoot + `<div class="foot">ccc-notifier v${esc(version)} \xB7 \u30C7\u30FC\u30BF\u306F\u30ED\u30FC\u30AB\u30EB\u306E\u307F / all data stays local</div></div><div id="cccn-tip" class="cccn-tip" hidden></div><script id="cccn-data" type="application/json">${dataJson}</script>` + APP_JS + `</body></html>`;
1111
1150
  const budgetSection = budgetCard(budgetUSD, budgetMonth, cfg.fx.fallbackRate, anyCodex, limited);
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/pricing.ts
4
+ import { promises as fs } from "fs";
5
+ import path from "path";
6
+ var LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
7
+ var LITELLM_FETCH_TIMEOUT_MS = 3e3;
8
+ var CACHE_FRESH_MS = 24 * 60 * 60 * 1e3;
9
+ var SONNET_5_STANDARD_PRICE_START_MS = Date.UTC(2026, 8, 1);
10
+ function price(input, output, cacheWrite5m, cacheWrite1h, cacheRead, source) {
11
+ return { input, output, cacheWrite5m, cacheWrite1h, cacheRead, source };
12
+ }
13
+ function builtinPriceTable(now = /* @__PURE__ */ new Date()) {
14
+ const sonnet5 = now.getTime() < SONNET_5_STANDARD_PRICE_START_MS ? price(2, 10, 2.5, 4, 0.2, "builtin") : price(3, 15, 3.75, 6, 0.3, "builtin");
15
+ return {
16
+ "claude-fable-5": price(10, 50, 12.5, 20, 1, "builtin"),
17
+ "claude-mythos-5": price(10, 50, 12.5, 20, 1, "builtin"),
18
+ "claude-opus-4-8": price(5, 25, 6.25, 10, 0.5, "builtin"),
19
+ "claude-opus-4-7": price(5, 25, 6.25, 10, 0.5, "builtin"),
20
+ "claude-opus-4-6": price(5, 25, 6.25, 10, 0.5, "builtin"),
21
+ "claude-opus-4-5": price(5, 25, 6.25, 10, 0.5, "builtin"),
22
+ "claude-opus-4-1": price(15, 75, 18.75, 30, 1.5, "builtin"),
23
+ "claude-opus-4": price(15, 75, 18.75, 30, 1.5, "builtin"),
24
+ // 旧 claude-opus-4-20250514 の受け皿
25
+ "claude-3-opus": price(15, 75, 18.75, 30, 1.5, "builtin"),
26
+ "claude-sonnet-5": sonnet5,
27
+ "claude-sonnet-4-6": price(3, 15, 3.75, 6, 0.3, "builtin"),
28
+ "claude-sonnet-4-5": price(3, 15, 3.75, 6, 0.3, "builtin"),
29
+ "claude-sonnet-4": price(3, 15, 3.75, 6, 0.3, "builtin"),
30
+ "claude-3-7-sonnet": price(3, 15, 3.75, 6, 0.3, "builtin"),
31
+ "claude-3-5-sonnet": price(3, 15, 3.75, 6, 0.3, "builtin"),
32
+ "claude-haiku-4-5": price(1, 5, 1.25, 2, 0.1, "builtin"),
33
+ "claude-3-5-haiku": price(0.8, 4, 1, 1.6, 0.08, "builtin"),
34
+ "claude-3-haiku": price(0.25, 1.25, 0.3125, 0.5, 0.025, "builtin"),
35
+ // OpenAI Codex CLI 対応(公式レートに基づく単価。キャッシュ書き込み課金は無いため 0)
36
+ "gpt-5.5": price(5, 30, 0, 0, 0.5, "builtin"),
37
+ "gpt-5.1": price(1.25, 10, 0, 0, 0.125, "builtin"),
38
+ "gpt-5": price(1.25, 10, 0, 0, 0.125, "builtin"),
39
+ "gpt-5-codex": price(1.25, 10, 0, 0, 0.125, "builtin"),
40
+ "gpt-5.1-codex": price(1.25, 10, 0, 0, 0.125, "builtin"),
41
+ "o3": price(2, 8, 0, 0, 0.5, "builtin")
42
+ };
43
+ }
44
+ function normalizeModelId(modelId) {
45
+ let s = modelId.toLowerCase();
46
+ s = s.replace(/^anthropic[\/.]/, "");
47
+ s = s.replace(/\[1m\]$/, "");
48
+ s = s.replace(/-20\d{6}$/, "");
49
+ return s.trim();
50
+ }
51
+ function resolvePrice(modelId, table) {
52
+ const target = normalizeModelId(modelId);
53
+ for (const rawKey of Object.keys(table)) {
54
+ const key = normalizeModelId(rawKey);
55
+ if (key.length > 0 && target === key) return { ...table[rawKey] };
56
+ }
57
+ return null;
58
+ }
59
+ function computeCost(main, sidechain, table) {
60
+ const byModel = /* @__PURE__ */ Object.create(null);
61
+ const unknownModels = [];
62
+ let usd = 0;
63
+ const accumulate = (usage) => {
64
+ for (const [model, tokens] of Object.entries(usage)) {
65
+ const p = resolvePrice(model, table);
66
+ let cost = 0;
67
+ if (p === null) {
68
+ if (!unknownModels.includes(model)) unknownModels.push(model);
69
+ } else {
70
+ cost = (tokens.input * p.input + tokens.output * p.output + tokens.cacheWrite5m * p.cacheWrite5m + tokens.cacheWrite1h * p.cacheWrite1h + tokens.cacheRead * p.cacheRead) / 1e6;
71
+ }
72
+ byModel[model] = (Object.hasOwn(byModel, model) ? byModel[model] : 0) + cost;
73
+ usd += cost;
74
+ }
75
+ };
76
+ accumulate(main);
77
+ accumulate(sidechain);
78
+ return { usd, byModel, unknownModels };
79
+ }
80
+ function cacheFilePath(cacheDir) {
81
+ return path.join(cacheDir, "pricing.json");
82
+ }
83
+ async function readPriceCache(cacheDir) {
84
+ try {
85
+ const raw = await fs.readFile(cacheFilePath(cacheDir), "utf8");
86
+ const parsed = JSON.parse(raw);
87
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.fetchedAt === "string" && typeof parsed.table === "object" && parsed.table !== null) {
88
+ const p = parsed;
89
+ return { fetchedAt: p.fetchedAt, table: p.table };
90
+ }
91
+ return null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+ async function writePriceCache(cacheDir, table) {
97
+ const file = cacheFilePath(cacheDir);
98
+ const payload = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), table };
99
+ await fs.mkdir(path.dirname(file), { recursive: true });
100
+ await fs.writeFile(file, JSON.stringify(payload, null, 2), "utf8");
101
+ }
102
+ function isCacheFresh(fetchedAt) {
103
+ const t = Date.parse(fetchedAt);
104
+ if (Number.isNaN(t)) return false;
105
+ return Date.now() - t <= CACHE_FRESH_MS;
106
+ }
107
+ function mergePriceTables(builtin, cached, fresh) {
108
+ const merged = { ...builtin };
109
+ const builtinKeyById = /* @__PURE__ */ new Map();
110
+ for (const rawKey of Object.keys(builtin)) {
111
+ const normalized = normalizeModelId(rawKey);
112
+ if (normalized.length > 0) builtinKeyById.set(normalized, rawKey);
113
+ }
114
+ const cachedKeyById = /* @__PURE__ */ new Map();
115
+ for (const [model, modelPrice] of Object.entries(cached)) {
116
+ const normalized = normalizeModelId(model);
117
+ if (normalized.length === 0) continue;
118
+ if (normalized === "claude-sonnet-5") continue;
119
+ const builtinKey = builtinKeyById.get(normalized);
120
+ if (builtinKey !== void 0) {
121
+ if (fresh) merged[builtinKey] = modelPrice;
122
+ continue;
123
+ }
124
+ const priorCachedKey = cachedKeyById.get(normalized);
125
+ if (priorCachedKey !== void 0 && priorCachedKey !== model) delete merged[priorCachedKey];
126
+ Object.defineProperty(merged, model, {
127
+ value: modelPrice,
128
+ enumerable: true,
129
+ configurable: true,
130
+ writable: true
131
+ });
132
+ cachedKeyById.set(normalized, model);
133
+ }
134
+ return merged;
135
+ }
136
+ function toFiniteNumber(v) {
137
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
138
+ }
139
+ var LITELLM_OPENAI_KEY_RE = /^(gpt-|o3($|-)|codex-)/;
140
+ function convertLiteLLMPayload(payload) {
141
+ if (payload === null || typeof payload !== "object") {
142
+ throw new Error("invalid litellm payload: not an object");
143
+ }
144
+ const table = {};
145
+ for (const [rawKey, rawEntry] of Object.entries(payload)) {
146
+ if (rawEntry === null || typeof rawEntry !== "object") continue;
147
+ const entry = rawEntry;
148
+ const provider = entry.litellm_provider;
149
+ if (provider === "openai") {
150
+ const key2 = rawKey.toLowerCase();
151
+ if (!LITELLM_OPENAI_KEY_RE.test(key2)) continue;
152
+ const inputRaw2 = toFiniteNumber(entry.input_cost_per_token);
153
+ const outputRaw2 = toFiniteNumber(entry.output_cost_per_token);
154
+ if (inputRaw2 === null || inputRaw2 <= 0) continue;
155
+ if (outputRaw2 === null || outputRaw2 <= 0) continue;
156
+ const cacheReadRaw2 = toFiniteNumber(entry.cache_read_input_token_cost);
157
+ table[key2] = {
158
+ input: inputRaw2 * 1e6,
159
+ output: outputRaw2 * 1e6,
160
+ cacheRead: cacheReadRaw2 !== null ? cacheReadRaw2 * 1e6 : 0,
161
+ cacheWrite5m: 0,
162
+ cacheWrite1h: 0,
163
+ source: "litellm"
164
+ };
165
+ continue;
166
+ }
167
+ if (typeof provider === "string" && provider !== "anthropic") continue;
168
+ let key = rawKey.toLowerCase();
169
+ if (key.startsWith("anthropic/")) key = key.slice("anthropic/".length);
170
+ if (!key.startsWith("claude")) continue;
171
+ const inputRaw = toFiniteNumber(entry.input_cost_per_token);
172
+ const outputRaw = toFiniteNumber(entry.output_cost_per_token);
173
+ if (inputRaw === null || inputRaw <= 0) continue;
174
+ if (outputRaw === null || outputRaw <= 0) continue;
175
+ const input = inputRaw * 1e6;
176
+ const output = outputRaw * 1e6;
177
+ const cacheReadRaw = toFiniteNumber(entry.cache_read_input_token_cost);
178
+ const cacheWrite5mRaw = toFiniteNumber(entry.cache_creation_input_token_cost);
179
+ const cacheWrite1hRaw = toFiniteNumber(entry.cache_creation_input_token_cost_above_1hr);
180
+ table[key] = {
181
+ input,
182
+ output,
183
+ cacheRead: cacheReadRaw !== null ? cacheReadRaw * 1e6 : input * 0.1,
184
+ cacheWrite5m: cacheWrite5mRaw !== null ? cacheWrite5mRaw * 1e6 : input * 1.25,
185
+ cacheWrite1h: cacheWrite1hRaw !== null ? cacheWrite1hRaw * 1e6 : input * 2,
186
+ source: "litellm"
187
+ };
188
+ }
189
+ return table;
190
+ }
191
+ async function fetchLiteLLMPriceTable() {
192
+ const controller = new AbortController();
193
+ const timer = setTimeout(() => controller.abort(), LITELLM_FETCH_TIMEOUT_MS);
194
+ try {
195
+ const res = await fetch(LITELLM_URL, { signal: controller.signal });
196
+ if (!res.ok) {
197
+ throw new Error(`litellm fetch failed with status ${res.status}`);
198
+ }
199
+ const json = await res.json();
200
+ return convertLiteLLMPayload(json);
201
+ } finally {
202
+ clearTimeout(timer);
203
+ }
204
+ }
205
+ async function loadPriceTable(cacheDir, opts) {
206
+ const builtin = builtinPriceTable();
207
+ const cached = await readPriceCache(cacheDir);
208
+ if (cached !== null && isCacheFresh(cached.fetchedAt)) {
209
+ return mergePriceTables(builtin, cached.table, true);
210
+ }
211
+ if (opts?.offline === true) {
212
+ return cached !== null ? mergePriceTables(builtin, cached.table, false) : builtin;
213
+ }
214
+ try {
215
+ const remoteTable = await fetchLiteLLMPriceTable();
216
+ await writePriceCache(cacheDir, remoteTable);
217
+ return mergePriceTables(builtin, remoteTable, true);
218
+ } catch {
219
+ return cached !== null ? mergePriceTables(builtin, cached.table, false) : builtin;
220
+ }
221
+ }
222
+
223
+ export {
224
+ computeCost,
225
+ loadPriceTable
226
+ };