ccc-notifier 0.5.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.
@@ -10,16 +10,14 @@ import {
10
10
  } from "./chunk-J5QAYTFE.js";
11
11
  import {
12
12
  makeFullDashboardState,
13
+ waitForDataLock,
13
14
  writeFullDashboardStateAtomic
14
- } from "./chunk-ENGUOLTD.js";
15
- import {
16
- waitForDataLock
17
- } from "./chunk-O34L3NSI.js";
15
+ } from "./chunk-27SJELD2.js";
18
16
  import {
19
17
  paths,
20
18
  readConfig,
21
19
  readTurns
22
- } from "./chunk-5PH7PPD6.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;
@@ -681,14 +686,28 @@ var APP_JS = `<script>
681
686
  lastRenderedGran = GRAN;
682
687
  }
683
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
+
684
702
  function showTip(evt, bucket){
685
703
  if(!tip) return;
686
704
  clearNode(tip);
687
705
  var h = document.createElement('div'); h.className = 'tip-title';
688
706
  h.textContent = periodText(bucket.key, GRAN) + ' \u5408\u8A08 ' + formatUSD(bucket.total) + ' \xB7 ' + bucket.turns + ' \u30BF\u30FC\u30F3';
689
707
  tip.appendChild(h);
690
- for(var j=0;j<slotOrder.length;j++){
691
- var slot = slotOrder[j];
708
+ var tipOrder = tooltipSlotOrder(bucket, GRAN);
709
+ for(var j=0;j<tipOrder.length;j++){
710
+ var slot = tipOrder[j];
692
711
  var v = bucket.bs[slot] || 0;
693
712
  if(v <= 0) continue;
694
713
  var row = document.createElement('div'); row.className = 'tip-row';
@@ -825,12 +844,13 @@ var APP_JS = `<script>
825
844
  sub.textContent = ' ' + mk + (mk === curMonth ? '(\u4ECA\u6708)' : '');
826
845
  h.appendChild(sub);
827
846
  budgetEl.appendChild(h);
828
- // \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
829
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
830
850
  if(BUDGET_FIXED || HAS_CODEX){
831
851
  var srcNote = document.createElement('p'); srcNote.className = 'note';
832
852
  srcNote.textContent = BUDGET_FIXED
833
- ? '\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'
834
854
  : '\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources';
835
855
  budgetEl.appendChild(srcNote);
836
856
  }
@@ -1062,7 +1082,7 @@ function budgetCard(budgetUSD, month, fallbackRate, hasCodex, fixedCurrentMonth)
1062
1082
  const width = Math.max(0, Math.min(100, pct));
1063
1083
  const level = pct >= 100 ? "over" : pct >= 70 ? "warn" : "ok";
1064
1084
  const budgetJpy = budgetUSD * fallbackRate;
1065
- 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>`;
1066
1086
  }
1067
1087
  function escapeJsonForScript(json) {
1068
1088
  return json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
@@ -1071,7 +1091,7 @@ function renderDashboard(turns, opts, fullHistory) {
1071
1091
  const version = readVersion();
1072
1092
  const generatedAtIso = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1073
1093
  const generatedAt = fmtLocalDateTime(generatedAtIso);
1074
- 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`;
1075
1095
  const limited = opts.days !== null;
1076
1096
  const variant = opts.variant ?? "custom";
1077
1097
  const totalLabel = limited ? "\u5BFE\u8C61\u671F\u9593\u5408\u8A08" : "\u901A\u7B97";
@@ -1096,13 +1116,17 @@ function renderDashboard(turns, opts, fullHistory) {
1096
1116
  }
1097
1117
  let minMs = Infinity;
1098
1118
  let maxMs = -Infinity;
1099
- for (const t of turnsEmbed) {
1100
- if (t.t > 0) {
1101
- if (t.t < minMs) minMs = t.t;
1102
- if (t.t > maxMs) maxMs = t.t;
1103
- }
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;
1104
1124
  }
1105
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`;
1106
1130
  const embed = {
1107
1131
  version,
1108
1132
  generatedAt,
@@ -1119,8 +1143,8 @@ function renderDashboard(turns, opts, fullHistory) {
1119
1143
  const reloadSec = Number.isFinite(opts.autoReloadSec) && opts.autoReloadSec > 0 ? Math.floor(opts.autoReloadSec) : 0;
1120
1144
  const refreshMeta = reloadSec > 0 ? `<meta http-equiv="refresh" content="${reloadSec}">` : "";
1121
1145
  const updateTrigger = anyCodex ? "Claude Code / Codex \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642" : "Claude Code \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642";
1122
- 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>` : "";
1123
- 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>` : "";
1124
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">`;
1125
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>`;
1126
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
+ };
@@ -7,11 +7,14 @@ import {
7
7
  codexHome,
8
8
  detectCodex
9
9
  } from "./chunk-HTYUYKFW.js";
10
+ import {
11
+ loadPriceTable
12
+ } from "./chunk-6HTETN26.js";
10
13
  import {
11
14
  configFilePath,
12
15
  paths,
13
16
  readConfig
14
- } from "./chunk-5PH7PPD6.js";
17
+ } from "./chunk-OOAC5ULQ.js";
15
18
 
16
19
  // src/setup.ts
17
20
  import {
@@ -303,8 +306,8 @@ function makeTestRecord() {
303
306
  sidechainTokens: null,
304
307
  apiCalls: 1,
305
308
  costUSD: 0.01,
306
- costJPY: 1.5,
307
- fxRate: 150,
309
+ costJPY: 1.6,
310
+ fxRate: 160,
308
311
  fxSource: "fixed",
309
312
  prompt: "\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u5B8C\u4E86\u30C6\u30B9\u30C8"
310
313
  };
@@ -635,6 +638,7 @@ async function runInit(argv) {
635
638
  if (installCodex) {
636
639
  codexResult = registerCodexHook(process.execPath, resolveCliPath());
637
640
  }
641
+ await loadPriceTable(cccn.cacheDir, { offline: false });
638
642
  const notifyDisabled = !cfg.notify.os && !cfg.notify.slack;
639
643
  if (notifyDisabled) {
640
644
  console.log("\u30C6\u30B9\u30C8\u901A\u77E5: \u901A\u77E5\u306A\u3057\u30E2\u30FC\u30C9\u306E\u305F\u3081\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F");