dsh-simple-usage-info 0.1.0 → 0.2.1
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 +14 -70
- package/lib/client.d.ts +39 -2
- package/lib/client.js +223 -18
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
6
|
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that puts your
|
|
7
|
-
**DeepSeek API balance
|
|
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
|
-
|
|
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
|
-
**
|
|
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.
|
|
289
|
-
*
|
|
290
|
-
*
|
|
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
|
|
298
|
-
|
|
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)
|
|
488
|
-
|
|
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
|
|
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: () =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -73,14 +73,14 @@
|
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"@deepseek-ai/cordis": "~4.0.4",
|
|
76
|
-
"@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
|
|
77
|
-
"@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-credentials": "0.2.0-rc.2 || 0.2.1-alpha.1",
|
|
77
|
+
"@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2 || 0.2.1-alpha.1",
|
|
78
78
|
"@deepseek-ai/schemastery": "~3.18.4"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@deepseek-ai/cordis": "~4.0.4",
|
|
82
|
-
"@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
|
|
83
|
-
"@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-credentials": "0.2.0-rc.2 || 0.2.1-alpha.1",
|
|
83
|
+
"@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2 || 0.2.1-alpha.1",
|
|
84
84
|
"@deepseek-ai/schemastery": "~3.18.4",
|
|
85
85
|
"react": "^18.3.1",
|
|
86
86
|
"react-dom": "^18.3.1"
|