@songsid/agend 2.3.2-beta.4 → 2.3.2-beta.5

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 (156) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/antigravity-usage-scan.d.ts +58 -0
  3. package/dist/antigravity-usage-scan.js +257 -0
  4. package/dist/antigravity-usage-scan.js.map +1 -0
  5. package/dist/antigravity-usage-worker.d.ts +1 -0
  6. package/dist/antigravity-usage-worker.js +10 -0
  7. package/dist/antigravity-usage-worker.js.map +1 -0
  8. package/dist/backend/kiro-identity.d.ts +45 -0
  9. package/dist/backend/kiro-identity.js +74 -0
  10. package/dist/backend/kiro-identity.js.map +1 -1
  11. package/dist/backend/kiro.d.ts +15 -0
  12. package/dist/backend/kiro.js +42 -5
  13. package/dist/backend/kiro.js.map +1 -1
  14. package/dist/backend/types.d.ts +6 -0
  15. package/dist/backend/types.js.map +1 -1
  16. package/dist/backend-signin-guide.d.ts +1 -0
  17. package/dist/backend-signin-guide.js +9 -0
  18. package/dist/backend-signin-guide.js.map +1 -0
  19. package/dist/backend-signin-pending.d.ts +10 -0
  20. package/dist/backend-signin-pending.js +44 -0
  21. package/dist/backend-signin-pending.js.map +1 -0
  22. package/dist/browser-backend-signins.d.ts +56 -0
  23. package/dist/browser-backend-signins.js +189 -0
  24. package/dist/browser-backend-signins.js.map +1 -0
  25. package/dist/cache-ledger.d.ts +38 -2
  26. package/dist/cache-ledger.js +133 -8
  27. package/dist/cache-ledger.js.map +1 -1
  28. package/dist/cache-prices.d.ts +2 -1
  29. package/dist/cache-prices.js +5 -2
  30. package/dist/cache-prices.js.map +1 -1
  31. package/dist/cache-service.d.ts +68 -0
  32. package/dist/cache-service.js +355 -13
  33. package/dist/cache-service.js.map +1 -1
  34. package/dist/channel/mcp-tools.js +3 -1
  35. package/dist/channel/mcp-tools.js.map +1 -1
  36. package/dist/cli.js +21 -2
  37. package/dist/cli.js.map +1 -1
  38. package/dist/config.d.ts +19 -0
  39. package/dist/config.js +25 -0
  40. package/dist/config.js.map +1 -1
  41. package/dist/cpu-quota.d.ts +23 -0
  42. package/dist/cpu-quota.js +86 -0
  43. package/dist/cpu-quota.js.map +1 -0
  44. package/dist/daemon.js +6 -3
  45. package/dist/daemon.js.map +1 -1
  46. package/dist/fleet-manager.d.ts +35 -0
  47. package/dist/fleet-manager.js +233 -11
  48. package/dist/fleet-manager.js.map +1 -1
  49. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  50. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  51. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +6 -3
  52. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +6 -3
  53. package/dist/instance-lifecycle.d.ts +9 -0
  54. package/dist/instance-lifecycle.js +27 -1
  55. package/dist/instance-lifecycle.js.map +1 -1
  56. package/dist/kiro-db-reader.d.ts +5 -0
  57. package/dist/kiro-db-reader.js +18 -0
  58. package/dist/kiro-db-reader.js.map +1 -1
  59. package/dist/kiro-engine-status.d.ts +2 -0
  60. package/dist/kiro-engine-status.js +9 -0
  61. package/dist/kiro-engine-status.js.map +1 -1
  62. package/dist/kiro-migrate.d.ts +49 -0
  63. package/dist/kiro-migrate.js +105 -0
  64. package/dist/kiro-migrate.js.map +1 -0
  65. package/dist/kiro-transcript-lane.d.ts +2 -0
  66. package/dist/kiro-transcript-lane.js.map +1 -1
  67. package/dist/kiro-transcript-worker.js +1 -0
  68. package/dist/kiro-transcript-worker.js.map +1 -1
  69. package/dist/kiro-usage-run.d.ts +7 -0
  70. package/dist/kiro-usage-run.js +39 -0
  71. package/dist/kiro-usage-run.js.map +1 -0
  72. package/dist/kiro-usage-scan.d.ts +49 -0
  73. package/dist/kiro-usage-scan.js +223 -0
  74. package/dist/kiro-usage-scan.js.map +1 -0
  75. package/dist/kiro-usage-worker.d.ts +1 -0
  76. package/dist/kiro-usage-worker.js +10 -0
  77. package/dist/kiro-usage-worker.js.map +1 -0
  78. package/dist/kiro-usage.d.ts +58 -0
  79. package/dist/kiro-usage.js +310 -0
  80. package/dist/kiro-usage.js.map +1 -0
  81. package/dist/locale.js +70 -2
  82. package/dist/locale.js.map +1 -1
  83. package/dist/log-service.js +1 -1
  84. package/dist/log-service.js.map +1 -1
  85. package/dist/login-url.d.ts +4 -0
  86. package/dist/login-url.js +22 -0
  87. package/dist/login-url.js.map +1 -0
  88. package/dist/needs-you-hub.d.ts +2 -1
  89. package/dist/needs-you-hub.js +1 -1
  90. package/dist/needs-you-hub.js.map +1 -1
  91. package/dist/needs-you.d.ts +8 -2
  92. package/dist/needs-you.js +6 -0
  93. package/dist/needs-you.js.map +1 -1
  94. package/dist/opencode-usage-scan.d.ts +28 -0
  95. package/dist/opencode-usage-scan.js +92 -0
  96. package/dist/opencode-usage-scan.js.map +1 -0
  97. package/dist/opencode-usage-worker.d.ts +1 -0
  98. package/dist/opencode-usage-worker.js +10 -0
  99. package/dist/opencode-usage-worker.js.map +1 -0
  100. package/dist/outbound-handlers.js +22 -2
  101. package/dist/outbound-handlers.js.map +1 -1
  102. package/dist/outbound-schemas.d.ts +4 -0
  103. package/dist/outbound-schemas.js +4 -0
  104. package/dist/outbound-schemas.js.map +1 -1
  105. package/dist/quickstart-api.js +13 -2
  106. package/dist/quickstart-api.js.map +1 -1
  107. package/dist/quickstart.js +2 -1
  108. package/dist/quickstart.js.map +1 -1
  109. package/dist/setup-form.d.ts +1 -1
  110. package/dist/setup-form.js +7 -1
  111. package/dist/setup-form.js.map +1 -1
  112. package/dist/setup-host.d.ts +9 -0
  113. package/dist/setup-host.js +124 -5
  114. package/dist/setup-host.js.map +1 -1
  115. package/dist/setup-signin-assets.d.ts +2 -0
  116. package/dist/setup-signin-assets.js +6 -0
  117. package/dist/setup-signin-assets.js.map +1 -0
  118. package/dist/setup-wizard.js +2 -0
  119. package/dist/setup-wizard.js.map +1 -1
  120. package/dist/structured-signin-transport.d.ts +8 -0
  121. package/dist/structured-signin-transport.js +79 -0
  122. package/dist/structured-signin-transport.js.map +1 -0
  123. package/dist/structured-signin.d.ts +131 -0
  124. package/dist/structured-signin.js +388 -0
  125. package/dist/structured-signin.js.map +1 -0
  126. package/dist/tool-permissions.js +2 -0
  127. package/dist/tool-permissions.js.map +1 -1
  128. package/dist/topic-commands.js +4 -1
  129. package/dist/topic-commands.js.map +1 -1
  130. package/dist/transcript-sources.d.ts +26 -2
  131. package/dist/transcript-sources.js +87 -40
  132. package/dist/transcript-sources.js.map +1 -1
  133. package/dist/types.d.ts +3 -2
  134. package/dist/ui/backend-signin.js +63 -10
  135. package/dist/ui/fleet-usage.js +19 -15
  136. package/dist/ui/panel-log.js +2 -2
  137. package/dist/ui/panel-needs.js +4 -4
  138. package/dist/ui/shared/app-i18n.js +2 -2
  139. package/dist/ui/shared/app.css +1 -0
  140. package/dist/ui/structured-signin.js +115 -0
  141. package/dist/usage-report.d.ts +20 -0
  142. package/dist/usage-report.js +54 -17
  143. package/dist/usage-report.js.map +1 -1
  144. package/dist/usage-rollup.d.ts +9 -1
  145. package/dist/usage-rollup.js +20 -4
  146. package/dist/usage-rollup.js.map +1 -1
  147. package/dist/web-api.d.ts +1 -0
  148. package/dist/web-api.js +99 -0
  149. package/dist/web-api.js.map +1 -1
  150. package/dist/web-auth.js +1 -1
  151. package/dist/web-auth.js.map +1 -1
  152. package/dist/web-backend-login.d.ts +1 -7
  153. package/dist/web-backend-login.js +2 -24
  154. package/dist/web-backend-login.js.map +1 -1
  155. package/package.json +2 -1
  156. package/templates/windows-shortcuts.ps1 +7 -5
@@ -15,14 +15,15 @@ register("fusage", {
15
15
  range: "Range", today: "Today", r7d: "7 days", month: "This month", all: "All", custom: "Custom", from: "From", to: "To", apply: "Apply",
16
16
  group: "Group by", g_instance: "Agent", g_backend: "Backend", g_model: "Model", g_profile: "Credential profile", g_team: "Team (as configured now)",
17
17
  csv: "Download CSV", basis: "API-equivalent at list price (checked {0}) — not money spent. Subscriptions are not billed per token.",
18
- amount: "API-equivalent", input: "Input", output: "Output", read: "Cache read", write: "Cache write", total: "Total tokens", requests: "Requests", messages: "Messages",
18
+ amount: "API-equivalent", input: "Input", output: "Output", reasoning: "Reasoning", read: "Cache read", write: "Cache write", total: "Total tokens", requests: "Requests", messages: "Messages",
19
19
  notPriced: "not priced", partial: "partial", noTeam: "(no team)", unknown: "unknown",
20
- f_cache_write_not_reported: "cache writes not reported (Codex) — an estimate", f_not_priced: "not priced: {0}", f_source_unknown: "read before backend and profile were recorded",
20
+ f_cache_write_not_reported: "cache writes not reported (Codex, Muse) — an estimate", f_not_priced: "not priced: {0}", f_source_unknown: "read before backend and profile were recorded",
21
+ f_estimated: "estimated from text size — Kiro records no token counts", f_includes_estimates: "includes estimates (Kiro)", f_cost_reported: "amount as the CLI reported it (no list price for this model)", estimatedPart: "of which estimated: {0}",
21
22
  scanning: "Scanning — totals may still grow.", updated: "Updated {0} · read every 10 min", since: "Collected since {0}", notYet: "Not collected yet",
22
23
  refresh: "Refresh",
23
24
  notMeasured: "Not measured", nm_credit_billed: "Kiro is credit-billed and records no tokens", nm_unsupported: "AgEnD can't read this CLI's usage yet", nm_no_data: "nothing read yet",
24
25
  shared: "shares its working directory with {0}; their transcripts can't be told apart",
25
- empty: "Nothing recorded yet", emptyHint: "Usage is read from Claude Code and Codex transcripts, from when the fleet started.",
26
+ empty: "Nothing recorded yet", emptyHint: "Usage is read from Claude Code, Codex, Grok, Muse, OpenCode and Antigravity, and estimated for Kiro, from when the fleet started.",
26
27
  blocked: "Not offered over the public link", blockedHint: "Usage is offered over the public link only when \"Usage report over the public link\" is on in Settings → Web dashboard — on the dashboard reached directly, on the computer running AgEnD.",
27
28
  openSettings: "Open Settings → Web dashboard on that computer",
28
29
  partialDays: "Daily detail from {0}: the earlier days in this range are too many rows to send. The rows and totals above cover the whole range; the trend, the heatmap and the CSV start at {0}.",
@@ -42,14 +43,15 @@ register("fusage", {
42
43
  range: "期間", today: "今天", r7d: "7 天", month: "本月", all: "全部", custom: "自訂", from: "從", to: "到", apply: "套用",
43
44
  group: "分組", g_instance: "Agent", g_backend: "Backend", g_model: "模型", g_profile: "憑證 profile", g_team: "Team(依目前設定)",
44
45
  csv: "下載 CSV", basis: "依牌價換算的 API 等值(查證日期 {0})——不是實際花費。訂閱方案不按 token 計費。",
45
- amount: "API 等值", input: "輸入", output: "輸出", read: "快取讀取", write: "快取寫入", total: "token 總計", requests: "請求數", messages: "訊息數",
46
+ amount: "API 等值", input: "輸入", output: "輸出", reasoning: "推理", read: "快取讀取", write: "快取寫入", total: "token 總計", requests: "請求數", messages: "訊息數",
46
47
  notPriced: "未定價", partial: "部分", noTeam: "(不屬於任何 team)", unknown: "未知",
47
- f_cache_write_not_reported: "未回報快取寫入(Codex)——是估算", f_not_priced: "未定價:{0}", f_source_unknown: "讀取時尚未記錄 backend 與 profile",
48
+ f_cache_write_not_reported: "未回報快取寫入(Codex、Muse)——是估算", f_not_priced: "未定價:{0}", f_source_unknown: "讀取時尚未記錄 backend 與 profile",
49
+ f_estimated: "依文字長度估算——Kiro 不記錄 token 數", f_includes_estimates: "含估算值(Kiro)", f_cost_reported: "金額為 CLI 自行回報(價目表沒有此模型)", estimatedPart: "其中估算:{0}",
48
50
  scanning: "掃描中——總數可能還會增加。", updated: "更新於 {0} · 每 10 分鐘讀取一次", since: "自 {0} 起收集", notYet: "尚未收集",
49
51
  refresh: "重新整理",
50
52
  notMeasured: "未量測", nm_credit_billed: "Kiro 以 credit 計費,不記錄 token", nm_unsupported: "AgEnD 目前還讀不到這個 CLI 的用量", nm_no_data: "尚未讀到任何資料",
51
53
  shared: "與 {0} 共用工作目錄,無法分辨各自的 transcript",
52
- empty: "還沒有任何紀錄", emptyHint: "用量從 Claude Code 與 Codex 的 transcript 讀取,自 fleet 啟動起計。",
54
+ empty: "還沒有任何紀錄", emptyHint: "用量從 Claude Code、Codex、Grok、Muse、OpenCode 與 Antigravity 讀取,Kiro 則為估算,自 fleet 啟動起計。",
53
55
  blocked: "公開連結上不提供", blockedHint: "只有在設定 → 網頁儀表板裡開啟「透過公開連結提供用量報告」時,公開連結才會提供用量;這個設定要在執行 AgEnD 的那台電腦上、直接開啟的儀表板裡變更。",
54
56
  openSettings: "在那台電腦上開啟設定 → 網頁儀表板",
55
57
  partialDays: "每日明細從 {0} 起:這段期間更早的日子資料列太多,無法一次送出。上方的各列與總計涵蓋整段期間;趨勢、熱度圖與 CSV 則從 {0} 開始。",
@@ -95,11 +97,11 @@ export function keyText(group, key) {
95
97
  const writes = (r) => (r.cacheWrite5m || 0) + (r.cacheWrite1h || 0) + (r.cacheWrite || 0);
96
98
  function flagText(f, r) {
97
99
  if (f === "not_priced") return tu("f_not_priced", (r.unpriced || []).join(", "));
98
- return ["cache_write_not_reported", "source_unknown"].includes(f) ? tu(`f_${f}`) : f;
100
+ return ["cache_write_not_reported", "source_unknown", "estimated", "includes_estimates", "cost_reported"].includes(f) ? tu(`f_${f}`) : f;
99
101
  }
100
102
 
101
103
  // ── CSV (design §4): the daily rows of this same answer; messages on their own rows (they are per agent, not per model).
102
- const CSV_COLUMNS = ["date", "zone", "instance", "backend", "model", "tier", "profile", "input", "output", "cache_read", "cache_write_5m", "cache_write_1h", "cache_write", "requests", "messages", "amount", "basis", "flags"];
104
+ const CSV_COLUMNS = ["date", "zone", "instance", "backend", "model", "tier", "profile", "input", "output", "reasoning", "cache_read", "cache_write_5m", "cache_write_1h", "cache_write", "requests", "messages", "amount", "basis", "flags", "estimated"];
103
105
  /** One CSV field: quoted when it must be, and never read as a formula by a spreadsheet (=, +, -, @, tab, CR first). */
104
106
  export function csvField(v) {
105
107
  let s = v === null || v === undefined ? "" : String(v);
@@ -109,11 +111,11 @@ export function csvField(v) {
109
111
  export function usageCsv(report) {
110
112
  const lines = [CSV_COLUMNS.join(",")];
111
113
  for (const d of report.daily || []) {
112
- lines.push([d.date, d.zone, d.instance, d.backend, d.model, d.tier, d.profile, d.input, d.output, d.cacheRead, d.cacheWrite5m, d.cacheWrite1h, d.cacheWrite,
113
- d.requests, "", d.amount === null ? "" : d.amount.toFixed(6), report.basis, (d.flags || []).join(" ")].map(csvField).join(","));
114
+ lines.push([d.date, d.zone, d.instance, d.backend, d.model, d.tier, d.profile, d.input, d.output, d.reasoning, d.cacheRead, d.cacheWrite5m, d.cacheWrite1h, d.cacheWrite,
115
+ d.requests, "", d.amount === null ? "" : d.amount.toFixed(6), report.basis, (d.flags || []).join(" "), d.estimated ? "yes" : "no"].map(csvField).join(","));
114
116
  }
115
117
  for (const m of report.messagesDaily || []) {
116
- lines.push([m.date, report.zone, m.instance, "", "", "", "", "", "", "", "", "", "", "", m.count, "", report.basis, ""].map(csvField).join(","));
118
+ lines.push([m.date, report.zone, m.instance, "", "", "", "", "", "", "", "", "", "", "", "", m.count, "", report.basis, "", ""].map(csvField).join(","));
117
119
  }
118
120
  return `${lines.join("\r\n")}\r\n`;
119
121
  }
@@ -151,7 +153,7 @@ export function levels(values) {
151
153
  return (v) => (!(v > 0) ? 0 : v <= cut[0] ? 1 : v <= cut[1] ? 2 : v <= cut[2] ? 3 : 4);
152
154
  }
153
155
 
154
- const tokensOf = (d) => d.input + d.output + d.cacheRead + d.cacheWrite5m + d.cacheWrite1h + d.cacheWrite;
156
+ const tokensOf = (d) => d.input + d.output + (d.reasoning || 0) + d.cacheRead + d.cacheWrite5m + d.cacheWrite1h + d.cacheWrite;
155
157
  /**
156
158
  * Each day of the answer: its tokens (whether it was used at all), the priced part of its amount, whether part of it is
157
159
  * not priced, and its parts by zone (a day folded before the fleet's timezone changed keeps its own zone, §5.3).
@@ -218,7 +220,7 @@ export function thinXLabels(n, slotPx, labelWidthPx) {
218
220
  /** The trend's series: the top `TOP` agents by the measure over the range, then "others"; one value per date. */
219
221
  export function trendSeries(report, dates) {
220
222
  const priced = report.totals && report.totals.amount !== null && report.totals.amount > 0;
221
- const measure = (d) => (priced ? d.amount || 0 : d.input + d.output + d.cacheRead + d.cacheWrite5m + d.cacheWrite1h + d.cacheWrite);
223
+ const measure = (d) => (priced ? d.amount || 0 : tokensOf(d));
222
224
  const byAgent = new Map();
223
225
  for (const d of report.daily || []) byAgent.set(d.instance, (byAgent.get(d.instance) || 0) + measure(d));
224
226
  const ranked = [...byAgent.entries()].filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
@@ -327,6 +329,7 @@ function Row({ r, group, max, measure }) {
327
329
  <td data-label=${tu("amount")} class=${`num${r.amount === null ? " muted" : ""}`}>${amountText(r)}</td>
328
330
  <td data-label=${tu("input")} class="num">${tokens(r.input)}</td>
329
331
  <td data-label=${tu("output")} class="num">${tokens(r.output)}</td>
332
+ <td data-label=${tu("reasoning")} class="num">${r.reasoningReported === false ? "—" : tokens(r.reasoning || 0)}</td>
330
333
  <td data-label=${tu("read")} class="num">${tokens(r.cacheRead)}</td>
331
334
  <td data-label=${tu("write")} class="num">${r.flags.includes("cache_write_not_reported") && writes(r) === 0 ? "—" : tokens(writes(r))}</td>
332
335
  <td data-label=${tu("total")} class="num">${tokens(r.total)}</td>
@@ -379,13 +382,14 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
379
382
  ${report.scanning.active && (!report.scanning.caughtUp || report.scanning.pendingBytes > 0) ? html` <span class="us-scan" role="status">${tu("scanning")} <button type="button" class="btn btn-sm" onClick=${refresh}>${tu("refresh")}</button></span>` : null}</p>
380
383
  ${empty ? html`<${Empty} icon="chart" title=${tu("empty")} hint=${tu("emptyHint")} />` : html`
381
384
  <dl class="us-totals card">
382
- <div class="ch-metric"><dt>${tu("amount")}</dt><dd>${amountText(report.totals)}</dd></div>
385
+ <div class="ch-metric"><dt>${tu("amount")}</dt><dd>${amountText(report.totals)}${report.totals.estimated
386
+ ? html`<span class="us-est">${report.totals.amountEstimated !== null && report.totals.amountEstimated !== undefined ? tu("estimatedPart", money(report.totals.amountEstimated)) : tu("f_includes_estimates")}</span>` : null}</dd></div>
383
387
  <div class="ch-metric"><dt>${tu("total")}</dt><dd>${tokens(report.totals.total)}</dd></div>
384
388
  <div class="ch-metric"><dt>${tu("requests")}</dt><dd>${n(report.totals.requests)}</dd></div>
385
389
  <div class="ch-metric"><dt>${tu("messages")}</dt><dd>${report.totals.messages === null ? "—" : n(report.totals.messages)}</dd></div>
386
390
  </dl>
387
391
  ${rows.length ? html`<table class="us-table"><thead><tr><th scope="col">${tu(`g_${group}`)}</th><th scope="col" class="num">${tu("amount")}</th>
388
- <th scope="col" class="num">${tu("input")}</th><th scope="col" class="num">${tu("output")}</th><th scope="col" class="num">${tu("read")}</th><th scope="col" class="num">${tu("write")}</th>
392
+ <th scope="col" class="num">${tu("input")}</th><th scope="col" class="num">${tu("output")}</th><th scope="col" class="num">${tu("reasoning")}</th><th scope="col" class="num">${tu("read")}</th><th scope="col" class="num">${tu("write")}</th>
389
393
  <th scope="col" class="num">${tu("total")}</th><th scope="col" class="num">${tu("requests")}</th><th scope="col" class="num">${tu("messages")}</th></tr></thead>
390
394
  <tbody>${rows.map((r) => html`<${Row} key=${r.key} r=${r} group=${group} max=${max} measure=${measure} />`)}</tbody></table>`
391
395
  : html`<p class="note">${tu("noRows")}</p>`}
@@ -62,7 +62,7 @@ register("log", {
62
62
  ev_backend_unreachable: "CLI unreachable", ev_startup_backend_unreachable: "CLI unreachable at start", ev_backend_launch_warning: "Launch warning",
63
63
  ev_clear_action: "Conversation cleared", ev_normal_exit_action: "Exited", ev_reply_drop_detected: "A reply went missing",
64
64
  ev_reply_drop_recovered: "Missing reply recovered", ev_reply_drop_unrecovered: "Missing reply not recovered",
65
- ev_supervision_ended: "Supervision ended", ev_tmux_server_crash: "tmux crashed", ev_malformed_tool_call: "Malformed tool call", ev_health_check_error: "Health check error", ev_working_state: "Working-state journal",
65
+ ev_supervision_ended: "Supervision ended", ev_tmux_server_crash: "tmux crashed", ev_malformed_tool_call: "Malformed tool call", ev_health_check_error: "Health check error", ev_context_lost: "Started fresh: context lost", ev_working_state: "Working-state journal",
66
66
  },
67
67
  "zh-TW": {
68
68
  offTitle: "紀錄未開啟",
@@ -100,7 +100,7 @@ register("log", {
100
100
  ev_backend_unreachable: "連不到 CLI", ev_startup_backend_unreachable: "啟動時連不到 CLI", ev_backend_launch_warning: "啟動警告",
101
101
  ev_clear_action: "對話已清除", ev_normal_exit_action: "已結束", ev_reply_drop_detected: "有回覆遺失",
102
102
  ev_reply_drop_recovered: "遺失的回覆已補回", ev_reply_drop_unrecovered: "遺失的回覆沒有補回",
103
- ev_supervision_ended: "監督結束", ev_tmux_server_crash: "tmux 當掉", ev_malformed_tool_call: "格式錯誤的工具呼叫", ev_health_check_error: "健康檢查錯誤", ev_working_state: "工作狀態紀錄",
103
+ ev_supervision_ended: "監督結束", ev_tmux_server_crash: "tmux 當掉", ev_malformed_tool_call: "格式錯誤的工具呼叫", ev_health_check_error: "健康檢查錯誤", ev_context_lost: "改用全新對話:context 遺失", ev_working_state: "工作狀態紀錄",
104
104
  },
105
105
  });
106
106
  const tl = (key, ...a) => t(`log.${key}`, ...a);
@@ -25,7 +25,7 @@ import { openSignIn, signInOffered } from "./backend-signin.js";
25
25
  register("needs", {
26
26
  en: {
27
27
  title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in your chat app's General.", emptyHintWeb: "When an agent waits for you, it shows here.",
28
- open: "Open chat", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", signIn: "Sign in",
28
+ open: "Open chat", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", fleetSignIns: "Fleet sign-ins", signIn: "Sign in",
29
29
  gap: "not wrapped up: {0}", gapUnchecked: "{0} checklist item(s) unchecked", gapNoResult: "no result", gapJoin: ", ", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
30
30
  ageNow: "just now", ageMin: "{0} min", ageHour: "{0} h", ageDay: "{0} d",
31
31
  notifyTitle: "Notify me on this device", notifyOn: "Notifications are on for this browser while a tab is open.",
@@ -38,7 +38,7 @@ register("needs", {
38
38
  },
39
39
  "zh-TW": {
40
40
  title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在聊天 app 的 General。", emptyHintWeb: "agent 在等你時,會出現在這裡。",
41
- open: "開啟聊天", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", signIn: "登入",
41
+ open: "開啟聊天", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", fleetSignIns: "Fleet 登入", signIn: "登入",
42
42
  gap: "還沒補齊:{0}", gapUnchecked: "還有 {0} 項沒勾", gapNoResult: "沒有產出總結", gapJoin: "、", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
43
43
  ageNow: "剛剛", ageMin: "{0} 分鐘", ageHour: "{0} 小時", ageDay: "{0} 天",
44
44
  notifyTitle: "在這台裝置通知我", notifyOn: "這個瀏覽器的通知已開啟(需要開著分頁)。",
@@ -143,7 +143,7 @@ export function NeedsPanel({ navKey }) {
143
143
  <div data-trust="panel-content" class="panel-body"><div class="col">
144
144
  ${!items.length ? html`<${Empty} icon="check" title=${tn("empty")} hint=${tn(chatApp ? "emptyHint" : "emptyHintWeb")} />`
145
145
  : groups.map((g) => html`<section key=${g.instance} class="n-group" aria-label=${g.instance}>
146
- <h2 class="n-group-head">${g.instance ? html`<a href=${chatPath(g.instance)} title=${g.instance}>${nameFor(g.instance)}</a>` : tn("noAssignee")}</h2>
146
+ <h2 class="n-group-head">${g.instance ? html`<a href=${chatPath(g.instance)} title=${g.instance}>${nameFor(g.instance)}</a>` : tn(g.items.some(i => i.type === "backend") ? "fleetSignIns" : "noAssignee")}</h2>
147
147
  ${g.items.map((item) => html`<${NeedsItem} key=${item.id} item=${item} nowMs=${nowMs} owner=${owner} />`)}
148
148
  </section>`)}
149
149
  <${NotifyToggle} />
@@ -200,7 +200,7 @@ function NeedsItem({ item, nowMs, owner }) {
200
200
  disabled=${promptBusy} onClick=${() => answer(a.id)}>${(p ? p.busy : fallback === "busy") ? tn("answering") : a.label}</button>`) : null}
201
201
  ${item.type === "delivery" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
202
202
  <${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
203
- ${item.reason === "auth_paused" && item.signIn && signInOffered() ? html`<button type="button" class="btn btn-sm btn-primary" onClick=${() => openSignIn(item.signIn)}>${tn("signIn")}</button>` : null}
203
+ ${(item.reason === "auth_paused" || item.reason === "backend_signin") && item.signIn && signInOffered() ? html`<button type="button" class="btn btn-sm btn-primary" onClick=${() => openSignIn(item.signIn)}>${tn("signIn")}</button>` : null}
204
204
  ${item.type === "task" && item.card ? html`<a class="btn btn-sm" href=${`/ui/fleet?card=${encodeURIComponent(item.card.code)}`}>${tn("openCard")}</a>` : null}
205
205
  ${item.instance ? html`<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn("open")}</a>` : null}
206
206
  </div>
@@ -105,7 +105,7 @@ register("app", {
105
105
  confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
106
106
  sidePanelLeave: "Leave {0} without saving? Your changes there will be lost.", sidePanelDiscard: "Discard changes",
107
107
  // The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
108
- needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
108
+ needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
109
109
  },
110
110
  "zh-TW": {
111
111
  brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
@@ -145,6 +145,6 @@ register("app", {
145
145
  search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
146
146
  confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
147
147
  sidePanelLeave: "不儲存就離開「{0}」?你在那裡的修改會遺失。", sidePanelDiscard: "捨棄修改",
148
- needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
148
+ needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
149
149
  },
150
150
  });
@@ -1936,6 +1936,7 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
1936
1936
  .us-bar { display: block; width: 100%; height: 6px; margin: 4px 0; }
1937
1937
  .us-bar-bg { fill: var(--surface-2); } .us-bar-fg { fill: var(--us-0); }
1938
1938
  .us-meta { display: block; font-size: var(--fs-xs); color: var(--text-3); overflow-wrap: anywhere; }
1939
+ .us-est { display: block; font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--text-3); }
1939
1940
  .us-flags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
1940
1941
  .us-trend, .us-heat { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
1941
1942
  .us-trend h3, .us-heat h3, .us-rest h3 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
@@ -0,0 +1,115 @@
1
+ /* Shared browser controls. No terminal relay, provider output or secrets in diagnostics. */
2
+ const NAMES = { "kiro-cli": "Kiro", "claude-code": "Claude Code", codex: "Codex", antigravity: "Antigravity (console fallback)" };
3
+ const CHOICES = { builder_id: "Builder ID", google: "Google", github: "GitHub", organization: "Your organization", subscription: "Claude subscription", console: "Anthropic Console" };
4
+ const ERRORS = {
5
+ unsupported_prompt: "This CLI screen is not supported. Cancel and use the console fallback.",
6
+ cleanup_failed: "The login process could not be confirmed closed. Close it on the host and restart setup before retrying.",
7
+ signin_cleanup_failed: "The login process could not be confirmed closed. Close it on the host and restart setup before retrying.",
8
+ signin_action_stale: "That prompt changed or was already answered. Refresh its status; do not submit it again.",
9
+ signin_console_only: "This backend uses the explicit console fallback.",
10
+ backend_signin_local_only: "Backend sign-in is available only on this computer. Use the local setup page, or sign in later.",
11
+ signin_session_expired: "This setup session has ended. Reopen setup on the host.",
12
+ expired: "This login attempt expired. Start a new attempt.",
13
+ auth_unconfirmed: "The CLI exited, but credentials could not be verified. Retry or sign in later.",
14
+ provider_rejected: "The provider rejected this sign-in. Retry or sign in later.",
15
+ };
16
+
17
+ export function mountStructuredSignIns(root, { endpoint, fetcher = fetch, changed = () => {} } = {}) {
18
+ const doc = root.ownerDocument;
19
+ let view = null, busy = false, disposed = false, timer = null, generation = 0, message = "", rendered = "";
20
+ const el = (tag, text, parent = root) => { const node = doc.createElement(tag); if (text) node.textContent = text; parent.append(node); return node; };
21
+ const errorText = code => ERRORS[code] || "Could not complete sign-in. Check status, retry, or sign in later.";
22
+ const button = (parent, label, action) => { const b = el("button", label, parent); b.type = "button"; b.disabled = busy; b.onclick = action; return b; };
23
+ const request = async data => {
24
+ const response = await fetcher(endpoint, { method: data ? "POST" : "GET", cache: "no-store", headers: { "Content-Type": "application/json" }, ...(data ? { body: JSON.stringify(data) } : {}) });
25
+ const body = await response.json().catch(() => null);
26
+ if (!response.ok) throw new Error(body?.error || "signin_failed");
27
+ return body;
28
+ };
29
+ const accept = next => { view = next; root.dataset.ready = String(view.ready); changed(view); };
30
+ async function perform(data) {
31
+ if (busy || disposed) return;
32
+ busy = true; generation++; message = ""; render();
33
+ try { const next = await request(data); if (!disposed) accept(next); }
34
+ catch (error) { if (!disposed) message = errorText(error?.message); }
35
+ finally { busy = false; if (!disposed) render(); }
36
+ }
37
+ function render() {
38
+ const signature = JSON.stringify([view, busy, message]);
39
+ if (disposed || rendered === signature) return;
40
+ rendered = signature; root.replaceChildren();
41
+ el("h2", "Sign in your AI backends");
42
+ el("p", "Use the controls here; no terminal typing is needed for Kiro, Claude Code or Codex. You can sign in later from the dashboard.");
43
+ if (message) { const alert = el("p", message); alert.setAttribute("role", "alert"); }
44
+ if (!view) { button(root, "Check sign-ins", () => perform({ operation: "refresh" })); return; }
45
+ for (const backend of view.selected) {
46
+ const card = el("section"); card.dataset.backend = backend;
47
+ el("h3", NAMES[backend] || backend, card);
48
+ const state = view.states[backend], pending = view.pending.includes(backend), skipped = view.skipped.includes(backend);
49
+ el("p", skipped ? "Sign in later · remains in Needs you" : state === "valid" ? "Signed in ✓" : state === "not_installed" ? "Not installed on this host" : state === "console_only" ? "Use the explicit console fallback, or sign in later." : "Sign-in needed", card);
50
+ const login = view.login?.backend === backend ? view.login : null;
51
+ const active = login && login.phase !== "done" && !(login.phase === "failed" && login.reason);
52
+ if (active) {
53
+ el("p", login.phase === "waiting" || login.phase === "starting" ? "Waiting for the CLI…" : "Login: " + login.phase.replaceAll("_", " "), card);
54
+ if (login.phase === "method" && login.canAct) for (const choice of login.choices || []) {
55
+ if (CHOICES[choice]) button(card, CHOICES[choice], () => perform({ operation: "input", id: login.id, revision: login.revision, action: { kind: "method", choice } }));
56
+ }
57
+ if (login.url) {
58
+ // The host already allowlists these; still never turn a non-HTTPS URL into a link.
59
+ let url; try { url = new URL(login.url); } catch {}
60
+ if (url?.protocol === "https:" && !url.username && !url.password) {
61
+ const link = el("a", "Open provider sign-in", card); link.href = url.href; link.target = "_blank"; link.rel = "noopener noreferrer";
62
+ }
63
+ }
64
+ if (login.code) { el("p", "Device code:", card); el("code", login.code, card); }
65
+ if (login.canAct && ["code", "start_url", "region"].includes(login.phase)) {
66
+ const label = el("label", { code: "Paste the returned code", start_url: "Organization Start URL", region: "AWS region" }[login.phase], card);
67
+ const input = el("input", null, label); input.type = login.phase === "code" ? "password" : "text"; input.autocomplete = "off"; input.spellcheck = false;
68
+ input.setAttribute("aria-label", label.textContent); input.disabled = busy;
69
+ button(card, "Submit " + login.phase.replaceAll("_", " "), () => { const value = input.value; input.value = ""; void perform({ operation: "input", id: login.id, revision: login.revision, action: { kind: login.phase, value } }); });
70
+ }
71
+ if (["method", "code", "start_url", "region"].includes(login.phase) && !login.canAct) el("p", "Answer sent. Waiting for the next screen; it will not be sent twice.", card);
72
+ button(card, "Cancel sign-in", () => perform({ operation: "cancel" }));
73
+ } else {
74
+ if (login?.reason && login.reason !== "cancelled") el("p", errorText(login.reason), card);
75
+ const usable = !["antigravity"].includes(backend) && state !== "not_installed";
76
+ if (usable) button(card, state === "valid" ? "Sign in again" : "Sign in", () => {
77
+ const relogin = state === "valid";
78
+ if (relogin && !globalThis.confirm("Sign in again? Kiro will sign out its existing account first.")) return;
79
+ void perform({ operation: "start", backend, relogin });
80
+ });
81
+ }
82
+ if (pending && !skipped) button(card, "Sign in later", () => perform({ operation: "skip", backend }));
83
+ }
84
+ button(root, "Check sign-ins", () => perform({ operation: "refresh" }));
85
+ if (view.ready) el("p", "You can continue setup.");
86
+ }
87
+ async function poll() {
88
+ if (disposed) return;
89
+ const before = generation;
90
+ if (!busy) try { const next = await request(); if (!disposed && before === generation) { accept(next); render(); } }
91
+ catch (error) { if (!disposed && before === generation) { message = errorText(error?.message); render(); } }
92
+ if (!disposed) timer = setTimeout(poll, 1000);
93
+ }
94
+ render(); void poll();
95
+ return {
96
+ addBackends(backends) {
97
+ if (view && Array.isArray(backends) && backends.every(b => NAMES[b]) && backends.some(b => !view.selected.includes(b)))
98
+ return perform({ operation: "select", backends: [...new Set([...view.selected, ...backends])] });
99
+ },
100
+ addBackend(backend) { if (view && NAMES[backend] && !view.selected.includes(backend)) return perform({ operation: "select", backends: [...view.selected, backend] }); },
101
+ dispose() { disposed = true; generation++; clearTimeout(timer); root.replaceChildren(); },
102
+ };
103
+ }
104
+
105
+ const setupRoot = typeof document === "undefined" ? null : document.getElementById("setupSignins");
106
+ if (setupRoot) {
107
+ const cards = mountStructuredSignIns(setupRoot, { endpoint: "setup/signins" });
108
+ const backend = document.getElementById("backend");
109
+ const select = () => cards.addBackend(backend?.value);
110
+ backend?.addEventListener("change", select);
111
+ const observer = new MutationObserver(select); if (backend) observer.observe(backend, { childList: true });
112
+ // Initial GET may finish after the environment list, so also observe our readiness result.
113
+ const ready = new MutationObserver(select); ready.observe(setupRoot, { attributes: true, attributeFilter: ["data-ready"] });
114
+ addEventListener("pagehide", () => { observer.disconnect(); ready.disconnect(); cards.dispose(); }, { once: true });
115
+ }
@@ -7,6 +7,9 @@
7
7
  * Honest by construction: an amount is "API-equivalent at list price (checked <date>) — not money spent"; an unpriced
8
8
  * model's tokens are shown and its amount is not; Codex's cache writes are "not reported" (every CLI seen records 0),
9
9
  * never priced as uncached; an instance whose backend AgEnD cannot measure is listed with its reason, never as $0.
10
+ * Kiro records no token counts: its rows are ESTIMATED from text size (kiro-usage.ts, design §10.2) and say so — on the
11
+ * row (`estimated`), on any total that includes one (`includes_estimates`, with the estimated part of the amount), in
12
+ * the API, on the page and in the CSV. An estimate never mixes silently into a measured number.
10
13
  */
11
14
  import type { Ledger } from "./cache-ledger.js";
12
15
  import { type UsageDays } from "./usage-rollup.js";
@@ -27,6 +30,12 @@ export declare const usageBasis: (checked?: string) => string;
27
30
  * latest whole days and says from which date (`dailyFrom`); the rows and totals always cover the whole range.
28
31
  */
29
32
  export declare const USAGE_DAILY_MAX = 20000;
33
+ /** The backends whose usage is estimated, not measured (#2122 §10.2: Kiro records no token counts). */
34
+ export declare const ESTIMATED_BACKENDS: ReadonlySet<string>;
35
+ /** The backends whose transcripts report reasoning tokens (#2122 §10.3); elsewhere a 0 means not reported. */
36
+ export declare const REPORTS_REASONING: ReadonlySet<string>;
37
+ /** The backends whose transcripts carry a cache-write field that stays 0: "not reported" until a real write is seen. */
38
+ export declare const WRITES_UNREPORTED: ReadonlySet<string>;
30
39
  export interface UsageInstanceInput {
31
40
  name: string;
32
41
  /** Its backend now: what it is collected with from here on (each slice of its history carries its own). */
@@ -63,7 +72,10 @@ export interface UsageInput {
63
72
  export interface UsageTotals {
64
73
  /** Uncached input. */
65
74
  input: number;
75
+ /** Output without its reasoning part — the units of tokscale's daily rows (design §10.3). */
66
76
  output: number;
77
+ /** The part of the output a CLI reports as reasoning (Codex); 0 where none is reported (Claude). */
78
+ reasoning: number;
67
79
  cacheRead: number;
68
80
  cacheWrite5m: number;
69
81
  cacheWrite1h: number;
@@ -73,6 +85,12 @@ export interface UsageTotals {
73
85
  }
74
86
  export interface UsageRow extends UsageTotals {
75
87
  key: string;
88
+ /** Whether any of it comes from a CLI that reports reasoning (REPORTS_REASONING): false → its 0 is "not reported". */
89
+ reasoningReported: boolean;
90
+ /** Some of it is estimated (ESTIMATED_BACKENDS); flagged "estimated" (a row) or "includes_estimates" (the totals). */
91
+ estimated: boolean;
92
+ /** The estimated part of `amount` (null: nothing estimated is priced). */
93
+ amountEstimated: number | null;
76
94
  total: number;
77
95
  /** Delivered messages; null where a message has no such key (by model, by profile). */
78
96
  messages: number | null;
@@ -92,6 +110,8 @@ export interface UsageRow extends UsageTotals {
92
110
  }
93
111
  export interface UsageDaily extends UsageTotals {
94
112
  date: string;
113
+ reasoningReported: boolean;
114
+ estimated: boolean;
95
115
  zone: string;
96
116
  instance: string;
97
117
  backend: string;
@@ -10,6 +10,12 @@ export const usageBasis = (checked = PRICES_CHECKED) => `API-equivalent at list
10
10
  */
11
11
  export const USAGE_DAILY_MAX = 20_000;
12
12
  const M = 1_000_000;
13
+ /** The backends whose usage is estimated, not measured (#2122 §10.2: Kiro records no token counts). */
14
+ export const ESTIMATED_BACKENDS = new Set(["kiro-cli"]);
15
+ /** The backends whose transcripts report reasoning tokens (#2122 §10.3); elsewhere a 0 means not reported. */
16
+ export const REPORTS_REASONING = new Set(["codex", "grok", "muse", "opencode", "antigravity"]);
17
+ /** The backends whose transcripts carry a cache-write field that stays 0: "not reported" until a real write is seen. */
18
+ export const WRITES_UNREPORTED = new Set(["codex", "muse"]);
13
19
  const DATE = /^\d{4}-\d{2}-\d{2}$/;
14
20
  /** A range as asked (query string or tool argument); anything else is the last 7 days. */
15
21
  export function usageRange(kind, from, to) {
@@ -31,19 +37,23 @@ export function rangeDates(range, now, zone) {
31
37
  case "custom": return { from: range.from, to: range.to };
32
38
  }
33
39
  }
34
- const zero = () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheWrite: 0, requests: 0 });
35
- const addTotals = (into, [n, uncached, read, w5, w1h, w, out]) => {
40
+ const zero = () => ({ input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheWrite: 0, requests: 0 });
41
+ /** One slice into a row: `reasoning` is the part of the slice's output reported as reasoning (shown beside the rest). */
42
+ const addTotals = (into, [n, uncached, read, w5, w1h, w, out], reasoning) => {
43
+ const r = Math.min(Math.max(0, reasoning), out);
36
44
  into.requests += n;
37
45
  into.input += uncached;
38
46
  into.cacheRead += read;
39
47
  into.cacheWrite5m += w5;
40
48
  into.cacheWrite1h += w1h;
41
49
  into.cacheWrite += w;
42
- into.output += out;
50
+ into.output += out - r;
51
+ into.reasoning += r;
43
52
  };
44
53
  const splitKey = (mk) => { const bar = mk.lastIndexOf("|"); return { model: mk.slice(0, bar), long: mk.slice(bar + 1) === "1" }; };
45
54
  /**
46
- * The list-price amount of one slice, or null when its model is not in the table. Codex writes not reported are 0 in the
55
+ * The list-price amount of one slice, or null when its model is not in the table. Reasoning is a part of the slice's
56
+ * output, so the whole output is priced at the output rate once. Codex writes not reported are 0 in the
47
57
  * ledger (it marks writesSeen on the first one above 0), so they add nothing and are never priced as uncached input.
48
58
  */
49
59
  function amountOf(model, long, [, uncached, read, w5, w1h, w, out]) {
@@ -52,6 +62,7 @@ function amountOf(model, long, [, uncached, read, w5, w1h, w, out]) {
52
62
  return null;
53
63
  return (uncached * r.input + read * r.read + w5 * r.write5m + w1h * r.write1h + w * r.write + out * r.output) / M;
54
64
  }
65
+ const estimatedSlice = (s) => ESTIMATED_BACKENDS.has(s.backend);
55
66
  const unknownSource = (s) => s.backend === "unknown" || s.profile === "unknown";
56
67
  export function buildUsageReport(input) {
57
68
  const { from, to } = rangeDates(input.range, input.now, input.zone);
@@ -70,14 +81,14 @@ export function buildUsageReport(input) {
70
81
  if (!i.ledger || !i.days)
71
82
  continue;
72
83
  coverage.push({ name: i.name, intervals: i.days.coverage.map(([a, b]) => [a, b]) });
73
- // Codex: a write is reported only once a real one has been seen (every CLI so far records 0).
84
+ // Codex and Muse: a write is reported only once a real one has been seen (every CLI so far records 0).
74
85
  const writesSeen = i.ledger.writesSeen === true;
75
86
  const base = { instance: i.name, team: i.team };
76
87
  for (const row of i.days.rows) {
77
88
  if (!inRange(row.date))
78
89
  continue;
79
90
  for (const [mk, tot] of Object.entries(row.m))
80
- slices.push({ ...base, backend: row.be, writesReported: row.be !== "codex" || writesSeen, date: row.date, zone: row.zone, profile: row.pf, ...splitKey(mk), tot });
91
+ slices.push({ ...base, backend: row.be, writesReported: !WRITES_UNREPORTED.has(row.be) || writesSeen, date: row.date, zone: row.zone, profile: row.pf, ...splitKey(mk), tot, reasoning: row.r?.[mk] ?? 0, cost: row.c?.[mk] ?? 0 });
81
92
  }
82
93
  for (const [hk, hour] of Object.entries(i.ledger.hours)) {
83
94
  const h = Number(hk);
@@ -88,7 +99,7 @@ export function buildUsageReport(input) {
88
99
  continue;
89
100
  for (const src of hourSources(hour)) {
90
101
  for (const [mk, tot] of Object.entries(src.m))
91
- slices.push({ ...base, backend: src.backend, writesReported: src.backend !== "codex" || writesSeen, date, zone: input.zone, profile: src.profile, ...splitKey(mk), tot });
102
+ slices.push({ ...base, backend: src.backend, writesReported: !WRITES_UNREPORTED.has(src.backend) || writesSeen, date, zone: input.zone, profile: src.profile, ...splitKey(mk), tot, reasoning: src.r[mk] ?? 0, cost: src.c[mk] ?? 0 });
92
103
  }
93
104
  }
94
105
  }
@@ -102,7 +113,7 @@ export function buildUsageReport(input) {
102
113
  }
103
114
  };
104
115
  const rows = new Map();
105
- const newRow = (key) => ({ key, ...zero(), total: 0, messages: null, amount: null, priced: "none", flags: [], unpriced: [], instances: [], backends: [], models: [], profiles: [], _amount: 0, _pricedAny: false, _unpricedAny: false });
116
+ const newRow = (key) => ({ key, ...zero(), reasoningReported: false, estimated: false, amountEstimated: null, _estAmount: 0, _estPriced: false, total: 0, messages: null, amount: null, priced: "none", flags: [], unpriced: [], instances: [], backends: [], models: [], profiles: [], _amount: 0, _pricedAny: false, _unpricedAny: false });
106
117
  const uniq = (list, v) => { if (!list.includes(v))
107
118
  list.push(v); };
108
119
  // Daily rows: one per day, zone, agent, backend, model, tier and profile (an hour's slices summed into their day).
@@ -110,12 +121,18 @@ export function buildUsageReport(input) {
110
121
  for (const s of slices) {
111
122
  const key = keyOf(s);
112
123
  const row = rows.get(key) ?? (rows.set(key, newRow(key)), rows.get(key));
113
- addTotals(row, s.tot);
124
+ addTotals(row, s.tot, s.reasoning);
125
+ if (REPORTS_REASONING.has(s.backend))
126
+ row.reasoningReported = true;
114
127
  uniq(row.instances, s.instance);
115
128
  uniq(row.backends, s.backend);
116
129
  uniq(row.models, s.model);
117
130
  uniq(row.profiles, s.profile);
118
- const amount = amountOf(s.model, s.long, s.tot);
131
+ // List price first; a model the table lacks takes the cost its CLI reported (Grok), said so (design §10.2).
132
+ const listed = amountOf(s.model, s.long, s.tot), reported = listed === null && s.cost > 0;
133
+ const amount = listed ?? (reported ? s.cost : null);
134
+ if (reported)
135
+ uniq(row.flags, "cost_reported");
119
136
  if (amount === null) {
120
137
  row._unpricedAny = true;
121
138
  uniq(row.unpriced, s.model);
@@ -124,6 +141,14 @@ export function buildUsageReport(input) {
124
141
  row._pricedAny = true;
125
142
  row._amount += amount;
126
143
  }
144
+ if (estimatedSlice(s)) {
145
+ row.estimated = true;
146
+ uniq(row.flags, "estimated");
147
+ if (amount !== null) {
148
+ row._estPriced = true;
149
+ row._estAmount += amount;
150
+ }
151
+ }
127
152
  if (!s.writesReported)
128
153
  uniq(row.flags, "cache_write_not_reported");
129
154
  if (unknownSource(s))
@@ -132,11 +157,13 @@ export function buildUsageReport(input) {
132
157
  const dk = [s.date, s.zone, s.instance, s.backend, s.model, tier, s.profile].join("\u0000");
133
158
  let d = dailyBy.get(dk);
134
159
  if (!d) {
135
- d = { date: s.date, zone: s.zone, instance: s.instance, backend: s.backend, model: s.model, tier, profile: s.profile, ...zero(), amount: amount === null ? null : 0,
136
- flags: [...(s.writesReported ? [] : ["cache_write_not_reported"]), ...(amount === null ? ["not_priced"] : []), ...(unknownSource(s) ? ["source_unknown"] : [])] };
160
+ d = { date: s.date, zone: s.zone, instance: s.instance, backend: s.backend, model: s.model, tier, profile: s.profile, ...zero(), reasoningReported: REPORTS_REASONING.has(s.backend), estimated: estimatedSlice(s), amount: amount === null ? null : 0,
161
+ flags: [...(estimatedSlice(s) ? ["estimated"] : []), ...(s.writesReported ? [] : ["cache_write_not_reported"]), ...(amount === null ? ["not_priced"] : []), ...(unknownSource(s) ? ["source_unknown"] : [])] };
137
162
  dailyBy.set(dk, d);
138
163
  }
139
- addTotals(d, s.tot);
164
+ addTotals(d, s.tot, s.reasoning);
165
+ if (reported && !d.flags.includes("cost_reported"))
166
+ d.flags.push("cost_reported");
140
167
  if (amount !== null && d.amount !== null)
141
168
  d.amount += amount;
142
169
  }
@@ -188,8 +215,9 @@ export function buildUsageReport(input) {
188
215
  row.messages ??= 0;
189
216
  }
190
217
  const finish = (row) => {
191
- const { _amount, _pricedAny, _unpricedAny, ...rest } = row;
192
- rest.total = rest.input + rest.output + rest.cacheRead + rest.cacheWrite5m + rest.cacheWrite1h + rest.cacheWrite;
218
+ const { _amount, _pricedAny, _unpricedAny, _estAmount, _estPriced, ...rest } = row;
219
+ rest.amountEstimated = _estPriced ? _estAmount : null;
220
+ rest.total = rest.input + rest.output + rest.reasoning + rest.cacheRead + rest.cacheWrite5m + rest.cacheWrite1h + rest.cacheWrite;
193
221
  rest.amount = _pricedAny ? _amount : null;
194
222
  rest.priced = _pricedAny ? (_unpricedAny ? "partial" : "all") : "none";
195
223
  if (_unpricedAny)
@@ -200,15 +228,24 @@ export function buildUsageReport(input) {
200
228
  .sort((a, b) => (b.amount ?? -1) - (a.amount ?? -1) || b.total - a.total || a.key.localeCompare(b.key));
201
229
  const totals = finish(newRow("all"));
202
230
  for (const r of out) {
203
- for (const k of ["input", "output", "cacheRead", "cacheWrite5m", "cacheWrite1h", "cacheWrite", "requests"])
231
+ for (const k of ["input", "output", "reasoning", "cacheRead", "cacheWrite5m", "cacheWrite1h", "cacheWrite", "requests"])
204
232
  totals[k] += r[k];
205
233
  totals.total += r.total;
206
234
  if (r.messages !== null)
207
235
  totals.messages = (totals.messages ?? 0) + r.messages;
208
236
  if (r.amount !== null)
209
237
  totals.amount = (totals.amount ?? 0) + r.amount;
238
+ if (r.reasoningReported)
239
+ totals.reasoningReported = true;
240
+ if (r.estimated) {
241
+ totals.estimated = true;
242
+ uniq(totals.flags, "includes_estimates");
243
+ }
244
+ if (r.amountEstimated !== null)
245
+ totals.amountEstimated = (totals.amountEstimated ?? 0) + r.amountEstimated;
210
246
  for (const f of r.flags)
211
- uniq(totals.flags, f);
247
+ if (f !== "estimated")
248
+ uniq(totals.flags, f);
212
249
  for (const m of r.unpriced)
213
250
  uniq(totals.unpriced, m);
214
251
  }