@kenz1117/dsh-ui-usage-billing 0.2.1 → 0.2.2

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.
package/lib/client.js CHANGED
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
23
23
  }
24
24
  //#endregion
25
25
  //#region \0dsh-css:/Users/ken/deepseek-harness/packages/client/ui-usage-billing/src/client/UsageBilling.module.css.mjs
26
- const css = ".VWh0dG_railButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:36px;height:36px;color:var(--dsw-static-blue-500);cursor:pointer;border-radius:10px;justify-content:center;align-items:center;padding:0;transition:background-color .14s,border-color .14s,color .14s;display:flex}.VWh0dG_railButton:hover,.VWh0dG_railButton:focus-visible{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3);color:var(--dsw-static-blue-400);outline:none}.VWh0dG_railButton svg{stroke-width:2px;width:18px;height:18px}.VWh0dG_trigger{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;border-radius:12px;align-items:center;gap:10px;padding:9px 12px;transition:border-color .14s,background-color .14s,transform .12s;display:flex}.VWh0dG_trigger:hover,.VWh0dG_trigger:focus-visible{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover);outline:none}.VWh0dG_trigger:active{transform:scale(.985)}.VWh0dG_triggerIcon{background:linear-gradient(135deg, var(--dsw-static-blue-500), var(--dsw-static-deepseek-500));width:30px;height:30px;color:var(--dsw-static-neutral-bluish-00);border-radius:9px;flex:none;justify-content:center;align-items:center;display:inline-flex}.VWh0dG_triggerIcon svg{stroke-width:2.2px;width:15px;height:15px}.VWh0dG_triggerText{flex-direction:column;gap:1px;min-width:0;display:flex}.VWh0dG_triggerLabel{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:16px}.VWh0dG_triggerMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;align-items:baseline;gap:6px;font-size:11px;line-height:15px;display:flex}.VWh0dG_triggerMeta strong{color:var(--dsw-static-blue-500);font-variant-numeric:tabular-nums;font-size:13px;font-weight:600}.VWh0dG_triggerMeta em{color:var(--dsw-alias-label-caption);font-style:normal}.VWh0dG_triggerChevron{width:14px;height:14px;color:var(--dsw-alias-label-caption);flex:none;margin-left:auto}.VWh0dG_dashboardModal{width:min(760px,100vw - 48px);max-height:min(760px,88vh)}.VWh0dG_dashboard{flex-direction:column;width:100%;max-height:min(760px,88vh);display:flex}.VWh0dG_dashboardHead{border-bottom:1px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:flex-start;gap:12px;padding:20px 24px 14px;display:flex}.VWh0dG_dashboardTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:17px;font-weight:600;line-height:24px}.VWh0dG_dashboardSubtitle{color:var(--dsw-alias-label-caption);margin:3px 0 0;font-size:12px;line-height:17px}.VWh0dG_closeButton{width:30px;height:30px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;transition:background-color .14s;display:inline-flex}.VWh0dG_closeButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_closeButton svg{stroke-width:2px;width:16px;height:16px}.VWh0dG_dashboardBody{flex-direction:column;gap:14px;padding:16px 24px 24px;display:flex;overflow-y:auto}.VWh0dG_hero{background:linear-gradient(135deg, var(--dsw-static-blue-800) 0%, var(--dsw-static-deepseek-600) 55%, var(--dsw-static-deepseek-500) 100%);min-height:118px;color:var(--dsw-static-neutral-bluish-00);border-radius:16px;align-items:center;gap:24px;padding:18px 22px;display:flex;position:relative;overflow:hidden}.VWh0dG_hero:after{content:\"\";pointer-events:none;background:radial-gradient(circle,#ffffff29 0%,#fff0 70%);border-radius:50%;width:200px;height:200px;position:absolute;top:-60px;right:-40px}.VWh0dG_heroMain{flex-direction:column;flex:auto;justify-content:center;gap:3px;min-width:0;display:flex}.VWh0dG_heroLabel{opacity:.82;white-space:nowrap;font-size:12px;font-weight:500;line-height:17px}.VWh0dG_heroValue{letter-spacing:-.02em;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:38px;font-weight:700;line-height:46px}.VWh0dG_heroMeta{opacity:.82;white-space:nowrap;align-items:center;gap:6px;font-size:12px;line-height:17px;display:flex}.VWh0dG_heroMeta em{background:#ffffff29;border-radius:6px;margin-left:2px;padding:1px 6px;font-style:normal}.VWh0dG_heroDivider{background:#ffffff38;flex:none;align-self:stretch;width:1px}.VWh0dG_heroSide{flex:none;align-items:center;gap:28px;min-width:140px;display:flex}.VWh0dG_heroSideItem{flex-direction:column;justify-content:center;gap:3px;display:flex}.VWh0dG_heroSideLabel{opacity:.82;white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSideValue{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:22px;font-weight:600;line-height:28px}.VWh0dG_delta{align-items:center;gap:2px;font-size:12px;font-weight:600;line-height:17px;display:inline-flex}.VWh0dG_deltaUp{color:#9ff0b8}.VWh0dG_deltaDown{color:#ffb4b4}.VWh0dG_kpiGrid{grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;display:grid}.VWh0dG_kpiTile{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:14px;flex-direction:column;gap:3px;padding:14px 15px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_kpiTile:hover{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_kpiLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}.VWh0dG_kpiValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:21px;font-weight:600;line-height:27px}.VWh0dG_kpiGreen{color:var(--dsw-static-green-500)}.VWh0dG_kpiDetail{color:var(--dsw-alias-label-caption);white-space:nowrap;text-overflow:ellipsis;font-size:11px;line-height:15px;overflow:hidden}.VWh0dG_panel{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:10px;padding:16px 18px;display:flex}.VWh0dG_panelHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.VWh0dG_panelTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:600;line-height:20px}.VWh0dG_panelHint{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:11px;line-height:16px}.VWh0dG_chartWrap{width:100%;position:relative}.VWh0dG_chartSvg{width:100%;height:auto;display:block}.VWh0dG_chartEmpty{height:140px;color:var(--dsw-alias-label-caption);justify-content:center;align-items:center;font-size:13px;display:flex}.VWh0dG_emptyRow{text-align:center;color:var(--dsw-alias-label-caption);padding:28px 0;font-size:13px}.VWh0dG_chartGrid{stroke:var(--dsw-alias-border-l1);stroke-width:1px}.VWh0dG_chartAxisLabel{fill:var(--dsw-alias-label-tertiary);font-size:10px}.VWh0dG_chartBar{fill:var(--dsw-static-blue-500);opacity:.35}.VWh0dG_chartBar:hover{opacity:.6}.VWh0dG_chartStack{stroke:var(--dsw-alias-bg-layer-1);stroke-width:.75px}.VWh0dG_chartStack:hover{opacity:.85}.VWh0dG_chartLine{stroke:var(--dsw-static-blue-500);stroke-linecap:round;stroke-linejoin:round}.VWh0dG_chartCrosshair{stroke:var(--dsw-alias-label-dimmed);stroke-width:1px;stroke-dasharray:3 3}.VWh0dG_chartDot{fill:var(--dsw-static-neutral-bluish-00);stroke:var(--dsw-static-blue-500);stroke-width:2.5px}.VWh0dG_chartTooltip{pointer-events:none;z-index:2;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2);white-space:nowrap;border-radius:10px;padding:8px 10px;position:absolute;transform:translate(-50%,calc(-100% - 10px))}.VWh0dG_chartTooltipDate{color:var(--dsw-alias-label-primary);margin-bottom:3px;font-size:11px;font-weight:600}.VWh0dG_chartTooltipRow{color:var(--dsw-alias-label-secondary);align-items:center;gap:5px;font-size:11px;line-height:17px;display:flex}.VWh0dG_chartTooltipRow strong{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-weight:600}.VWh0dG_chartLegendLine{background:var(--dsw-static-blue-500);border-radius:2px;width:10px;height:3px;display:inline-block}.VWh0dG_chartLegendBar{background:var(--dsw-static-blue-500);opacity:.5;border-radius:2px;width:7px;height:7px;display:inline-block}.VWh0dG_chartTooltipSwatch{border-radius:3px;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_chartLegend{color:var(--dsw-alias-label-tertiary);justify-content:flex-end;gap:14px;margin-top:2px;font-size:11px;display:flex}.VWh0dG_chartLegend span{align-items:center;gap:5px;display:inline-flex}.VWh0dG_tableScroll{border:1px solid var(--dsw-alias-border-l1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;overflow:auto}.VWh0dG_modelTable,.VWh0dG_pricingTable{border-collapse:collapse;width:100%;font-size:12.5px}.VWh0dG_modelTable th,.VWh0dG_modelTable td,.VWh0dG_pricingTable th,.VWh0dG_pricingTable td{border-bottom:1px solid var(--dsw-alias-border-l1);text-align:left;white-space:nowrap;padding:9px 12px}.VWh0dG_modelTable tbody tr:last-child td,.VWh0dG_pricingTable tbody tr:last-child td{border-bottom:0}.VWh0dG_modelTable thead th,.VWh0dG_pricingTable thead th{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);font-size:11px;font-weight:500}.VWh0dG_modelTable tbody tr,.VWh0dG_pricingTable tbody tr{transition:background-color .12s}.VWh0dG_modelTable tbody tr:hover,.VWh0dG_pricingTable tbody tr:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_numCol{text-align:right;font-variant-numeric:tabular-nums}.VWh0dG_costCol{color:var(--dsw-static-blue-500);font-weight:600}.VWh0dG_na{color:var(--dsw-alias-label-dimmed)}.VWh0dG_modelCell{align-items:center;gap:8px;display:inline-flex}.VWh0dG_modelDot{width:9px;height:9px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-bg-layer-1) 60%, transparent);border-radius:50%;flex:none}.VWh0dG_modelName{color:var(--dsw-alias-label-primary);font-weight:500;line-height:16px;display:block}.VWh0dG_modelProvider{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px;display:block}.VWh0dG_pricingToggle{cursor:pointer;text-align:left;background:0 0;border:none;justify-content:space-between;align-items:center;gap:10px;width:100%;padding:0;display:flex}.VWh0dG_pricingToggle:hover .VWh0dG_panelTitle,.VWh0dG_pricingToggle:focus-visible .VWh0dG_panelTitle{color:var(--dsw-static-blue-500)}.VWh0dG_pricingToggleText{align-items:baseline;gap:10px;min-width:0;display:flex}.VWh0dG_pricingChevron{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.VWh0dG_pricingChevronOpen{transform:rotate(180deg)}.VWh0dG_bandPrice{align-items:baseline;gap:6px;display:inline-flex}.VWh0dG_bandPriceOff{color:var(--dsw-alias-label-caption)}.VWh0dG_bandPriceOff:before{content:\"/\";color:var(--dsw-alias-label-dimmed);margin-right:4px}.VWh0dG_bandTag{color:var(--dsw-alias-label-secondary);flex-direction:column;gap:1px;font-size:10.5px;line-height:14px;display:inline-flex}.VWh0dG_bandTagOff{color:var(--dsw-static-green-500);font-weight:600}.VWh0dG_flatTag{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px}.VWh0dG_healthDot{border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_healthOk{background:var(--dsw-static-green-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-green-500) 18%, transparent)}.VWh0dG_healthBad{background:var(--dsw-static-red-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-red-500) 18%, transparent)}.VWh0dG_healthIdle{background:var(--dsw-static-neutral-bluish-400)}.VWh0dG_dashboardRight{align-items:center;gap:8px;min-width:0;display:flex}.VWh0dG_healthBadge{white-space:nowrap;border-radius:999px;align-items:center;gap:6px;padding:3px 9px;font-size:11px;line-height:16px;display:inline-flex}.VWh0dG_healthBadgeOk{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_healthBadgeBad{color:var(--dsw-static-red-500);background:color-mix(in srgb, var(--dsw-static-red-500) 10%, transparent)}.VWh0dG_planTag{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 12%, transparent);border-radius:999px;align-items:center;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}@media (width<=640px){.VWh0dG_kpiGrid{grid-template-columns:repeat(2,minmax(0,1fr))}.VWh0dG_hero{flex-direction:column;align-items:flex-start;gap:12px}.VWh0dG_heroMain{width:100%;min-width:0}.VWh0dG_heroDivider{align-self:stretch;width:100%;height:1px}.VWh0dG_heroSide{min-width:0}}";
26
+ const css = ".VWh0dG_railButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:36px;height:36px;color:var(--dsw-static-blue-500);cursor:pointer;border-radius:10px;justify-content:center;align-items:center;padding:0;transition:background-color .14s,border-color .14s,color .14s;display:flex}.VWh0dG_railButton:hover,.VWh0dG_railButton:focus-visible{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3);color:var(--dsw-static-blue-400);outline:none}.VWh0dG_railButton svg{stroke-width:2px;width:18px;height:18px}.VWh0dG_trigger{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;border-radius:12px;align-items:center;gap:12px;padding:10px 14px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_trigger:hover,.VWh0dG_trigger:focus-visible{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover);outline:none}.VWh0dG_triggerIcon{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.VWh0dG_triggerIcon svg{stroke-width:1.8px;width:15px;height:15px}.VWh0dG_triggerToday,.VWh0dG_triggerMonth{align-items:baseline;gap:4px;min-width:0;display:flex}.VWh0dG_triggerDivider{background:var(--dsw-alias-border-l1);align-self:stretch;width:1px}.VWh0dG_triggerAmount{color:var(--dsw-alias-label-primary);letter-spacing:-.01em;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:17px;font-weight:600;line-height:22px}.VWh0dG_triggerAmountSub{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:13px;font-weight:600;line-height:18px}.VWh0dG_triggerMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:10.5px;line-height:14px}.VWh0dG_dashboardModal{width:min(760px,100vw - 48px);max-height:min(760px,88vh)}.VWh0dG_dashboard{flex-direction:column;width:100%;max-height:min(760px,88vh);display:flex}.VWh0dG_dashboardHead{border-bottom:1px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:flex-start;gap:12px;padding:20px 24px 14px;display:flex}.VWh0dG_dashboardTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:17px;font-weight:600;line-height:24px}.VWh0dG_dashboardSubtitle{color:var(--dsw-alias-label-caption);margin:3px 0 0;font-size:12px;line-height:17px}.VWh0dG_closeButton{width:30px;height:30px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;transition:background-color .14s;display:inline-flex}.VWh0dG_closeButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_closeButton svg{stroke-width:2px;width:16px;height:16px}.VWh0dG_dashboardBody{flex-direction:column;gap:14px;padding:16px 24px 24px;display:flex;overflow-y:auto}.VWh0dG_hero{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;justify-content:space-between;align-items:center;gap:24px;padding:20px 24px;display:flex}.VWh0dG_heroMain{flex-direction:column;gap:4px;min-width:0;display:flex}.VWh0dG_heroLabel{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroValue{letter-spacing:-.02em;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:40px;font-weight:600;line-height:48px}.VWh0dG_heroMeta{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSide{flex-direction:column;flex:none;gap:10px;display:flex}.VWh0dG_heroSideItem{align-items:baseline;gap:8px;display:flex}.VWh0dG_heroSideLabel{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSideValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:16px;font-weight:600;line-height:22px}.VWh0dG_delta{align-items:center;gap:2px;margin-left:4px;font-size:11px;font-weight:600;line-height:17px;display:inline-flex}.VWh0dG_deltaUp{color:var(--dsw-static-green-500)}.VWh0dG_deltaDown{color:var(--dsw-static-red-500)}.VWh0dG_kpiGrid{grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;display:grid}.VWh0dG_kpiTile{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:14px;flex-direction:column;gap:3px;padding:14px 15px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_kpiTile:hover{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_kpiLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}.VWh0dG_kpiValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:21px;font-weight:600;line-height:27px}.VWh0dG_kpiGreen{color:var(--dsw-static-green-500)}.VWh0dG_kpiDetail{color:var(--dsw-alias-label-caption);white-space:nowrap;text-overflow:ellipsis;font-size:11px;line-height:15px;overflow:hidden}.VWh0dG_panel{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:10px;padding:16px 18px;display:flex}.VWh0dG_panelHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.VWh0dG_panelTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:600;line-height:20px}.VWh0dG_panelHint{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:11px;line-height:16px}.VWh0dG_chartWrap{width:100%;position:relative}.VWh0dG_chartSvg{width:100%;height:auto;display:block}.VWh0dG_chartEmpty{height:140px;color:var(--dsw-alias-label-caption);justify-content:center;align-items:center;font-size:13px;display:flex}.VWh0dG_emptyRow{text-align:center;color:var(--dsw-alias-label-caption);padding:28px 0;font-size:13px}.VWh0dG_chartGrid{stroke:var(--dsw-alias-border-l1);stroke-width:1px}.VWh0dG_chartAxisLabel{fill:var(--dsw-alias-label-tertiary);font-size:10px}.VWh0dG_chartBar{fill:var(--dsw-static-blue-500);opacity:.35}.VWh0dG_chartBar:hover{opacity:.6}.VWh0dG_chartStack{stroke:var(--dsw-alias-bg-layer-1);stroke-width:.75px}.VWh0dG_chartStack:hover{opacity:.85}.VWh0dG_chartLine{stroke:var(--dsw-static-blue-500);stroke-width:2px;stroke-linecap:round;stroke-linejoin:round}.VWh0dG_chartCrosshair{stroke:var(--dsw-alias-label-dimmed);stroke-width:1px;stroke-dasharray:3 3}.VWh0dG_chartDot{fill:var(--dsw-static-neutral-bluish-00);stroke:var(--dsw-static-blue-500);stroke-width:2.5px}.VWh0dG_chartTooltip{pointer-events:none;z-index:2;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2);white-space:nowrap;border-radius:10px;padding:8px 10px;position:absolute;transform:translate(-50%,calc(-100% - 10px))}.VWh0dG_chartTooltipDate{color:var(--dsw-alias-label-primary);margin-bottom:3px;font-size:11px;font-weight:600}.VWh0dG_chartTooltipRow{color:var(--dsw-alias-label-secondary);align-items:center;gap:5px;font-size:11px;line-height:17px;display:flex}.VWh0dG_chartTooltipRow strong{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-weight:600}.VWh0dG_chartLegendLine{background:var(--dsw-static-blue-500);border-radius:2px;width:10px;height:3px;display:inline-block}.VWh0dG_chartLegendBar{background:var(--dsw-static-blue-500);opacity:.5;border-radius:2px;width:7px;height:7px;display:inline-block}.VWh0dG_chartTooltipSwatch{border-radius:3px;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_chartLegend{color:var(--dsw-alias-label-tertiary);justify-content:flex-end;gap:14px;margin-top:2px;font-size:11px;display:flex}.VWh0dG_chartLegend span{align-items:center;gap:5px;display:inline-flex}.VWh0dG_tableScroll{border:1px solid var(--dsw-alias-border-l1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;overflow:auto}.VWh0dG_modelTable,.VWh0dG_pricingTable{border-collapse:collapse;width:100%;font-size:12.5px}.VWh0dG_modelTable th,.VWh0dG_modelTable td,.VWh0dG_pricingTable th,.VWh0dG_pricingTable td{border-bottom:1px solid var(--dsw-alias-border-l1);text-align:left;white-space:nowrap;padding:9px 12px}.VWh0dG_modelTable tbody tr:last-child td,.VWh0dG_pricingTable tbody tr:last-child td{border-bottom:0}.VWh0dG_modelTable thead th,.VWh0dG_pricingTable thead th{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);font-size:11px;font-weight:500}.VWh0dG_modelTable tbody tr,.VWh0dG_pricingTable tbody tr{transition:background-color .12s}.VWh0dG_modelTable tbody tr:hover,.VWh0dG_pricingTable tbody tr:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_numCol{text-align:right;font-variant-numeric:tabular-nums}.VWh0dG_costCol{color:var(--dsw-static-blue-500);font-weight:600}.VWh0dG_na{color:var(--dsw-alias-label-dimmed)}.VWh0dG_modelCell{align-items:center;gap:8px;display:inline-flex}.VWh0dG_modelDot{width:9px;height:9px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-bg-layer-1) 60%, transparent);border-radius:50%;flex:none}.VWh0dG_modelName{color:var(--dsw-alias-label-primary);font-weight:500;line-height:16px;display:block}.VWh0dG_modelProvider{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px;display:block}.VWh0dG_pricingToggle{cursor:pointer;text-align:left;background:0 0;border:none;justify-content:space-between;align-items:center;gap:10px;width:100%;padding:0;display:flex}.VWh0dG_pricingToggle:hover .VWh0dG_panelTitle,.VWh0dG_pricingToggle:focus-visible .VWh0dG_panelTitle{color:var(--dsw-static-blue-500)}.VWh0dG_rateBadge{vertical-align:1px;white-space:nowrap;border-radius:999px;align-items:center;margin-left:6px;padding:1px 6px;font-size:10px;font-weight:500;line-height:14px;display:inline-flex}.VWh0dG_rateBadgeLive{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_rateBadgeBuiltin{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform)}.VWh0dG_pricingToggleText{align-items:baseline;gap:10px;min-width:0;display:flex}.VWh0dG_pricingChevron{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.VWh0dG_pricingChevronOpen{transform:rotate(180deg)}.VWh0dG_bandPrice{align-items:baseline;gap:6px;display:inline-flex}.VWh0dG_bandPriceOff{color:var(--dsw-alias-label-caption)}.VWh0dG_bandPriceOff:before{content:\"/\";color:var(--dsw-alias-label-dimmed);margin-right:4px}.VWh0dG_bandTag{color:var(--dsw-alias-label-secondary);flex-direction:column;gap:1px;font-size:10.5px;line-height:14px;display:inline-flex}.VWh0dG_bandTagOff{color:var(--dsw-static-green-500);font-weight:600}.VWh0dG_flatTag{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px}.VWh0dG_healthDot{border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_healthOk{background:var(--dsw-static-green-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-green-500) 18%, transparent)}.VWh0dG_healthBad{background:var(--dsw-static-red-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-red-500) 18%, transparent)}.VWh0dG_healthIdle{background:var(--dsw-static-neutral-bluish-400)}.VWh0dG_dashboardRight{align-items:center;gap:8px;min-width:0;display:flex}.VWh0dG_healthBadge{white-space:nowrap;border-radius:999px;align-items:center;gap:6px;padding:3px 9px;font-size:11px;line-height:16px;display:inline-flex}.VWh0dG_healthBadgeOk{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_healthBadgeBad{color:var(--dsw-static-red-500);background:color-mix(in srgb, var(--dsw-static-red-500) 10%, transparent)}.VWh0dG_planTag{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 12%, transparent);border-radius:999px;align-items:center;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}@media (width<=640px){.VWh0dG_kpiGrid{grid-template-columns:repeat(2,minmax(0,1fr))}.VWh0dG_hero{flex-direction:column;align-items:flex-start;gap:14px}.VWh0dG_heroSide{flex-direction:row;gap:20px}}";
27
27
  const tagId = "@kenz1117/dsh-ui-usage-billing/UsageBilling.module.css";
28
28
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
29
29
  const tag = document.createElement("style");
@@ -33,112 +33,120 @@ window.__ModuleLoader__.load({
33
33
  document.head.appendChild(tag);
34
34
  }
35
35
  var UsageBilling_module_css_default = {
36
- "bandPrice": "VWh0dG_bandPrice",
37
- "heroSideItem": "VWh0dG_heroSideItem",
38
- "chartWrap": "VWh0dG_chartWrap",
39
- "bandPriceOff": "VWh0dG_bandPriceOff",
40
- "pricingToggle": "VWh0dG_pricingToggle",
41
- "chartCrosshair": "VWh0dG_chartCrosshair",
42
- "healthBadgeBad": "VWh0dG_healthBadgeBad",
43
- "hero": "VWh0dG_hero",
36
+ "costCol": "VWh0dG_costCol",
37
+ "rateBadge": "VWh0dG_rateBadge",
38
+ "pricingToggleText": "VWh0dG_pricingToggleText",
39
+ "bandTagOff": "VWh0dG_bandTagOff",
40
+ "healthDot": "VWh0dG_healthDot",
44
41
  "kpiTile": "VWh0dG_kpiTile",
45
- "kpiDetail": "VWh0dG_kpiDetail",
46
- "triggerIcon": "VWh0dG_triggerIcon",
47
- "chartDot": "VWh0dG_chartDot",
48
- "numCol": "VWh0dG_numCol",
49
- "railButton": "VWh0dG_railButton",
50
- "deltaDown": "VWh0dG_deltaDown",
51
- "kpiValue": "VWh0dG_kpiValue",
52
- "kpiLabel": "VWh0dG_kpiLabel",
53
- "chartLine": "VWh0dG_chartLine",
54
- "pricingChevronOpen": "VWh0dG_pricingChevronOpen",
55
- "heroLabel": "VWh0dG_heroLabel",
42
+ "deltaUp": "VWh0dG_deltaUp",
56
43
  "delta": "VWh0dG_delta",
57
- "heroValue": "VWh0dG_heroValue",
58
- "chartAxisLabel": "VWh0dG_chartAxisLabel",
59
- "heroMain": "VWh0dG_heroMain",
60
- "modelTable": "VWh0dG_modelTable",
44
+ "panelHead": "VWh0dG_panelHead",
61
45
  "panelTitle": "VWh0dG_panelTitle",
62
- "na": "VWh0dG_na",
63
- "modelProvider": "VWh0dG_modelProvider",
64
- "triggerText": "VWh0dG_triggerText",
65
- "modelCell": "VWh0dG_modelCell",
46
+ "chartBar": "VWh0dG_chartBar",
47
+ "hero": "VWh0dG_hero",
48
+ "emptyRow": "VWh0dG_emptyRow",
49
+ "dashboardModal": "VWh0dG_dashboardModal",
66
50
  "chartLegendBar": "VWh0dG_chartLegendBar",
67
- "modelDot": "VWh0dG_modelDot",
51
+ "tableScroll": "VWh0dG_tableScroll",
52
+ "heroMain": "VWh0dG_heroMain",
53
+ "heroSide": "VWh0dG_heroSide",
54
+ "pricingTable": "VWh0dG_pricingTable",
55
+ "kpiDetail": "VWh0dG_kpiDetail",
68
56
  "healthBadgeOk": "VWh0dG_healthBadgeOk",
69
- "triggerLabel": "VWh0dG_triggerLabel",
70
- "chartEmpty": "VWh0dG_chartEmpty",
71
- "chartBar": "VWh0dG_chartBar",
72
- "heroDivider": "VWh0dG_heroDivider",
73
- "triggerChevron": "VWh0dG_triggerChevron",
57
+ "triggerDivider": "VWh0dG_triggerDivider",
58
+ "heroLabel": "VWh0dG_heroLabel",
59
+ "pricingToggle": "VWh0dG_pricingToggle",
74
60
  "dashboardRight": "VWh0dG_dashboardRight",
75
- "dashboardTitle": "VWh0dG_dashboardTitle",
76
- "panelHint": "VWh0dG_panelHint",
77
- "panelHead": "VWh0dG_panelHead",
78
- "tableScroll": "VWh0dG_tableScroll",
79
- "heroSideLabel": "VWh0dG_heroSideLabel",
61
+ "railButton": "VWh0dG_railButton",
62
+ "kpiLabel": "VWh0dG_kpiLabel",
63
+ "healthBadge": "VWh0dG_healthBadge",
64
+ "triggerMonth": "VWh0dG_triggerMonth",
65
+ "triggerToday": "VWh0dG_triggerToday",
66
+ "rateBadgeBuiltin": "VWh0dG_rateBadgeBuiltin",
67
+ "dashboardSubtitle": "VWh0dG_dashboardSubtitle",
68
+ "flatTag": "VWh0dG_flatTag",
69
+ "dashboard": "VWh0dG_dashboard",
70
+ "chartWrap": "VWh0dG_chartWrap",
80
71
  "panel": "VWh0dG_panel",
72
+ "chartLegendLine": "VWh0dG_chartLegendLine",
73
+ "deltaDown": "VWh0dG_deltaDown",
74
+ "kpiGrid": "VWh0dG_kpiGrid",
75
+ "chartSvg": "VWh0dG_chartSvg",
81
76
  "closeButton": "VWh0dG_closeButton",
82
- "triggerMeta": "VWh0dG_triggerMeta",
77
+ "chartLegend": "VWh0dG_chartLegend",
83
78
  "modelName": "VWh0dG_modelName",
84
79
  "pricingChevron": "VWh0dG_pricingChevron",
85
- "deltaUp": "VWh0dG_deltaUp",
86
- "emptyRow": "VWh0dG_emptyRow",
87
- "healthOk": "VWh0dG_healthOk",
80
+ "na": "VWh0dG_na",
81
+ "healthBadgeBad": "VWh0dG_healthBadgeBad",
88
82
  "chartStack": "VWh0dG_chartStack",
89
- "dashboardBody": "VWh0dG_dashboardBody",
90
- "dashboardHead": "VWh0dG_dashboardHead",
91
- "dashboardModal": "VWh0dG_dashboardModal",
92
- "kpiGrid": "VWh0dG_kpiGrid",
93
- "kpiGreen": "VWh0dG_kpiGreen",
94
- "heroSide": "VWh0dG_heroSide",
95
- "bandTagOff": "VWh0dG_bandTagOff",
96
- "planTag": "VWh0dG_planTag",
97
- "pricingToggleText": "VWh0dG_pricingToggleText",
83
+ "chartTooltipRow": "VWh0dG_chartTooltipRow",
84
+ "modelTable": "VWh0dG_modelTable",
85
+ "chartCrosshair": "VWh0dG_chartCrosshair",
86
+ "bandTag": "VWh0dG_bandTag",
87
+ "heroValue": "VWh0dG_heroValue",
98
88
  "heroMeta": "VWh0dG_heroMeta",
99
- "chartGrid": "VWh0dG_chartGrid",
100
- "healthDot": "VWh0dG_healthDot",
101
- "chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
102
- "dashboardSubtitle": "VWh0dG_dashboardSubtitle",
103
- "pricingTable": "VWh0dG_pricingTable",
104
- "healthIdle": "VWh0dG_healthIdle",
105
- "costCol": "VWh0dG_costCol",
89
+ "healthOk": "VWh0dG_healthOk",
90
+ "kpiValue": "VWh0dG_kpiValue",
91
+ "planTag": "VWh0dG_planTag",
92
+ "heroSideLabel": "VWh0dG_heroSideLabel",
93
+ "chartDot": "VWh0dG_chartDot",
106
94
  "healthBad": "VWh0dG_healthBad",
107
- "bandTag": "VWh0dG_bandTag",
108
- "chartTooltipRow": "VWh0dG_chartTooltipRow",
109
- "chartLegend": "VWh0dG_chartLegend",
110
- "chartSvg": "VWh0dG_chartSvg",
111
- "dashboard": "VWh0dG_dashboard",
95
+ "kpiGreen": "VWh0dG_kpiGreen",
96
+ "heroSideItem": "VWh0dG_heroSideItem",
97
+ "pricingChevronOpen": "VWh0dG_pricingChevronOpen",
98
+ "dashboardBody": "VWh0dG_dashboardBody",
99
+ "panelHint": "VWh0dG_panelHint",
112
100
  "chartTooltipDate": "VWh0dG_chartTooltipDate",
113
- "chartTooltip": "VWh0dG_chartTooltip",
114
- "chartLegendLine": "VWh0dG_chartLegendLine",
115
- "healthBadge": "VWh0dG_healthBadge",
116
101
  "heroSideValue": "VWh0dG_heroSideValue",
102
+ "triggerAmount": "VWh0dG_triggerAmount",
117
103
  "trigger": "VWh0dG_trigger",
118
- "flatTag": "VWh0dG_flatTag"
104
+ "chartAxisLabel": "VWh0dG_chartAxisLabel",
105
+ "modelProvider": "VWh0dG_modelProvider",
106
+ "chartGrid": "VWh0dG_chartGrid",
107
+ "healthIdle": "VWh0dG_healthIdle",
108
+ "chartLine": "VWh0dG_chartLine",
109
+ "numCol": "VWh0dG_numCol",
110
+ "rateBadgeLive": "VWh0dG_rateBadgeLive",
111
+ "triggerAmountSub": "VWh0dG_triggerAmountSub",
112
+ "bandPrice": "VWh0dG_bandPrice",
113
+ "dashboardTitle": "VWh0dG_dashboardTitle",
114
+ "triggerIcon": "VWh0dG_triggerIcon",
115
+ "dashboardHead": "VWh0dG_dashboardHead",
116
+ "bandPriceOff": "VWh0dG_bandPriceOff",
117
+ "chartTooltip": "VWh0dG_chartTooltip",
118
+ "chartEmpty": "VWh0dG_chartEmpty",
119
+ "modelCell": "VWh0dG_modelCell",
120
+ "modelDot": "VWh0dG_modelDot",
121
+ "chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
122
+ "triggerMeta": "VWh0dG_triggerMeta"
119
123
  };
120
- //#endregion
121
- //#region src/client/pricing.ts
124
+ /** 运行时实时覆盖:undefined = 用内置目录与内置汇率(默认值降级)。 */
125
+ let liveRate;
126
+ let livePrices;
122
127
  /**
123
- * Billing engine: per-model price tables and token-usage cost estimation.
124
- *
125
- * Each model's price table uses its NATIVE currency: domestic providers
126
- * (DeepSeek, 智谱, 通义…) publish RMB prices and store them directly;
127
- * overseas providers (OpenAI, Google, xAI, Meta) publish USD.
128
- * Cost is always computed and displayed in CNY — only USD-priced models go
129
- * through the exchange rate, never domestic ones.
130
- *
131
- * Google-style two-band billing is modeled per model: Gemini's Flex tier
132
- * prices spare-capacity traffic at -50%; DeepSeek V4 splits peak
133
- * (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate. The estimator
134
- * mixes both bands by a configured peak share ({@link DEFAULT_PEAK_SHARE}).
128
+ * Apply the node half's live pricing snapshot. Absent fields keep the
129
+ * built-in catalog and rate; callers never fabricate values.
130
+ * @param pricing - the `/api/billing/pricing` response.
135
131
  */
132
+ function applyLivePricing(pricing) {
133
+ liveRate = pricing.rate;
134
+ livePrices = pricing.prices;
135
+ }
136
+ /** 当前汇率:实时覆盖优先,缺省回退内置固定值。 */
137
+ function currentRate() {
138
+ return liveRate ?? 6.79;
139
+ }
136
140
  /**
137
- * USD → CNY rate for display. Source: China Foreign Exchange Trade System
138
- * mid-rate 6.7878 on 2026-08-14; rounded to 6.79. Only applies to overseas
139
- * USD-priced models — domestic models never pass through this rate.
141
+ * 当前生效的 USD → CNY 汇率及其来源:live = 启动时实时拉取成功,
142
+ * builtin = 实时拉取失败、正在用内置默认值。
140
143
  */
141
- const USD_TO_CNY = 6.79;
144
+ function getRateInfo() {
145
+ return {
146
+ rate: currentRate(),
147
+ live: liveRate !== void 0
148
+ };
149
+ }
142
150
  /** Default share of traffic assumed to fall in the peak band (0..1). */
143
151
  const DEFAULT_PEAK_SHARE = .5;
144
152
  /**
@@ -617,11 +625,22 @@ window.__ModuleLoader__.load({
617
625
  ];
618
626
  /** Lookup a model by its stats key; falls back to the generic `other` entry. */
619
627
  function modelOf(key) {
620
- const found = MODEL_CATALOG.find((entry) => entry.key === key);
621
- if (found !== void 0) return found;
622
- const fallback = MODEL_CATALOG.at(-1);
623
- if (fallback !== void 0) return fallback;
624
- throw new Error("MODEL_CATALOG must not be empty");
628
+ const base = MODEL_CATALOG.find((entry) => entry.key === key) ?? (() => {
629
+ const fallback = MODEL_CATALOG.at(-1);
630
+ if (fallback !== void 0) return fallback;
631
+ throw new Error("MODEL_CATALOG must not be empty");
632
+ })();
633
+ const live = livePrices?.[key];
634
+ if (live === void 0) return base;
635
+ return {
636
+ ...base,
637
+ price: {
638
+ currency: "USD",
639
+ input: live.input,
640
+ cacheHit: live.cacheHit,
641
+ output: live.output
642
+ }
643
+ };
625
644
  }
626
645
  /** Resolve a price-table row by its CSS variable name (theme token or fallback color). */
627
646
  function resolveToken(name) {
@@ -640,7 +659,7 @@ window.__ModuleLoader__.load({
640
659
  const miss = buckets.cacheMiss > 0 ? buckets.cacheMiss : Math.max(0, buckets.input - buckets.cacheHit);
641
660
  const hit = Math.min(buckets.cacheHit, buckets.input);
642
661
  const raw = (miss * (band.cacheMiss ?? band.input) + hit * band.cacheHit + buckets.output * band.output) / 1e6;
643
- return currency === "USD" ? raw * USD_TO_CNY : raw;
662
+ return currency === "USD" ? raw * currentRate() : raw;
644
663
  }
645
664
  /**
646
665
  * Estimate the CNY cost of one model's token usage, mixing the peak and
@@ -659,10 +678,12 @@ window.__ModuleLoader__.load({
659
678
  }
660
679
  /** Format a CNY amount with adaptive precision. */
661
680
  function formatMoney(cny) {
662
- if (cny >= 1e3) return `¥${cny.toFixed(0)}`;
663
- if (cny >= 10) return `¥${cny.toFixed(1)}`;
664
- if (cny >= .1) return `¥${cny.toFixed(2)}`;
665
- return `¥${cny.toFixed(3)}`;
681
+ const value = Number(cny);
682
+ if (!Number.isFinite(value)) return "¥0";
683
+ if (value >= 1e3) return `¥${value.toFixed(0)}`;
684
+ if (value >= 10) return `¥${value.toFixed(1)}`;
685
+ if (value >= .1) return `¥${value.toFixed(2)}`;
686
+ return `¥${value.toFixed(3)}`;
666
687
  }
667
688
  /**
668
689
  * Format a per-1M-token price in its native currency (free when the rate is
@@ -686,23 +707,27 @@ window.__ModuleLoader__.load({
686
707
  }
687
708
  /** Format a percentage. */
688
709
  function formatPercent(value) {
689
- return `${value.toFixed(1)}%`;
710
+ const normalized = Number(value);
711
+ if (!Number.isFinite(normalized)) return "0.0%";
712
+ return `${normalized.toFixed(1)}%`;
690
713
  }
691
714
  //#endregion
692
715
  //#region src/client/TrendChart.tsx
693
716
  /**
694
- * TrendChart: dependency-free SVG stacked bar chart of daily cost per model.
695
- * Each day's column stacks every model's share in its brand color, so the
696
- * total trend and the per-model composition are visible at once. A hover
697
- * crosshair shows the day's model breakdown. No chart library the surface
698
- * stays self-contained and offline.
717
+ * TrendChart: dependency-free SVG chart of daily cost + calls.
718
+ *
719
+ * The columns are GROUPED per model one bar per model per day, each in its
720
+ * brand color, so per-model cost is directly comparable. The blue line is the
721
+ * total call volume across all models, plotted on its own right-hand axis.
722
+ * A hover crosshair shows the day's model breakdown. No chart library — the
723
+ * surface stays self-contained and offline.
699
724
  */
700
725
  /** Fixed viewBox; the SVG scales to its container. */
701
726
  const W = 680;
702
727
  const H = 220;
703
728
  const PAD = {
704
729
  top: 18,
705
- right: 16,
730
+ right: 40,
706
731
  bottom: 26,
707
732
  left: 46
708
733
  };
@@ -711,6 +736,12 @@ window.__ModuleLoader__.load({
711
736
  const [, month, day] = iso.split("-");
712
737
  return `${Number(month)}/${Number(day)}`;
713
738
  }
739
+ /** Compact tick label for the calls axis: `1.2K` / `3.4M`. */
740
+ function shortNumber(value) {
741
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
742
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
743
+ return String(Math.round(value));
744
+ }
714
745
  /** Ticks every `step` items for sparse axis labels. */
715
746
  function tickIndexes(length, step) {
716
747
  const out = [];
@@ -718,52 +749,56 @@ window.__ModuleLoader__.load({
718
749
  if (length > 0 && out[out.length - 1] !== length - 1) out.push(length - 1);
719
750
  return out;
720
751
  }
752
+ /** Single-color fallback identity used when the stats carry no per-model detail. */
753
+ const TOTAL_MODEL = {
754
+ key: "__total__",
755
+ name: "总计",
756
+ color: ""
757
+ };
721
758
  /**
722
- * Render the daily per-model stacked cost chart.
759
+ * Render the daily grouped cost bars plus the total-calls line.
723
760
  * @param props.data - sorted daily rows (ascending date).
724
- * @param props.models - the model legend, in stack order (bottom first).
761
+ * @param props.models - the model legend, in bar order.
725
762
  */
726
763
  function TrendChart({ data, models = [] }) {
727
764
  const [hover, setHover] = (0, react.useState)(null);
728
765
  const layout = (0, react.useMemo)(() => {
729
766
  const n = data.length;
730
767
  if (n === 0) return null;
731
- const maxCost = Math.max(...data.map((d) => d.cost), 1e-4);
732
768
  const plotW = W - PAD.left - PAD.right;
733
769
  const plotH = H - PAD.top - PAD.bottom;
734
770
  const inner = (i) => {
735
771
  if (n === 1) return PAD.left + plotW / 2;
736
772
  return PAD.left + plotW * i / (n - 1);
737
773
  };
774
+ const maxCost = Math.max(...data.flatMap((d) => Object.values(d.byModel ?? {})), ...data.map((d) => d.cost), 1e-4);
738
775
  const yCost = (value) => PAD.top + plotH - value / maxCost * plotH;
776
+ const maxCalls = Math.max(...data.map((d) => d.calls), 1);
777
+ const yCalls = (value) => PAD.top + plotH - value / maxCalls * plotH;
778
+ const groupW = plotW / n;
779
+ const barCount = Math.max(models.length, 1);
780
+ const barW = Math.min(14, groupW / barCount * .7);
739
781
  return {
740
782
  n,
741
783
  plotW,
742
784
  plotH,
743
785
  inner,
744
786
  yCost,
745
- barW: Math.min(18, plotW / n * .5),
746
- columns: data.map((d, i) => {
747
- let acc = 0;
748
- const segments = models.map((model) => {
749
- const value = d.byModel?.[model.key] ?? 0;
750
- const y0 = acc;
751
- acc += value;
752
- return {
753
- model,
754
- y0,
755
- y1: acc,
756
- rounded: false
757
- };
758
- });
759
- const top = segments.at(-1);
760
- if (top !== void 0) top.rounded = true;
761
- return {
787
+ yCalls,
788
+ barW,
789
+ bars: data.flatMap((d, i) => {
790
+ if (models.length === 0) return [{
762
791
  date: d.date,
763
- x: inner(i),
764
- segments,
765
- total: acc
766
- };
792
+ model: TOTAL_MODEL,
793
+ x: inner(i) - barW / 2,
794
+ value: d.cost
795
+ }];
796
+ return models.map((model, k) => ({
797
+ date: d.date,
798
+ model,
799
+ x: inner(i) - groupW / 2 + groupW * (k + .5) / models.length - barW / 2,
800
+ value: d.byModel?.[model.key] ?? 0
801
+ }));
767
802
  }),
768
803
  costTicks: [
769
804
  0,
@@ -772,15 +807,24 @@ window.__ModuleLoader__.load({
772
807
  .75,
773
808
  1
774
809
  ].map((f) => maxCost * f).reverse(),
775
- maxCost
810
+ callsTicks: [
811
+ 0,
812
+ .25,
813
+ .5,
814
+ .75,
815
+ 1
816
+ ].map((f) => maxCalls * f).reverse(),
817
+ linePath: data.map((d, i) => {
818
+ const y = yCalls(d.calls);
819
+ return `${i === 0 ? "M" : "L"}${inner(i)} ${y}`;
820
+ }).join(" ")
776
821
  };
777
822
  }, [data, models]);
778
823
  if (layout === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
779
824
  className: UsageBilling_module_css_default.chartEmpty,
780
825
  children: "暂无趋势数据"
781
826
  });
782
- const { n, plotW, inner, yCost, barW, columns, costTicks } = layout;
783
- const activeColumn = hover === null ? void 0 : columns[hover];
827
+ const { n, plotW, plotH, inner, yCost, yCalls, barW, bars, costTicks, callsTicks, linePath } = layout;
784
828
  const activePoint = hover === null ? void 0 : data[hover];
785
829
  const indices = tickIndexes(n, Math.max(1, Math.ceil(n / 8)));
786
830
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -790,7 +834,7 @@ window.__ModuleLoader__.load({
790
834
  viewBox: `0 0 ${W} ${H}`,
791
835
  className: UsageBilling_module_css_default.chartSvg,
792
836
  role: "img",
793
- "aria-label": "Daily cost by model",
837
+ "aria-label": "Daily cost by model and total calls",
794
838
  onMouseLeave: () => {
795
839
  setHover(null);
796
840
  },
@@ -817,15 +861,30 @@ window.__ModuleLoader__.load({
817
861
  children: formatMoney(value)
818
862
  })] }, `cost-${idx}`);
819
863
  }),
820
- columns.map((column) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", { children: column.segments.map((segment) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
821
- x: column.x - barW / 2,
822
- y: yCost(segment.y1),
864
+ bars.map((bar) => bar.value > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
865
+ x: bar.x,
866
+ y: yCost(bar.value),
823
867
  width: barW,
824
- height: Math.max(segment.y1 - segment.y0 > 0 ? 1 : 0, yCost(segment.y0) - yCost(segment.y1)),
825
- rx: segment.rounded ? 3 : 0,
826
- className: UsageBilling_module_css_default.chartStack,
827
- style: { fill: segment.model.color }
828
- }, segment.model.key)) }, column.date)),
868
+ height: yCost(0) - yCost(bar.value),
869
+ rx: 2,
870
+ className: bar.model.color === "" ? UsageBilling_module_css_default.chartBar : UsageBilling_module_css_default.chartStack,
871
+ style: bar.model.color === "" ? void 0 : { fill: bar.model.color }
872
+ }, `${bar.date}-${bar.model.key}`) : null),
873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
874
+ d: linePath,
875
+ fill: "none",
876
+ className: UsageBilling_module_css_default.chartLine
877
+ }),
878
+ callsTicks.map((value, idx) => {
879
+ const y = yCalls(value);
880
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
881
+ x: W - PAD.right + 8,
882
+ y: y + 3,
883
+ textAnchor: "start",
884
+ className: UsageBilling_module_css_default.chartAxisLabel,
885
+ children: shortNumber(value)
886
+ }, `calls-${idx}`);
887
+ }),
829
888
  indices.map((i) => {
830
889
  const point = data[i];
831
890
  if (point === void 0) return null;
@@ -837,44 +896,44 @@ window.__ModuleLoader__.load({
837
896
  children: shortDate(point.date)
838
897
  }, point.date);
839
898
  }),
840
- activeColumn !== void 0 && hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
899
+ hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
841
900
  x1: inner(hover),
842
901
  x2: inner(hover),
843
902
  y1: PAD.top,
844
- y2: PAD.top + layout.plotH,
903
+ y2: PAD.top + plotH,
845
904
  className: UsageBilling_module_css_default.chartCrosshair
846
905
  })
847
906
  ]
848
907
  }),
849
- activeColumn !== void 0 && activePoint !== void 0 && hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
908
+ activePoint !== void 0 && hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
850
909
  className: UsageBilling_module_css_default.chartTooltip,
851
910
  style: {
852
911
  left: `${inner(hover) / W * 100}%`,
853
- top: `${yCost(activeColumn.total) / H * 100}%`
912
+ top: `${yCost(activePoint.cost) / H * 100}%`
854
913
  },
855
914
  children: [
856
915
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
857
916
  className: UsageBilling_module_css_default.chartTooltipDate,
858
917
  children: activePoint.date
859
918
  }),
860
- activeColumn.segments.filter((segment) => segment.y1 - segment.y0 > 0).map((segment) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
919
+ models.filter((model) => (activePoint.byModel?.[model.key] ?? 0) > 0).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
861
920
  className: UsageBilling_module_css_default.chartTooltipRow,
862
921
  children: [
863
922
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
864
923
  className: UsageBilling_module_css_default.chartTooltipSwatch,
865
- style: { background: segment.model.color }
924
+ style: { background: model.color }
866
925
  }),
867
- segment.model.name,
926
+ model.name,
868
927
  " ",
869
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(segment.y1 - segment.y0) })
928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(activePoint.byModel?.[model.key] ?? 0) })
870
929
  ]
871
- }, segment.model.key)),
930
+ }, model.key)),
872
931
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
873
932
  className: UsageBilling_module_css_default.chartTooltipRow,
874
933
  children: [
875
934
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendBar }),
876
935
  "总计 ",
877
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(activeColumn.total) })
936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(activePoint.cost) })
878
937
  ]
879
938
  }),
880
939
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -887,12 +946,12 @@ window.__ModuleLoader__.load({
887
946
  })
888
947
  ]
889
948
  }),
890
- models.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
949
+ models.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
891
950
  className: UsageBilling_module_css_default.chartLegend,
892
- children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
951
+ children: [models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
893
952
  className: UsageBilling_module_css_default.chartTooltipSwatch,
894
953
  style: { background: model.color }
895
- }), model.name] }, model.key))
954
+ }), model.name] }, model.key)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendLine }), "调用"] })]
896
955
  })
897
956
  ]
898
957
  });
@@ -1001,6 +1060,8 @@ window.__ModuleLoader__.load({
1001
1060
  }
1002
1061
  /** Path to the usage-stats endpoint served by this plugin's node half. */
1003
1062
  const USAGE_STATS_PATH = "/api/billing/usage-stats";
1063
+ /** Path to the live-pricing endpoint served by this plugin's node half. */
1064
+ const PRICING_PATH = "/api/billing/pricing";
1004
1065
  /** Empty snapshot: shown before (or without) real host data — zeros, never fabricated samples. */
1005
1066
  const EMPTY_STATS = {
1006
1067
  total: {
@@ -1022,67 +1083,110 @@ window.__ModuleLoader__.load({
1022
1083
  if (!response.ok) return null;
1023
1084
  const text = await response.text();
1024
1085
  const parsed = JSON.parse(text);
1025
- if (parsed !== null && typeof parsed === "object" && "total" in parsed) return parsed;
1026
- return null;
1086
+ if (parsed === null || typeof parsed !== "object" || !("total" in parsed)) return null;
1087
+ const candidate = parsed;
1088
+ return {
1089
+ total: candidate.total ?? EMPTY_STATS.total,
1090
+ byModel: candidate.byModel ?? {},
1091
+ byDay: candidate.byDay ?? {},
1092
+ ...candidate.byDayModels !== void 0 ? { byDayModels: candidate.byDayModels } : {}
1093
+ };
1027
1094
  } catch {
1028
1095
  return null;
1029
1096
  }
1030
1097
  }
1031
1098
  /**
1099
+ * Apply the node half's live pricing snapshot. The node half refreshes once
1100
+ * at boot, so an early `builtin` answer may just mean the refresh is still
1101
+ * in flight — retry briefly before settling for the built-in values.
1102
+ * Any final failure keeps the built-in catalog and rate — degrade, never
1103
+ * fabricate.
1104
+ * @param attempt - current retry index (0-based).
1105
+ */
1106
+ async function loadLivePricing(attempt = 0) {
1107
+ const MAX_ATTEMPTS = 4;
1108
+ try {
1109
+ const response = await fetch(PRICING_PATH);
1110
+ if (!response.ok) return;
1111
+ const text = await response.text();
1112
+ const parsed = JSON.parse(text);
1113
+ if (parsed === null || typeof parsed !== "object" || !("source" in parsed)) return;
1114
+ const pricing = parsed;
1115
+ if (pricing.source === "builtin" && attempt < MAX_ATTEMPTS - 1) {
1116
+ setTimeout(() => {
1117
+ loadLivePricing(attempt + 1);
1118
+ }, 2e3);
1119
+ return;
1120
+ }
1121
+ applyLivePricing(pricing);
1122
+ } catch {}
1123
+ }
1124
+ /**
1032
1125
  * Sidebar footer trigger: compact pill in wide mode, icon in rail mode.
1033
1126
  * @param props - framework props plus `wide` column state.
1034
1127
  */
1035
1128
  function UsageBillingTrigger(props) {
1036
- const { wide, onOpen, monthCost, todayCost } = props;
1129
+ const { wide, t, onOpen, monthCost, todayCost } = props;
1130
+ const cardIcon = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1131
+ viewBox: "0 0 24 24",
1132
+ fill: "none",
1133
+ stroke: "currentColor",
1134
+ "aria-hidden": "true",
1135
+ children: [
1136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1137
+ x: "2.5",
1138
+ y: "5",
1139
+ width: "19",
1140
+ height: "14",
1141
+ rx: "2.5"
1142
+ }),
1143
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 9.5h19" }),
1144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1145
+ x: "6",
1146
+ y: "12",
1147
+ width: "4",
1148
+ height: "3.5",
1149
+ rx: "0.75"
1150
+ })
1151
+ ]
1152
+ });
1037
1153
  if (!wide) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1038
1154
  type: "button",
1039
1155
  className: UsageBilling_module_css_default.railButton,
1040
1156
  onClick: onOpen,
1041
- title: formatMoney(monthCost),
1042
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1043
- viewBox: "0 0 24 24",
1044
- fill: "none",
1045
- stroke: "currentColor",
1046
- "aria-hidden": "true",
1047
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" })
1048
- })
1157
+ title: `${t("billing.title")} · ${formatMoney(monthCost)}`,
1158
+ children: cardIcon
1049
1159
  });
1050
1160
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1051
1161
  type: "button",
1052
1162
  className: UsageBilling_module_css_default.trigger,
1053
1163
  onClick: onOpen,
1164
+ title: `${t("billing.title")} · 本月 ${formatMoney(monthCost)}`,
1054
1165
  children: [
1055
1166
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1056
1167
  className: UsageBilling_module_css_default.triggerIcon,
1057
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1058
- viewBox: "0 0 24 24",
1059
- fill: "none",
1060
- stroke: "currentColor",
1061
- "aria-hidden": "true",
1062
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" })
1063
- })
1168
+ children: cardIcon
1064
1169
  }),
1065
1170
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1066
- className: UsageBilling_module_css_default.triggerText,
1171
+ className: UsageBilling_module_css_default.triggerToday,
1067
1172
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1068
- className: UsageBilling_module_css_default.triggerLabel,
1069
- children: "计费仪表盘"
1070
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1071
1173
  className: UsageBilling_module_css_default.triggerMeta,
1072
- children: [
1073
- "当月 ",
1074
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(monthCost) }),
1075
- todayCost > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("em", { children: ["今日 ", formatMoney(todayCost)] })
1076
- ]
1174
+ children: "今日"
1175
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1176
+ className: UsageBilling_module_css_default.triggerAmount,
1177
+ children: formatMoney(todayCost)
1077
1178
  })]
1078
1179
  }),
1079
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1080
- className: UsageBilling_module_css_default.triggerChevron,
1081
- viewBox: "0 0 24 24",
1082
- fill: "none",
1083
- stroke: "currentColor",
1084
- "aria-hidden": "true",
1085
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m9 6 6 6-6 6" })
1180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.triggerDivider }),
1181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1182
+ className: UsageBilling_module_css_default.triggerMonth,
1183
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1184
+ className: UsageBilling_module_css_default.triggerMeta,
1185
+ children: "当月"
1186
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1187
+ className: UsageBilling_module_css_default.triggerAmountSub,
1188
+ children: formatMoney(monthCost)
1189
+ })]
1086
1190
  })
1087
1191
  ]
1088
1192
  });
@@ -1094,9 +1198,9 @@ window.__ModuleLoader__.load({
1094
1198
  function BillingDashboard({ stats, t, onClose, health }) {
1095
1199
  const { total, byModel, byDay } = stats;
1096
1200
  const [pricingOpen, setPricingOpen] = (0, react.useState)(false);
1201
+ const rateInfo = getRateInfo();
1097
1202
  const cacheHitRate = total.cacheHit + total.cacheMiss > 0 ? total.cacheHit / (total.cacheHit + total.cacheMiss) * 100 : 0;
1098
1203
  const dates = Object.keys(byDay).sort();
1099
- const latestDate = dates.at(-1) ?? "";
1100
1204
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1101
1205
  const todayCost = byDay[today]?.cost ?? 0;
1102
1206
  const monthPrefix = today.slice(0, 7);
@@ -1104,7 +1208,17 @@ window.__ModuleLoader__.load({
1104
1208
  const monthCost = dates.reduce((sum, d) => sum + (d.startsWith(monthPrefix) ? byDay[d]?.cost ?? 0 : 0), 0);
1105
1209
  const monthCalls = dates.reduce((sum, d) => sum + (d.startsWith(monthPrefix) ? byDay[d]?.calls ?? 0 : 0), 0);
1106
1210
  const yearCost = dates.reduce((sum, d) => sum + (d.startsWith(yearPrefix) ? byDay[d]?.cost ?? 0 : 0), 0);
1107
- const trend = (0, react.useMemo)(() => dates.map((date) => {
1211
+ const trendDates = (0, react.useMemo)(() => {
1212
+ const out = [];
1213
+ for (let offset = 6; offset >= 0; offset -= 1) {
1214
+ const day = /* @__PURE__ */ new Date();
1215
+ day.setDate(day.getDate() - offset);
1216
+ out.push(day.toISOString().slice(0, 10));
1217
+ }
1218
+ return out;
1219
+ }, []);
1220
+ const latestDate = trendDates.at(-1) ?? today;
1221
+ const trend = (0, react.useMemo)(() => trendDates.map((date) => {
1108
1222
  const byModel = {};
1109
1223
  const dayModels = stats.byDayModels?.[date];
1110
1224
  if (dayModels !== void 0) {
@@ -1118,7 +1232,7 @@ window.__ModuleLoader__.load({
1118
1232
  byModel
1119
1233
  };
1120
1234
  }), [
1121
- dates,
1235
+ trendDates,
1122
1236
  byDay,
1123
1237
  stats.byDayModels
1124
1238
  ]);
@@ -1201,64 +1315,56 @@ window.__ModuleLoader__.load({
1201
1315
  children: [
1202
1316
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1203
1317
  className: UsageBilling_module_css_default.hero,
1204
- children: [
1205
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1206
- className: UsageBilling_module_css_default.heroMain,
1207
- children: [
1208
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1209
- className: UsageBilling_module_css_default.heroLabel,
1210
- children: t("billing.monthCost")
1211
- }),
1212
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1213
- className: UsageBilling_module_css_default.heroValue,
1214
- children: formatMoney(monthCost)
1215
- }),
1216
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1217
- className: UsageBilling_module_css_default.heroMeta,
1318
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1319
+ className: UsageBilling_module_css_default.heroMain,
1320
+ children: [
1321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1322
+ className: UsageBilling_module_css_default.heroLabel,
1323
+ children: t("billing.monthCost")
1324
+ }),
1325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1326
+ className: UsageBilling_module_css_default.heroValue,
1327
+ children: formatMoney(monthCost)
1328
+ }),
1329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1330
+ className: UsageBilling_module_css_default.heroMeta,
1331
+ children: [
1332
+ monthCalls.toLocaleString(),
1333
+ " ",
1334
+ t("billing.calls")
1335
+ ]
1336
+ })
1337
+ ]
1338
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1339
+ className: UsageBilling_module_css_default.heroSide,
1340
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1341
+ className: UsageBilling_module_css_default.heroSideItem,
1342
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1343
+ className: UsageBilling_module_css_default.heroSideLabel,
1344
+ children: t("billing.yearCost")
1345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1346
+ className: UsageBilling_module_css_default.heroSideValue,
1347
+ children: formatMoney(yearCost)
1348
+ })]
1349
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1350
+ className: UsageBilling_module_css_default.heroSideItem,
1351
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1352
+ className: UsageBilling_module_css_default.heroSideLabel,
1353
+ children: t("billing.todayCost")
1354
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1355
+ className: UsageBilling_module_css_default.heroSideValue,
1356
+ children: [formatMoney(todayCost), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1357
+ className: clsx(UsageBilling_module_css_default.delta, deltaPct >= 0 ? UsageBilling_module_css_default.deltaUp : UsageBilling_module_css_default.deltaDown),
1218
1358
  children: [
1219
- monthCalls.toLocaleString(),
1359
+ deltaPct >= 0 ? "▲" : "▼",
1220
1360
  " ",
1221
- t("billing.calls")
1361
+ Math.abs(deltaPct).toFixed(1),
1362
+ "%"
1222
1363
  ]
1223
- })
1224
- ]
1225
- }),
1226
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: UsageBilling_module_css_default.heroDivider }),
1227
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1228
- className: UsageBilling_module_css_default.heroSide,
1229
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1230
- className: UsageBilling_module_css_default.heroSideItem,
1231
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1232
- className: UsageBilling_module_css_default.heroSideLabel,
1233
- children: t("billing.yearCost")
1234
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1235
- className: UsageBilling_module_css_default.heroSideValue,
1236
- children: formatMoney(yearCost)
1237
1364
  })]
1238
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1239
- className: UsageBilling_module_css_default.heroSideItem,
1240
- children: [
1241
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1242
- className: UsageBilling_module_css_default.heroSideLabel,
1243
- children: t("billing.todayCost")
1244
- }),
1245
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1246
- className: UsageBilling_module_css_default.heroSideValue,
1247
- children: formatMoney(todayCost)
1248
- }),
1249
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1250
- className: clsx(UsageBilling_module_css_default.delta, deltaPct >= 0 ? UsageBilling_module_css_default.deltaUp : UsageBilling_module_css_default.deltaDown),
1251
- children: [
1252
- deltaPct >= 0 ? "▲" : "▼",
1253
- " ",
1254
- Math.abs(deltaPct).toFixed(1),
1255
- "%"
1256
- ]
1257
- })
1258
- ]
1259
1365
  })]
1260
- })
1261
- ]
1366
+ })]
1367
+ })]
1262
1368
  }),
1263
1369
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1264
1370
  className: UsageBilling_module_css_default.kpiGrid,
@@ -1483,9 +1589,17 @@ window.__ModuleLoader__.load({
1483
1589
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1484
1590
  className: UsageBilling_module_css_default.panelTitle,
1485
1591
  children: t("billing.pricing")
1486
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1592
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1487
1593
  className: UsageBilling_module_css_default.panelHint,
1488
- children: t("billing.pricePerM")
1594
+ children: [
1595
+ t("billing.todayRate"),
1596
+ " 1 USD = ",
1597
+ formatMoney(rateInfo.rate),
1598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1599
+ className: clsx(UsageBilling_module_css_default.rateBadge, rateInfo.live ? UsageBilling_module_css_default.rateBadgeLive : UsageBilling_module_css_default.rateBadgeBuiltin),
1600
+ children: rateInfo.live ? t("billing.rateLive") : t("billing.rateBuiltin")
1601
+ })
1602
+ ]
1489
1603
  })]
1490
1604
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1491
1605
  className: clsx(UsageBilling_module_css_default.pricingChevron, pricingOpen && UsageBilling_module_css_default.pricingChevronOpen),
@@ -1601,6 +1715,7 @@ window.__ModuleLoader__.load({
1601
1715
  loadUsageStats().then((data) => {
1602
1716
  if (mounted && data !== null) setStats(data);
1603
1717
  });
1718
+ loadLivePricing();
1604
1719
  return () => {
1605
1720
  mounted = false;
1606
1721
  };
@@ -1665,7 +1780,10 @@ window.__ModuleLoader__.load({
1665
1780
  "billing.openDashboard": "打开计费仪表盘",
1666
1781
  "billing.close": "关闭",
1667
1782
  "billing.lastUpdated": "数据更新于",
1668
- "billing.noData": "暂无计费数据"
1783
+ "billing.noData": "暂无计费数据",
1784
+ "billing.todayRate": "今日汇率",
1785
+ "billing.rateLive": "实时",
1786
+ "billing.rateBuiltin": "内置"
1669
1787
  };
1670
1788
  const en = {
1671
1789
  "billing.title": "Usage",
@@ -1701,7 +1819,10 @@ window.__ModuleLoader__.load({
1701
1819
  "billing.openDashboard": "Open billing dashboard",
1702
1820
  "billing.close": "Close",
1703
1821
  "billing.lastUpdated": "Updated",
1704
- "billing.noData": "No billing data yet"
1822
+ "billing.noData": "No billing data yet",
1823
+ "billing.todayRate": "Today rate",
1824
+ "billing.rateLive": "Live",
1825
+ "billing.rateBuiltin": "Built-in"
1705
1826
  };
1706
1827
  //#endregion
1707
1828
  //#region src/client/apply.ts
package/lib/index.js CHANGED
@@ -1,27 +1,10 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- //#region lib/types/client/pricing.js
5
- /**
6
- * Billing engine: per-model price tables and token-usage cost estimation.
7
- *
8
- * Each model's price table uses its NATIVE currency: domestic providers
9
- * (DeepSeek, 智谱, 通义…) publish RMB prices and store them directly;
10
- * overseas providers (OpenAI, Google, xAI, Meta) publish USD.
11
- * Cost is always computed and displayed in CNY — only USD-priced models go
12
- * through the exchange rate, never domestic ones.
13
- *
14
- * Google-style two-band billing is modeled per model: Gemini's Flex tier
15
- * prices spare-capacity traffic at -50%; DeepSeek V4 splits peak
16
- * (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate. The estimator
17
- * mixes both bands by a configured peak share ({@link DEFAULT_PEAK_SHARE}).
18
- */
19
- /**
20
- * USD → CNY rate for display. Source: China Foreign Exchange Trade System
21
- * mid-rate 6.7878 on 2026-08-14; rounded to 6.79. Only applies to overseas
22
- * USD-priced models — domestic models never pass through this rate.
23
- */
24
- const USD_TO_CNY = 6.79;
4
+ /** 当前汇率:实时覆盖优先,缺省回退内置固定值。 */
5
+ function currentRate() {
6
+ return 6.79;
7
+ }
25
8
  /** Default share of traffic assumed to fall in the peak band (0..1). */
26
9
  const DEFAULT_PEAK_SHARE = .5;
27
10
  /**
@@ -500,11 +483,11 @@ const MODEL_CATALOG = [
500
483
  ];
501
484
  /** Lookup a model by its stats key; falls back to the generic `other` entry. */
502
485
  function modelOf(key) {
503
- const found = MODEL_CATALOG.find((entry) => entry.key === key);
504
- if (found !== void 0) return found;
505
- const fallback = MODEL_CATALOG.at(-1);
506
- if (fallback !== void 0) return fallback;
507
- throw new Error("MODEL_CATALOG must not be empty");
486
+ return MODEL_CATALOG.find((entry) => entry.key === key) ?? (() => {
487
+ const fallback = MODEL_CATALOG.at(-1);
488
+ if (fallback !== void 0) return fallback;
489
+ throw new Error("MODEL_CATALOG must not be empty");
490
+ })();
508
491
  }
509
492
  /**
510
493
  * Price one band's token usage in CNY. The stats `input` field is the TOTAL
@@ -518,7 +501,7 @@ function priceBandCost(band, buckets, currency) {
518
501
  const miss = buckets.cacheMiss > 0 ? buckets.cacheMiss : Math.max(0, buckets.input - buckets.cacheHit);
519
502
  const hit = Math.min(buckets.cacheHit, buckets.input);
520
503
  const raw = (miss * (band.cacheMiss ?? band.input) + hit * band.cacheHit + buckets.output * band.output) / 1e6;
521
- return currency === "USD" ? raw * USD_TO_CNY : raw;
504
+ return currency === "USD" ? raw * currentRate() : raw;
522
505
  }
523
506
  /**
524
507
  * Estimate the CNY cost of one model's token usage, mixing the peak and
@@ -673,6 +656,158 @@ async function aggregateUsage(persistence, options = {}) {
673
656
  };
674
657
  }
675
658
  //#endregion
659
+ //#region lib/types/pricing-fetch.js
660
+ /**
661
+ * One-shot live pricing refresh for the billing dashboard.
662
+ *
663
+ * Fetches the USD → CNY mid rate and the OpenRouter model price list, maps
664
+ * matched models onto the built-in catalog keys, and returns the combined
665
+ * LivePricing. Every fetch failure degrades to the built-in values: the node
666
+ * half caches whatever succeeded and the browser dashboard falls back to the
667
+ * catalog for the rest — a total outage answers `{ source: 'builtin' }`.
668
+ */
669
+ /** Abort a fetch when the upstream hangs beyond this budget. */
670
+ const FETCH_TIMEOUT_MS = 8e3;
671
+ /**
672
+ * USD → CNY 汇率源,按顺序尝试:国内可达的腾讯财经行情(免 key、`~` 分隔
673
+ * 第 4 个字段为价格)优先,国外 open.er-api.com 兜底。任一源失败自动落到
674
+ * 下一个;全部失败由调用方降级内置汇率。
675
+ */
676
+ const RATE_SOURCES = [{
677
+ url: "https://qt.gtimg.cn/q=whUSDCNY",
678
+ parse: (text) => {
679
+ const price = /"([^"]*)"/.exec(text)?.[1]?.split("~")[3];
680
+ return price !== void 0 && price !== "" ? Number(price) : void 0;
681
+ }
682
+ }, {
683
+ url: "https://open.er-api.com/v6/latest/USD",
684
+ parse: (text) => {
685
+ try {
686
+ const cny = JSON.parse(text).rates?.CNY;
687
+ return typeof cny === "number" && Number.isFinite(cny) && cny > 0 ? cny : void 0;
688
+ } catch {
689
+ return;
690
+ }
691
+ }
692
+ }];
693
+ /** OpenRouter's public model list: per-token USD prices, no key needed. */
694
+ const ROUTER_URL = "https://openrouter.ai/api/v1/models";
695
+ /**
696
+ * Built-in catalog key → OpenRouter model-id candidates. Matching prefers an
697
+ * exact id, then a single strong substring hit (the router id contains the
698
+ * hint); an ambiguous hit is skipped so the built-in price stays
699
+ * authoritative. Hints are provider+generation words the router ids carry —
700
+ * correct or extend them as the market moves.
701
+ */
702
+ const ROUTER_ID_HINTS = {
703
+ "flash": ["deepseek-v4-flash", "deepseek-v4"],
704
+ "pro": ["deepseek-v4-pro"],
705
+ "glm": ["glm-5"],
706
+ "qwen-3.8-max": ["qwen-3.8-max", "qwen3.8-max"],
707
+ "qwen-max": ["qwen-max"],
708
+ "qwen-plus": ["qwen-plus"],
709
+ "gemini-pro": ["gemini-3-pro", "gemini-pro"],
710
+ "gemini-flash": ["gemini-3-flash", "gemini-flash"],
711
+ "gpt-5.6-sol": ["gpt-5.6-sol"],
712
+ "gpt-5.6-terra": ["gpt-5.6-terra"],
713
+ "gpt-5.6-luna": ["gpt-5.6-luna"],
714
+ "grok": ["grok-4"],
715
+ "llama": ["llama-4"],
716
+ "kimi": ["kimi-k2"]
717
+ };
718
+ /** GET a URL's text body with a hard timeout; null on any failure. */
719
+ async function fetchText(url) {
720
+ const controller = new AbortController();
721
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
722
+ try {
723
+ const response = await fetch(url, { signal: controller.signal });
724
+ if (!response.ok) return null;
725
+ return await response.text();
726
+ } catch {
727
+ return null;
728
+ } finally {
729
+ clearTimeout(timer);
730
+ }
731
+ }
732
+ /** GET a JSON endpoint with a hard timeout; null on any failure. */
733
+ async function fetchJson(url) {
734
+ const text = await fetchText(url);
735
+ if (text === null) return null;
736
+ try {
737
+ return JSON.parse(text);
738
+ } catch {
739
+ return null;
740
+ }
741
+ }
742
+ /** Latest USD → CNY rate from the first working source, or undefined when none respond. */
743
+ async function fetchRate() {
744
+ for (const source of RATE_SOURCES) {
745
+ const text = await fetchText(source.url);
746
+ if (text === null) continue;
747
+ const value = source.parse(text);
748
+ if (value !== void 0 && Number.isFinite(value) && value > 0) return value;
749
+ }
750
+ }
751
+ /** OpenRouter model rows with usable USD unit prices, or undefined on failure. */
752
+ async function fetchRouterModels() {
753
+ const data = await fetchJson(ROUTER_URL);
754
+ if (data === null || typeof data !== "object") return void 0;
755
+ const list = data.data;
756
+ if (!Array.isArray(list)) return void 0;
757
+ const models = [];
758
+ for (const item of list) {
759
+ if (item === null || typeof item !== "object") continue;
760
+ const { id, pricing } = item;
761
+ if (typeof id !== "string" || pricing === null || typeof pricing !== "object") continue;
762
+ const { prompt, completion } = pricing;
763
+ if (typeof prompt !== "number" || typeof completion !== "number") continue;
764
+ if (!Number.isFinite(prompt) || !Number.isFinite(completion)) continue;
765
+ models.push({
766
+ id,
767
+ input: prompt * 1e6,
768
+ output: completion * 1e6
769
+ });
770
+ }
771
+ return models;
772
+ }
773
+ /** Match one catalog key's candidates: exact id first, then a single strong substring hit. */
774
+ function matchRouterModel(hints, models) {
775
+ const exact = models.find((model) => hints.some((hint) => model.id === hint));
776
+ if (exact !== void 0) return exact;
777
+ const strong = models.filter((model) => hints.some((hint) => hint.length >= 8 && model.id.includes(hint)));
778
+ if (strong.length !== 1) return void 0;
779
+ return strong[0];
780
+ }
781
+ /** Map router matches onto catalog keys; undefined when nothing matched. */
782
+ function buildPrices(models) {
783
+ const result = {};
784
+ for (const [key, hints] of Object.entries(ROUTER_ID_HINTS)) {
785
+ const hit = matchRouterModel(hints, models);
786
+ if (hit === void 0) continue;
787
+ result[key] = {
788
+ input: hit.input,
789
+ cacheHit: hit.input * .1,
790
+ output: hit.output
791
+ };
792
+ }
793
+ return Object.keys(result).length > 0 ? result : void 0;
794
+ }
795
+ /**
796
+ * Fetch the live pricing once at boot. Both upstreams run in parallel; a
797
+ * failure in either degrades independently to the built-in value.
798
+ * @returns the live pricing snapshot (builtin when everything failed).
799
+ */
800
+ async function fetchLivePricing() {
801
+ const [rate, models] = await Promise.all([fetchRate(), fetchRouterModels()]);
802
+ const prices = models === void 0 ? void 0 : buildPrices(models);
803
+ if (rate === void 0 && prices === void 0) return { source: "builtin" };
804
+ return {
805
+ source: "live",
806
+ ...rate !== void 0 ? { rate } : {},
807
+ ...prices !== void 0 ? { prices } : {}
808
+ };
809
+ }
810
+ //#endregion
676
811
  //#region lib/types/index.js
677
812
  /**
678
813
  * Usage billing surface plugin, node half.
@@ -700,6 +835,18 @@ function apply(ctx, config = {}) {
700
835
  join(cwd, ".dsh-usage-stats.json"),
701
836
  join(homedir(), ".dsh/.dsh-usage-stats.json")
702
837
  ].filter((path) => typeof path === "string" && path.length > 0);
838
+ let live = { source: "builtin" };
839
+ fetchLivePricing().then((result) => {
840
+ live = result;
841
+ });
842
+ ctx.effect(() => ctx.webServer.register({
843
+ kind: "exact",
844
+ path: "/api/billing/pricing",
845
+ handler: async (_req, res) => {
846
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
847
+ res.end(JSON.stringify(live));
848
+ }
849
+ }), "usage-billing: pricing route");
703
850
  ctx.effect(() => ctx.webServer.register({
704
851
  kind: "exact",
705
852
  path: "/api/billing/usage-stats",
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData';
2
+ export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin';
3
3
  export declare const NS = "usageBilling";
4
4
  export declare const zh: Record<UsageBillingKey, string>;
5
5
  export declare const en: Record<UsageBillingKey, string>;
@@ -12,12 +12,29 @@
12
12
  * (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate. The estimator
13
13
  * mixes both bands by a configured peak share ({@link DEFAULT_PEAK_SHARE}).
14
14
  */
15
+ import type { LivePricing } from '../pricing-shared.ts';
15
16
  /**
16
17
  * USD → CNY rate for display. Source: China Foreign Exchange Trade System
17
18
  * mid-rate 6.7878 on 2026-08-14; rounded to 6.79. Only applies to overseas
18
19
  * USD-priced models — domestic models never pass through this rate.
20
+ * The node half may refresh this at boot via `/api/billing/pricing`; until a
21
+ * live rate arrives the built-in value stays in force.
19
22
  */
20
23
  export declare const USD_TO_CNY = 6.79;
24
+ /**
25
+ * Apply the node half's live pricing snapshot. Absent fields keep the
26
+ * built-in catalog and rate; callers never fabricate values.
27
+ * @param pricing - the `/api/billing/pricing` response.
28
+ */
29
+ export declare function applyLivePricing(pricing: LivePricing): void;
30
+ /**
31
+ * 当前生效的 USD → CNY 汇率及其来源:live = 启动时实时拉取成功,
32
+ * builtin = 实时拉取失败、正在用内置默认值。
33
+ */
34
+ export declare function getRateInfo(): {
35
+ rate: number;
36
+ live: boolean;
37
+ };
21
38
  /** Default share of traffic assumed to fall in the peak band (0..1). */
22
39
  export declare const DEFAULT_PEAK_SHARE = 0.5;
23
40
  /**
@@ -0,0 +1,17 @@
1
+ /**
2
+ * One-shot live pricing refresh for the billing dashboard.
3
+ *
4
+ * Fetches the USD → CNY mid rate and the OpenRouter model price list, maps
5
+ * matched models onto the built-in catalog keys, and returns the combined
6
+ * LivePricing. Every fetch failure degrades to the built-in values: the node
7
+ * half caches whatever succeeded and the browser dashboard falls back to the
8
+ * catalog for the rest — a total outage answers `{ source: 'builtin' }`.
9
+ */
10
+ import type { LivePricing } from './pricing-shared.ts';
11
+ /**
12
+ * Fetch the live pricing once at boot. Both upstreams run in parallel; a
13
+ * failure in either degrades independently to the built-in value.
14
+ * @returns the live pricing snapshot (builtin when everything failed).
15
+ */
16
+ export declare function fetchLivePricing(): Promise<LivePricing>;
17
+ //# sourceMappingURL=pricing-fetch.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Live-pricing wire types shared by the node half (which fetches and caches
3
+ * once at boot) and the browser half (which applies the overrides on top of
4
+ * the built-in catalog). An absent field means the built-in value still
5
+ * applies — the dashboard degrades to the catalog, never to fabricated data.
6
+ */
7
+ /** One model's unit prices as reported by the router, in USD per 1M tokens. */
8
+ export interface LivePrice {
9
+ /** Uncached input price per 1M tokens. */
10
+ input: number;
11
+ /** Cache-hit input price per 1M tokens (estimated as 10% of input; the router list carries no cache band). */
12
+ cacheHit: number;
13
+ /** Output price per 1M tokens. */
14
+ output: number;
15
+ }
16
+ /** Response of `/api/billing/pricing` consumed by the dashboard. */
17
+ export interface LivePricing {
18
+ /** live = at least one fetch succeeded; builtin = full fallback to the catalog. */
19
+ source: 'live' | 'builtin';
20
+ /** USD → CNY mid rate (present when the rate fetch succeeded). */
21
+ rate?: number;
22
+ /** Overrides keyed by built-in catalog key (present when router matches succeeded). */
23
+ prices?: Record<string, LivePrice>;
24
+ }
25
+ //# sourceMappingURL=pricing-shared.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kenz1117/dsh-ui-usage-billing",
3
3
  "description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },