dsh-simple-usage-info 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,8 +4,8 @@
4
4
  [![license](https://img.shields.io/npm/l/dsh-simple-usage-info.svg)](LICENSE)
5
5
 
6
6
  A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that puts your
7
- **DeepSeek API balance** and the **current billing window** in the composer dock — the ambient row
8
- directly below the message input, after the context meter.
7
+ **DeepSeek API balance**, the **current billing window**, and the **session estimated cost** in the
8
+ composer dock — the ambient row directly below the message input, after the context meter.
9
9
 
10
10
  ```
11
11
  ┌──────────────────────────────────────────────────────────┐
@@ -26,6 +26,10 @@ Applies because Chinese public holiday
26
26
  Peak hours today (Asia/Manila) 09:00–12:00, 14:00–18:00
27
27
  Next switch peak in 6d 13h (Thu 09:00)
28
28
  ─────────────────────────────────────────────────────────────
29
+ Estimated cost
30
+ USD $0.012
31
+ CNY ¥0.08
32
+ ─────────────────────────────────────────────────────────────
29
33
  CNY
30
34
  Total ¥22.48
31
35
  Granted ¥0.00
@@ -45,6 +49,12 @@ holidays. The holiday list is fetched from
45
49
  [NateScarlet/holiday-cn](https://github.com/NateScarlet/holiday-cn), which scrapes the State Council
46
50
  announcements daily, so it keeps itself current.
47
51
 
52
+ The **Estimated cost** section shows the running session cost in both USD and CNY, calculated from
53
+ the token usage (cached input, uncached input, and output) using the published peak and off-peak
54
+ rates of the model selected for the session. Supported models are `deepseek-flash`
55
+ (DeepSeek-V41-Flash) and `deepseek-v4-pro` (DeepSeek-V4-Pro-0813). The section appears only when
56
+ the session has non-zero token usage.
57
+
48
58
  ## Requirements
49
59
 
50
60
  - DeepSeek Harness `0.2.0-rc.2` or later
@@ -187,57 +197,9 @@ bun run preview:panel # prints the popover markup and stylesheet, no browse
187
197
  There is no build step: `lib/index.js` is plain ESM, and `lib/client.js` is hand-authored in the web
188
198
  client's module-loader bundle format. Neither half needs a bundler.
189
199
 
190
- ## Publishing
191
-
192
- The repository publishes to npm from GitHub Actions
193
- ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)): tests run on every push and pull request,
194
- and a **GitHub Release** — or the *Run workflow* button — publishes to the registry.
195
-
196
- Authentication uses **[npm trusted publishing](https://docs.npmjs.com/trusted-publishers)**, so
197
- there is **no `NPM_TOKEN` secret and no long-lived credential to rotate**. The publish job holds
198
- `id-token: write`, GitHub mints an OIDC token, and npm exchanges it for a credential valid for that
199
- one publish. Provenance comes from the same token.
200
-
201
- The two jobs install differently on purpose:
202
-
203
- - **`test`** installs with **bun** (`bun install --frozen-lockfile`) and runs the preflight suite.
204
- - **`publish`** is **npm-only and installs nothing**. The published files are committed sources with
205
- no build step, so `npm publish` just packs `lib/` and `cordis.patch.yml`. It is gated on `test`
206
- via `needs:`, so nothing reaches the registry untested.
207
-
208
- ### One-time setup
209
-
210
- **Step 1 — publish the first version by hand.** This is not optional. A trusted publisher is
211
- configured on a package page, and npm's own setup path is *Packages → YOUR_PACKAGE → Settings →
212
- Trusted publishing* — which does not exist until the package does. An OIDC token **cannot create a
213
- new package**, so publishing before this step fails with `E404` on the `PUT`.
214
-
215
- ```sh
216
- bun install && bun run preflight
217
- npm login # as your npm account
218
- npm whoami
219
- npm publish --access public
220
- ```
221
-
222
- That publishes `0.1.0` and creates the package page.
223
-
224
- **Step 2 — add the trusted publisher** on npmjs.com at
225
- `https://www.npmjs.com/package/dsh-simple-usage-info/access`:
226
-
227
- | Field | Value |
228
- |---|---|
229
- | Publisher | GitHub Actions |
230
- | Organization or user | `MarJose123` — the **GitHub** owner, *not* the npm username |
231
- | Repository | `dsh-simple-usage-info` |
232
- | Workflow filename | `ci.yml` — the filename only, with the extension, not the full path |
233
- | Environment name | *(leave blank — the job declares no environment)* |
234
- | Allowed actions | **tick "Allow npm publish"** |
235
-
236
- That last row is the trap: `npm stage publish` is always permitted, but direct `npm publish` is a
237
- separate opt-in and is **off by default**. Without it the publish is rejected even though the
238
- workflow is otherwise correct.
200
+ ### Preparation for release
239
201
 
240
- **Step 3 — release via CI.** Bump the version, then cut a GitHub Release:
202
+ **release via CI.** Bump the version, then cut a GitHub Release:
241
203
 
242
204
  ```sh
243
205
  npm version 0.1.1 --no-git-tag-version
@@ -245,24 +207,6 @@ git commit -am "Release 0.1.1" && git push
245
207
  gh release create v0.1.1 --generate-notes
246
208
  ```
247
209
 
248
- The publish job runs, and from here on every release goes through OIDC. Revoke the bootstrap token
249
- if you created one — nothing needs it any more.
250
-
251
- > If you would rather `0.1.0` be the CI-published version, bootstrap a throwaway `0.0.1` instead,
252
- > then set the version back to `0.1.0` and release that. The bootstrap version is scaffolding, so it
253
- > never carries provenance.
254
-
255
- ### Reading a failed publish
256
-
257
- The two failure modes look similar but have different causes, and npm's error code tells them apart:
258
-
259
- | Error | Meaning |
260
- |---|---|
261
- | `E404` on `PUT` | The package does not exist yet — do the bootstrap above |
262
- | `ENEEDAUTH` | The package exists but the trusted publisher does not match: check the GitHub owner, repository, and workflow filename are **exact and case-sensitive**, and that the job has `id-token: write` |
263
-
264
- The workflow checks for the first case before publishing and fails with the remedy instead of a bare
265
- 404.
266
210
 
267
211
  ## License
268
212
 
package/lib/client.d.ts CHANGED
@@ -62,8 +62,37 @@ export interface PanelModel {
62
62
  readonly footer: string
63
63
  }
64
64
 
65
+ /** Token usage from the `tokenUsage` session projection (flat object). */
66
+ export interface TokenUsageProjection {
67
+ readonly uncachedInputTokens: number
68
+ readonly outputTokens: number
69
+ readonly cacheReadTokens: number
70
+ readonly cacheWriteTokens: number
71
+ }
72
+
73
+ /** Per-currency per-million-token rates for one billing window. */
74
+ export interface CurrencyRates {
75
+ readonly usd: { readonly inputCacheHit: number; readonly inputCacheMiss: number; readonly output: number }
76
+ readonly cny: { readonly inputCacheHit: number; readonly inputCacheMiss: number; readonly output: number }
77
+ }
78
+
79
+ /** Rates for a model across peak and off-peak billing windows. */
80
+ export interface ModelRates {
81
+ readonly peak: CurrencyRates
82
+ readonly offPeak: CurrencyRates
83
+ }
84
+
85
+ /** Estimated cost in both currencies. */
86
+ export interface EstimatedCost {
87
+ readonly usd: number
88
+ readonly cny: number
89
+ }
90
+
91
+ /** Known model keys recognised by the rates table. */
92
+ export type ModelKey = 'deepseek-flash' | 'deepseek-v4-pro'
93
+
65
94
  /** The composer-dock entry component. */
66
- export declare function UsageBar(): unknown
95
+ export declare function UsageBar(props?: { useProjection?: (key: string) => unknown }): unknown
67
96
  /** Client plugin body: one entry in the composer dock. */
68
97
  export declare function apply(ctx: ClientContext): void
69
98
 
@@ -74,7 +103,15 @@ export declare function summarize(state: { status: string; payload?: unknown }):
74
103
  /** The peak / off-peak pill: the window name and nothing else. */
75
104
  export declare function priceBadge(pricing?: { window?: string } | null): unknown
76
105
  /** The popover's contents, as data. */
77
- export declare function panelModel(payload: unknown, pricing: unknown, now: number): PanelModel
106
+ export declare function panelModel(payload: unknown, pricing: unknown, now: number, tokenUsage?: TokenUsageProjection | undefined, model?: string | undefined): PanelModel
107
+ /** Estimate cost from token usage at the current billing window. */
108
+ export declare function estimateCost(usage: TokenUsageProjection | undefined, isPeak: boolean, model?: string | undefined): EstimatedCost | null
109
+ /** Resolve the rates table for a model key and billing window, falling back to the default. */
110
+ export declare function getModelRates(model?: string | undefined, isPeak?: boolean): CurrencyRates
111
+ /** Format a dollar amount. */
112
+ export declare function formatUsd(value: number): string
113
+ /** Format a CNY amount. */
114
+ export declare function formatCny(value: number): string
78
115
  /** `2m` / `2h 5m` / `2d 2h`, always rounding up. */
79
116
  export declare function countdown(target?: number, now?: number): string | undefined
80
117
  /** Which window one instant falls in, mirroring lib/pricing.js. */
package/lib/client.js CHANGED
@@ -49,10 +49,133 @@ window.__ModuleLoader__.load({
49
49
  const POLL_MS = 60000
50
50
  /** How often the clock-derived badge re-evaluates between polls. */
51
51
  const TICK_MS = 20000
52
+ /** Random retry window when in error state (5-10 seconds). */
53
+ const RETRY_MIN_MS = 5000
54
+ const RETRY_MAX_MS = 10000
52
55
 
53
56
  const PEAK = 'peak'
54
57
  const OFF_PEAK = 'off-peak'
55
58
 
59
+ // ------------------------------------------------------------------ pricing
60
+
61
+ /**
62
+ * Per-model per-million-token rates for peak and off-peak billing windows.
63
+ *
64
+ * Source: https://api-docs.deepseek.com/quick_start/pricing
65
+ */
66
+ const RATES = {
67
+ 'deepseek-flash': {
68
+ peak: {
69
+ usd: {
70
+ inputCacheHit: 0.006,
71
+ inputCacheMiss: 0.30,
72
+ output: 1.20
73
+ },
74
+ cny: {
75
+ inputCacheHit: 0.04,
76
+ inputCacheMiss: 2,
77
+ output: 8
78
+ }
79
+ },
80
+ offPeak: {
81
+ usd: {
82
+ inputCacheHit: 0.003,
83
+ inputCacheMiss: 0.15,
84
+ output: 0.60
85
+ },
86
+ cny: {
87
+ inputCacheHit: 0.02,
88
+ inputCacheMiss: 1,
89
+ output: 4
90
+ }
91
+ }
92
+ },
93
+ 'deepseek-v4-pro': {
94
+ peak: {
95
+ usd: {
96
+ inputCacheHit: 0.044,
97
+ inputCacheMiss: 1.32,
98
+ output: 3.96
99
+ },
100
+ cny: {
101
+ inputCacheHit: 0.30,
102
+ inputCacheMiss: 9.00,
103
+ output: 27.0
104
+ }
105
+ },
106
+ offPeak: {
107
+ usd: {
108
+ inputCacheHit: 0.022,
109
+ inputCacheMiss: 0.66,
110
+ output: 1.98
111
+ },
112
+ cny: {
113
+ inputCacheHit: 0.15,
114
+ inputCacheMiss: 4.5,
115
+ output: 13.5
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ /** Default model key when the session model is unknown. */
122
+ const DEFAULT_MODEL = 'deepseek-flash'
123
+
124
+ /**
125
+ * Resolve the rates table for a model key and billing window,
126
+ * falling back to the default model.
127
+ */
128
+ function getModelRates(model, isPeak) {
129
+ const modelRates = RATES[model] ?? RATES[DEFAULT_MODEL]
130
+ return isPeak ? modelRates.peak : modelRates.offPeak
131
+ }
132
+
133
+ /**
134
+ * Estimate the cost for one token-usage snapshot at the current billing
135
+ * window. Returns costs in both currencies.
136
+ * @param usage - the flat `TokenUsageProjection` from `useProjection('tokenUsage')`.
137
+ * @param isPeak - whether the current window is peak-rate.
138
+ * @param model - optional model key (e.g. `'deepseek-flash'`, `'deepseek-v4-pro'`); defaults to Flash.
139
+ * @returns estimated costs in USD and CNY, or `null` when usage is zero.
140
+ */
141
+ function estimateCost(usage, isPeak, model) {
142
+ if (usage === undefined || usage === null) return null
143
+ const cacheHit = usage.cacheReadTokens ?? 0
144
+ const cacheMiss = usage.uncachedInputTokens ?? 0
145
+ const output = usage.outputTokens ?? 0
146
+ if (cacheHit + cacheMiss + output === 0) return null
147
+
148
+ const rates = getModelRates(model, isPeak)
149
+
150
+ const usd = (
151
+ cacheHit * rates.usd.inputCacheHit +
152
+ cacheMiss * rates.usd.inputCacheMiss +
153
+ output * rates.usd.output
154
+ ) / 1_000_000
155
+
156
+ const cny = (
157
+ cacheHit * rates.cny.inputCacheHit +
158
+ cacheMiss * rates.cny.inputCacheMiss +
159
+ output * rates.cny.output
160
+ ) / 1_000_000
161
+
162
+ return { usd, cny }
163
+ }
164
+
165
+ /** Format a small dollar amount, e.g. `$0.012` / `$0.00`. */
166
+ function formatUsd(value) {
167
+ const clamped = Math.max(0, value)
168
+ if (clamped >= 1) return `$${clamped.toFixed(2)}`
169
+ return `$${parseFloat(clamped.toFixed(4)).toFixed(4).replace(/0+$/, '').replace(/\.$/, '')}`
170
+ }
171
+
172
+ /** Format a small CNY amount, e.g. `¥0.08` / `¥0.00`. */
173
+ function formatCny(value) {
174
+ const clamped = Math.max(0, value)
175
+ if (clamped >= 1) return `¥${clamped.toFixed(2)}`
176
+ return `¥${parseFloat(clamped.toFixed(4)).toFixed(4).replace(/0+$/, '').replace(/\.$/, '')}`
177
+ }
178
+
56
179
  // ------------------------------------------------------------------ styles
57
180
 
58
181
  /**
@@ -95,6 +218,7 @@ window.__ModuleLoader__.load({
95
218
  .dsu-dot-ok{background:var(--dsw-alias-state-success-primary)}
96
219
  .dsu-dot-low{background:var(--dsw-alias-state-error-primary)}
97
220
  .dsu-dot-unknown{background:var(--dsw-alias-label-quaternary)}
221
+ .dsu-dot-error{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:8px solid var(--dsw-alias-state-warning-primary, #e67e22);flex:none;border-radius:0}
98
222
  .dsu-pill{padding:0 6px;border-radius:999px;border:1px solid currentColor;font-size:11px;line-height:15px;font-weight:500;flex:none}
99
223
  .dsu-pill-off{color:var(--dsw-alias-state-success-primary)}
100
224
  .dsu-pill-peak{color:var(--dsw-alias-state-error-primary)}
@@ -285,17 +409,30 @@ window.__ModuleLoader__.load({
285
409
  }
286
410
 
287
411
  /**
288
- * One line of text for the dock. The API reports every configured wallet,
289
- * usually including a permanently zero one, so the bar shows the first
290
- * funded wallet; the popover carries the full breakdown either way.
412
+ * One line of text for the dock. Shows "DeepSeek ¥19.41" if USD is empty or
413
+ * zero, "DeepSeek ¥19.41 + $10.00" if both are funded, or "DeepSeek $10.00"
414
+ * if CNY is empty; the popover carries the full breakdown.
291
415
  */
292
416
  function summarize(state) {
293
417
  if (state.status === 'loading') return 'DeepSeek …'
294
418
  if (state.status === 'error') return 'DeepSeek unavailable'
295
419
  const infos = state.payload?.balance?.balance_infos ?? []
296
420
  if (infos.length === 0) return 'DeepSeek no balance'
297
- const funded = infos.find((info) => Number(info?.total_balance ?? 0) > 0)
298
- return `DeepSeek ${formatWallet(funded ?? infos[0])}`
421
+ const cny = infos.find((info) => info?.currency === 'CNY')
422
+ const usd = infos.find((info) => info?.currency === 'USD')
423
+ const hasCny = cny !== undefined && Number(cny.total_balance ?? 0) > 0
424
+ const hasUsd = usd !== undefined && Number(usd.total_balance ?? 0) > 0
425
+
426
+ if (hasCny && hasUsd) {
427
+ return `DeepSeek ${formatWallet(cny)} + ${formatWallet(usd)}`
428
+ }
429
+ if (hasCny || (cny !== undefined && !hasUsd)) {
430
+ return `DeepSeek ${formatWallet(cny)}`
431
+ }
432
+ if (hasUsd || usd !== undefined) {
433
+ return `DeepSeek ${formatWallet(usd)}`
434
+ }
435
+ return `DeepSeek ${infos.map(formatWallet).join(' + ')}`
299
436
  }
300
437
 
301
438
  /**
@@ -303,9 +440,11 @@ window.__ModuleLoader__.load({
303
440
  * @param payload - the host's balance branch, or undefined.
304
441
  * @param pricing - the live billing reading.
305
442
  * @param now - skew-corrected epoch milliseconds.
443
+ * @param tokenUsage - the token usage totals from the `tokenUsage` projection.
444
+ * @param model - optional model key for cost estimation (e.g. `'deepseek-flash'`, `'deepseek-v4-pro'`).
306
445
  * @returns the headline, the label/value groups, and the footer.
307
446
  */
308
- function panelModel(payload, pricing, now) {
447
+ function panelModel(payload, pricing, now, tokenUsage, model) {
309
448
  const infos = payload?.ok === true ? (payload.balance?.balance_infos ?? []) : []
310
449
  const funded = infos.find((info) => Number(info?.total_balance ?? 0) > 0)
311
450
  // The API lists a permanently zero wallet first (USD beside a funded CNY),
@@ -340,6 +479,18 @@ window.__ModuleLoader__.load({
340
479
  groups.push({ title: 'Billing window', rows })
341
480
  }
342
481
 
482
+ // Estimated cost from session token usage
483
+ const cost = estimateCost(tokenUsage, pricing?.window === PEAK, model)
484
+ if (cost !== null) {
485
+ groups.push({
486
+ title: 'Estimated cost',
487
+ rows: [
488
+ { label: 'USD', value: formatUsd(cost.usd) },
489
+ { label: 'CNY', value: formatCny(cost.cny) }
490
+ ]
491
+ })
492
+ }
493
+
343
494
  for (const info of ordered) {
344
495
  groups.push({
345
496
  title: String(info.currency ?? 'balance'),
@@ -462,15 +613,35 @@ window.__ModuleLoader__.load({
462
613
  * The dock entry. Reads the host route on mount and once a minute after,
463
614
  * re-evaluates the billing window every 20 s in between, and opens the
464
615
  * popover on click.
616
+ * @param props - dock props including `useProjection` from the session standard kit.
465
617
  * @returns the bar plus, while open, the portaled panel.
466
618
  */
467
- function UsageBar() {
619
+ function UsageBar(props) {
620
+ // Read session projections so the popover can estimate cost with the correct model.
621
+ // `useProjection` is supplied by the slot system's session standard kit.
622
+ const useProjection = props && props.useProjection ? props.useProjection : undefined
623
+ const tokenUsage = useProjection ? useProjection('tokenUsage') : undefined
624
+ const modelSelection = useProjection ? useProjection('modelSelection') : undefined
625
+ // Derive the model key from the last-used model, falling back to the pending selection.
626
+ const model = (modelSelection?.lastUsed?.model ?? modelSelection?.next?.model ?? undefined)
468
627
  const [state, setState] = React.useState({ status: 'loading' })
469
628
  const [open, setOpen] = React.useState(false)
470
629
  const [, setTick] = React.useState(0)
471
630
  const alive = React.useRef(true)
472
631
  const rootRef = React.useRef(null)
473
632
  const panelRef = React.useRef(null)
633
+ const retryTimer = React.useRef(null)
634
+
635
+ const scheduleRetry = () => {
636
+ clearTimeout(retryTimer.current)
637
+ const delay = RETRY_MIN_MS + Math.random() * (RETRY_MAX_MS - RETRY_MIN_MS)
638
+ retryTimer.current = setTimeout(() => load(true), delay)
639
+ }
640
+
641
+ const clearRetry = () => {
642
+ clearTimeout(retryTimer.current)
643
+ retryTimer.current = null
644
+ }
474
645
 
475
646
  const load = React.useCallback(async (force) => {
476
647
  try {
@@ -484,18 +655,26 @@ window.__ModuleLoader__.load({
484
655
  // stays on screen even when the balance read failed.
485
656
  const pricing = body?.pricing
486
657
  const skewMs = typeof pricing?.now === 'number' ? pricing.now - Date.now() : 0
487
- if (body !== undefined && body.ok === true) setState({ status: 'ready', payload: body, pricing, skewMs })
488
- else if (body !== undefined && body.ok === false)
658
+ if (body !== undefined && body.ok === true) {
659
+ clearRetry()
660
+ setState({ status: 'ready', payload: body, pricing, skewMs })
661
+ } else if (body !== undefined && body.ok === false) {
662
+ scheduleRetry()
489
663
  setState({ status: 'error', message: String(body.error?.message ?? 'unknown error'), pricing, skewMs })
490
- else setState({ status: 'error', message: `HTTP ${response.status}` })
664
+ } else {
665
+ scheduleRetry()
666
+ setState({ status: 'error', message: `HTTP ${response.status}` })
667
+ }
491
668
  } catch (error) {
492
- if (alive.current === true)
669
+ if (alive.current === true) {
670
+ scheduleRetry()
493
671
  setState((previous) => ({
494
672
  status: 'error',
495
673
  message: String(error?.message ?? error),
496
674
  pricing: previous.pricing,
497
675
  skewMs: previous.skewMs
498
676
  }))
677
+ }
499
678
  }
500
679
  }, [])
501
680
 
@@ -506,6 +685,7 @@ window.__ModuleLoader__.load({
506
685
  const tick = setInterval(() => setTick((count) => count + 1), TICK_MS)
507
686
  return () => {
508
687
  alive.current = false
688
+ clearRetry()
509
689
  clearInterval(poll)
510
690
  clearInterval(tick)
511
691
  }
@@ -547,6 +727,17 @@ window.__ModuleLoader__.load({
547
727
  : host
548
728
 
549
729
  const available = state.status === 'ready' ? state.payload?.balance?.is_available : undefined
730
+ const isError = state.status === 'error'
731
+ const barChildren = []
732
+ if (isError) {
733
+ barChildren.push(React.createElement('span', { key: 'dot', 'aria-hidden': true, className: 'dsu-dot-error' }))
734
+ } else {
735
+ barChildren.push(dot(available))
736
+ }
737
+ barChildren.push(React.createElement('span', { key: 'text' }, summarize(state)))
738
+ if (!isError) {
739
+ barChildren.push(priceBadge(pricing))
740
+ }
550
741
  const bar = React.createElement(
551
742
  'span',
552
743
  {
@@ -554,26 +745,36 @@ window.__ModuleLoader__.load({
554
745
  ref: rootRef,
555
746
  role: 'button',
556
747
  tabIndex: 0,
557
- 'aria-haspopup': 'dialog',
748
+ 'aria-haspopup': isError ? undefined : 'dialog',
558
749
  'aria-expanded': open,
559
- title: 'DeepSeek balance and billing window',
560
- onClick: () => setOpen((value) => value !== true),
750
+ title: isError ? 'Reconnecting automatically' : 'DeepSeek balance and billing window',
751
+ onClick: () => {
752
+ if (isError) {
753
+ load(true)
754
+ } else {
755
+ setOpen((value) => value !== true)
756
+ }
757
+ },
561
758
  onKeyDown: (event) => {
562
759
  if (event.key === 'Enter' || event.key === ' ') {
563
760
  event.preventDefault()
564
- setOpen((value) => value !== true)
761
+ if (isError) {
762
+ load(true)
763
+ } else {
764
+ setOpen((value) => value !== true)
765
+ }
565
766
  }
566
767
  },
567
768
  className: 'dsu-bar'
568
769
  },
569
- [dot(available), React.createElement('span', { key: 'text' }, summarize(state)), priceBadge(pricing)]
770
+ barChildren
570
771
  )
571
772
 
572
773
  return React.createElement(React.Fragment, null, [
573
774
  bar,
574
- open === true
775
+ open === true && !isError
575
776
  ? ReactDOM.createPortal(
576
- panelElement(panelModel(state.payload, pricing, now), position, panelRef, () => load(true)),
777
+ panelElement(panelModel(state.payload, pricing, now, tokenUsage, model), position, panelRef, () => load(true)),
577
778
  document.body
578
779
  )
579
780
  : null
@@ -608,6 +809,10 @@ window.__ModuleLoader__.load({
608
809
  exports.summarize = summarize
609
810
  exports.priceBadge = priceBadge
610
811
  exports.panelModel = panelModel
812
+ exports.estimateCost = estimateCost
813
+ exports.getModelRates = getModelRates
814
+ exports.formatUsd = formatUsd
815
+ exports.formatCny = formatCny
611
816
  exports.countdown = countdown
612
817
  exports.windowAt = windowAt
613
818
  exports.readWindow = readWindow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-simple-usage-info",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "DeepSeek Harness plugin: your DeepSeek API balance and the current peak/off-peak billing window, below the message input.",
5
5
  "keywords": [
6
6
  "dsh",