Unified Agent + reusable Go agent core.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add cache stats columns and quiet projection refresh log

Lyric 324778af f9552d4e

+145 -4
+1 -1
internal/llmstats/projection.go
··· 121 121 if rebuild { 122 122 mode = "rebuild" 123 123 } 124 - logger.Info("llm_usage_projection_refreshed", 124 + logger.Debug("llm_usage_projection_refreshed", 125 125 "mode", mode, 126 126 "duration_ms", time.Since(startedAt).Milliseconds(), 127 127 "journal_segments", len(segments),
+6
web/console/src/i18n/index.js
··· 189 189 stats_output_cost: "Output Cost", 190 190 stats_cached_input_cost: "Cached Input Cost", 191 191 stats_cache_creation_input_cost: "Cache Write Cost", 192 + stats_cache_rate: "Cache Rate", 193 + stats_cache_cost_delta: "Cache Cost Delta", 192 194 stats_tokens: "Tokens", 193 195 stats_api_host: "API Host", 194 196 stats_model: "Model", ··· 831 833 stats_output_cost: "输出费用", 832 834 stats_cached_input_cost: "缓存命中费用", 833 835 stats_cache_creation_input_cost: "缓存写入费用", 836 + stats_cache_rate: "缓存率", 837 + stats_cache_cost_delta: "缓存费用变动", 834 838 stats_tokens: "Tokens", 835 839 stats_api_host: "API Host", 836 840 stats_model: "模型", ··· 1473 1477 stats_output_cost: "出力コスト", 1474 1478 stats_cached_input_cost: "キャッシュ入力コスト", 1475 1479 stats_cache_creation_input_cost: "キャッシュ書き込みコスト", 1480 + stats_cache_rate: "キャッシュ率", 1481 + stats_cache_cost_delta: "キャッシュ費用変動", 1476 1482 stats_tokens: "Tokens", 1477 1483 stats_api_host: "API Host", 1478 1484 stats_model: "モデル",
+11 -1
web/console/src/views/StatsView.css
··· 5 5 --stats-muted: color-mix(in srgb, var(--text-1) 70%, var(--accent-1) 30%); 6 6 --stats-line: color-mix(in srgb, var(--line) 74%, var(--accent-1) 26%); 7 7 --stats-line-soft: color-mix(in srgb, var(--line-soft) 72%, var(--accent-1) 28%); 8 + --stats-cost-up: color-mix(in srgb, var(--danger) 76%, var(--text-0) 24%); 9 + --stats-cost-down: color-mix(in srgb, var(--ok) 72%, var(--text-0) 28%); 8 10 display: grid; 9 11 gap: clamp(20px, 2.6vw, 34px); 10 12 padding-bottom: 24px; ··· 566 568 } 567 569 568 570 .stats-model-ledger-value-cell { 569 - min-width: 126px; 571 + min-width: 132px; 570 572 font-family: var(--q-card-title-font-family); 571 573 font-size: 1rem; 572 574 font-weight: 600; ··· 580 582 .stats-model-ledger-value-cell-unavailable { 581 583 color: var(--stats-muted); 582 584 font-weight: 500; 585 + } 586 + 587 + .stats-model-ledger-value-cell-cost-up { 588 + color: var(--stats-cost-up); 589 + } 590 + 591 + .stats-model-ledger-value-cell-cost-down { 592 + color: var(--stats-cost-down); 583 593 } 584 594 585 595 .stats-empty {
+127 -2
web/console/src/views/StatsView.js
··· 51 51 return Math.trunc(n).toLocaleString(); 52 52 } 53 53 54 + function toFiniteNumber(value) { 55 + const n = Number(value); 56 + return Number.isFinite(n) ? n : 0; 57 + } 58 + 54 59 function formatCost(value, currency = "USD") { 55 60 const n = Number(value); 56 61 if (!Number.isFinite(n)) { ··· 85 90 } 86 91 } 87 92 93 + function formatSignedFixedCost(value, currency = "USD", fractionDigits = 6) { 94 + const n = Number(value); 95 + if (!Number.isFinite(n)) { 96 + return "-"; 97 + } 98 + const rounded = Number(n.toFixed(fractionDigits)); 99 + if (!Number.isFinite(rounded)) { 100 + return "-"; 101 + } 102 + try { 103 + return new Intl.NumberFormat(undefined, { 104 + style: "currency", 105 + currency: String(currency || "USD").toUpperCase(), 106 + minimumFractionDigits: fractionDigits, 107 + maximumFractionDigits: fractionDigits, 108 + signDisplay: "exceptZero", 109 + }).format(rounded); 110 + } catch { 111 + const sign = rounded > 0 ? "+" : rounded < 0 ? "-" : ""; 112 + return `${sign}${String(currency || "USD").toUpperCase()} ${Math.abs(rounded).toFixed(fractionDigits)}`; 113 + } 114 + } 115 + 116 + function formatPercent(value) { 117 + const n = Number(value); 118 + if (!Number.isFinite(n)) { 119 + return "-"; 120 + } 121 + const clamped = Math.min(Math.max(n, 0), 1); 122 + return new Intl.NumberFormat(undefined, { 123 + style: "percent", 124 + minimumFractionDigits: 1, 125 + maximumFractionDigits: 1, 126 + }).format(clamped); 127 + } 128 + 129 + function modelCacheBaseInputTokens(row) { 130 + const inputTokens = toFiniteNumber(row?.input_tokens); 131 + const cachedInputTokens = toFiniteNumber(row?.cached_input_tokens); 132 + const cacheCreationInputTokens = toFiniteNumber(row?.cache_creation_input_tokens); 133 + return Math.max(0, inputTokens - cachedInputTokens - cacheCreationInputTokens); 134 + } 135 + 136 + function modelCacheRate(row) { 137 + const inputTokens = toFiniteNumber(row?.input_tokens); 138 + if (inputTokens <= 0) { 139 + return null; 140 + } 141 + const cachedInputTokens = Math.min(toFiniteNumber(row?.cached_input_tokens), inputTokens); 142 + return Math.max(0, cachedInputTokens / inputTokens); 143 + } 144 + 145 + function modelCacheCostDelta(row) { 146 + const inputTokens = toFiniteNumber(row?.input_tokens); 147 + if (inputTokens <= 0) { 148 + return null; 149 + } 150 + 151 + const cachedInputTokens = toFiniteNumber(row?.cached_input_tokens); 152 + const cacheCreationInputTokens = toFiniteNumber(row?.cache_creation_input_tokens); 153 + if (cachedInputTokens <= 0 && cacheCreationInputTokens <= 0) { 154 + return 0; 155 + } 156 + 157 + const baseInputTokens = modelCacheBaseInputTokens(row); 158 + if (baseInputTokens <= 0 || !hasMetricValue(row, "input_cost")) { 159 + return null; 160 + } 161 + 162 + const inputCost = Number(row?.input_cost); 163 + if (!Number.isFinite(inputCost)) { 164 + return null; 165 + } 166 + 167 + const baseInputCostPerToken = inputCost / baseInputTokens; 168 + if (!Number.isFinite(baseInputCostPerToken)) { 169 + return null; 170 + } 171 + 172 + const actualInputCost = 173 + inputCost + toFiniteNumber(row?.cached_input_cost) + toFiniteNumber(row?.cache_creation_input_cost); 174 + const baselineInputCostWithoutCache = baseInputCostPerToken * inputTokens; 175 + return actualInputCost - baselineInputCostWithoutCache; 176 + } 177 + 88 178 function summaryHeroMetric(t, totals, key) { 89 179 const costCurrency = typeof totals?.cost_currency === "string" ? totals.cost_currency : "USD"; 90 180 switch (key) { ··· 283 373 { key: "output_cost", label: t("stats_output_cost"), kind: "cost" }, 284 374 { key: "cached_input_cost", label: t("stats_cached_input_cost"), kind: "cost" }, 285 375 { key: "cache_creation_input_cost", label: t("stats_cache_creation_input_cost"), kind: "cost" }, 376 + { key: "cache_cost_delta", label: t("stats_cache_cost_delta"), kind: "cache_cost_delta" }, 286 377 ]; 287 378 return columns; 288 379 } ··· 294 385 { key: "output_tokens", label: t("stats_output_tokens"), kind: "token" }, 295 386 { key: "cached_input_tokens", label: t("stats_cached_input_tokens"), kind: "token" }, 296 387 { key: "cache_creation_input_tokens", label: t("stats_cache_creation_input_tokens"), kind: "token" }, 388 + { key: "cache_rate", label: t("stats_cache_rate"), kind: "cache_rate" }, 297 389 ]; 298 390 return columns; 299 391 } ··· 303 395 const currency = typeof row?.cost_currency === "string" ? row.cost_currency : "USD"; 304 396 return hasMetricValue(row, column.key) ? formatFixedCost(row[column.key], currency) : "-"; 305 397 } 398 + if (column.kind === "cache_cost_delta") { 399 + const currency = typeof row?.cost_currency === "string" ? row.cost_currency : "USD"; 400 + const delta = modelCacheCostDelta(row); 401 + return delta === null ? "-" : formatSignedFixedCost(delta, currency); 402 + } 403 + if (column.kind === "cache_rate") { 404 + const rate = modelCacheRate(row); 405 + return rate === null ? "-" : formatPercent(rate); 406 + } 306 407 return hasMetricValue(row, column.key) ? formatNumber(row[column.key]) : "-"; 307 408 } 308 409 309 410 function isModelLedgerValueUnavailable(row, column) { 411 + if (column.kind === "cache_cost_delta") { 412 + return modelCacheCostDelta(row) === null; 413 + } 414 + if (column.kind === "cache_rate") { 415 + return modelCacheRate(row) === null; 416 + } 310 417 return !hasMetricValue(row, column.key); 311 418 } 312 419 420 + function modelLedgerValueToneClass(row, column) { 421 + if (column.kind !== "cache_cost_delta") { 422 + return ""; 423 + } 424 + const delta = modelCacheCostDelta(row); 425 + if (delta === null || Math.abs(delta) < 1e-12) { 426 + return ""; 427 + } 428 + return delta > 0 ? "stats-model-ledger-value-cell-cost-up" : "stats-model-ledger-value-cell-cost-down"; 429 + } 430 + 313 431 function normalizeModelName(value) { 314 432 return String(value || "").trim().toLowerCase(); 315 433 } ··· 457 575 modelLedgerTokenColumns, 458 576 formatModelLedgerValue, 459 577 isModelLedgerValueUnavailable, 578 + modelLedgerValueToneClass, 460 579 modelVendorMeta, 461 580 formatNumber, 462 581 }; ··· 624 743 v-for="column in modelLedgerCostColumns(host.models)" 625 744 :key="host.api_host + ':' + model.model + ':cost:' + column.key" 626 745 class="stats-model-ledger-value-cell" 627 - :class="{ 'stats-model-ledger-value-cell-unavailable': isModelLedgerValueUnavailable(model, column) }" 746 + :class="[ 747 + { 'stats-model-ledger-value-cell-unavailable': isModelLedgerValueUnavailable(model, column) }, 748 + modelLedgerValueToneClass(model, column), 749 + ]" 628 750 > 629 751 {{ formatModelLedgerValue(model, column) }} 630 752 </td> ··· 718 840 v-for="column in modelLedgerCostColumns(visibleModels)" 719 841 :key="model.model + ':cost:' + column.key" 720 842 class="stats-model-ledger-value-cell" 721 - :class="{ 'stats-model-ledger-value-cell-unavailable': isModelLedgerValueUnavailable(model, column) }" 843 + :class="[ 844 + { 'stats-model-ledger-value-cell-unavailable': isModelLedgerValueUnavailable(model, column) }, 845 + modelLedgerValueToneClass(model, column), 846 + ]" 722 847 > 723 848 {{ formatModelLedgerValue(model, column) }} 724 849 </td>