@rayadesu/dsh-billing 0.1.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/AGENTS.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +118 -0
  5. package/README.zh.md +114 -0
  6. package/cordis.patch.yml +20 -0
  7. package/package.json +33 -0
  8. package/packages/llm-billing/README.i18n.yaml +6 -0
  9. package/packages/llm-billing/README.md +44 -0
  10. package/packages/llm-billing/README.zh.md +44 -0
  11. package/packages/llm-billing/lib/index.js +562 -0
  12. package/packages/llm-billing/lib/invariant.js +23 -0
  13. package/packages/llm-billing/lib/tsconfig.tsbuildinfo +1 -0
  14. package/packages/llm-billing/lib/typert.host.d.ts +3 -0
  15. package/packages/llm-billing/lib/typert.host.js +114 -0
  16. package/packages/llm-billing/lib/typert.remote-client.d.ts +27 -0
  17. package/packages/llm-billing/lib/typert.remote-client.d.ts.map +1 -0
  18. package/packages/llm-billing/lib/typert.remote-client.js +108 -0
  19. package/packages/llm-billing/lib/types/balance.d.ts +71 -0
  20. package/packages/llm-billing/lib/types/balance.d.ts.map +1 -0
  21. package/packages/llm-billing/lib/types/balance.js +187 -0
  22. package/packages/llm-billing/lib/types/balance.js.map +1 -0
  23. package/packages/llm-billing/lib/types/billing.d.ts +106 -0
  24. package/packages/llm-billing/lib/types/billing.d.ts.map +1 -0
  25. package/packages/llm-billing/lib/types/billing.js +163 -0
  26. package/packages/llm-billing/lib/types/billing.js.map +1 -0
  27. package/packages/llm-billing/lib/types/index.d.ts +49 -0
  28. package/packages/llm-billing/lib/types/index.d.ts.map +1 -0
  29. package/packages/llm-billing/lib/types/index.js +155 -0
  30. package/packages/llm-billing/lib/types/index.js.map +1 -0
  31. package/packages/llm-billing/lib/types/invariant.d.ts +16 -0
  32. package/packages/llm-billing/lib/types/invariant.d.ts.map +1 -0
  33. package/packages/llm-billing/lib/types/invariant.js +22 -0
  34. package/packages/llm-billing/lib/types/invariant.js.map +1 -0
  35. package/packages/llm-billing/lib/types/types/balance.d.ts +71 -0
  36. package/packages/llm-billing/lib/types/types/balance.d.ts.map +1 -0
  37. package/packages/llm-billing/lib/types/types/balance.js +187 -0
  38. package/packages/llm-billing/lib/types/types/balance.js.map +1 -0
  39. package/packages/llm-billing/lib/types/types/billing.d.ts +106 -0
  40. package/packages/llm-billing/lib/types/types/billing.d.ts.map +1 -0
  41. package/packages/llm-billing/lib/types/types/billing.js +170 -0
  42. package/packages/llm-billing/lib/types/types/billing.js.map +1 -0
  43. package/packages/llm-billing/lib/types/types/index.d.ts +49 -0
  44. package/packages/llm-billing/lib/types/types/index.d.ts.map +1 -0
  45. package/packages/llm-billing/lib/types/types/index.js +156 -0
  46. package/packages/llm-billing/lib/types/types/index.js.map +1 -0
  47. package/packages/llm-billing/lib/types/types/invariant.d.ts +16 -0
  48. package/packages/llm-billing/lib/types/types/invariant.d.ts.map +1 -0
  49. package/packages/llm-billing/lib/types/types/invariant.js +22 -0
  50. package/packages/llm-billing/lib/types/types/invariant.js.map +1 -0
  51. package/packages/llm-billing/lib/types/types/types.d.ts +63 -0
  52. package/packages/llm-billing/lib/types/types/types.d.ts.map +1 -0
  53. package/packages/llm-billing/lib/types/types/types.js +7 -0
  54. package/packages/llm-billing/lib/types/types/types.js.map +1 -0
  55. package/packages/llm-billing/lib/types/types.d.ts +63 -0
  56. package/packages/llm-billing/lib/types/types.d.ts.map +1 -0
  57. package/packages/llm-billing/lib/types/types.js +7 -0
  58. package/packages/llm-billing/lib/types/types.js.map +1 -0
  59. package/packages/llm-billing/node_modules/.bin/cordis +43 -0
  60. package/packages/llm-billing/node_modules/.bin/cordis.CMD +12 -0
  61. package/packages/llm-billing/node_modules/.bin/cordis.ps1 +41 -0
  62. package/packages/llm-billing/package.json +75 -0
  63. package/packages/llm-billing/src/balance.ts +164 -0
  64. package/packages/llm-billing/src/billing.ts +251 -0
  65. package/packages/llm-billing/src/index.ts +219 -0
  66. package/packages/llm-billing/src/invariant.ts +30 -0
  67. package/packages/llm-billing/src/types.ts +67 -0
  68. package/packages/llm-billing/tests/balance.spec.ts +254 -0
  69. package/packages/llm-billing/tests/billing.spec.ts +216 -0
  70. package/packages/llm-billing/tsconfig.json +42 -0
  71. package/packages/ui-billing/README.i18n.yaml +6 -0
  72. package/packages/ui-billing/README.md +30 -0
  73. package/packages/ui-billing/README.zh.md +30 -0
  74. package/packages/ui-billing/lib/client.js +4466 -0
  75. package/packages/ui-billing/lib/client.js.map +1 -0
  76. package/packages/ui-billing/lib/index.js +11 -0
  77. package/packages/ui-billing/lib/invariant.js +25 -0
  78. package/packages/ui-billing/lib/tsconfig.tsbuildinfo +1 -0
  79. package/packages/ui-billing/lib/types/client/BalanceBadge.d.ts +33 -0
  80. package/packages/ui-billing/lib/types/client/BalanceBadge.d.ts.map +1 -0
  81. package/packages/ui-billing/lib/types/client/BalanceBadge.js +166 -0
  82. package/packages/ui-billing/lib/types/client/BalanceBadge.js.map +1 -0
  83. package/packages/ui-billing/lib/types/client/index.d.ts +20 -0
  84. package/packages/ui-billing/lib/types/client/index.d.ts.map +1 -0
  85. package/packages/ui-billing/lib/types/client/index.js +55 -0
  86. package/packages/ui-billing/lib/types/client/index.js.map +1 -0
  87. package/packages/ui-billing/lib/types/client/locales.d.ts +26 -0
  88. package/packages/ui-billing/lib/types/client/locales.d.ts.map +1 -0
  89. package/packages/ui-billing/lib/types/client/locales.js +40 -0
  90. package/packages/ui-billing/lib/types/client/locales.js.map +1 -0
  91. package/packages/ui-billing/lib/types/index.d.ts +9 -0
  92. package/packages/ui-billing/lib/types/index.d.ts.map +1 -0
  93. package/packages/ui-billing/lib/types/index.js +9 -0
  94. package/packages/ui-billing/lib/types/index.js.map +1 -0
  95. package/packages/ui-billing/lib/types/invariant.d.ts +16 -0
  96. package/packages/ui-billing/lib/types/invariant.d.ts.map +1 -0
  97. package/packages/ui-billing/lib/types/invariant.js +24 -0
  98. package/packages/ui-billing/lib/types/invariant.js.map +1 -0
  99. package/packages/ui-billing/node_modules/.bin/cordis +43 -0
  100. package/packages/ui-billing/node_modules/.bin/cordis.CMD +12 -0
  101. package/packages/ui-billing/node_modules/.bin/cordis.ps1 +41 -0
  102. package/packages/ui-billing/node_modules/.bin/vite +43 -0
  103. package/packages/ui-billing/node_modules/.bin/vite.CMD +12 -0
  104. package/packages/ui-billing/node_modules/.bin/vite.ps1 +41 -0
  105. package/packages/ui-billing/package.json +76 -0
  106. package/packages/ui-billing/src/client/BalanceBadge.module.css +184 -0
  107. package/packages/ui-billing/src/client/BalanceBadge.tsx +266 -0
  108. package/packages/ui-billing/src/client/index.ts +80 -0
  109. package/packages/ui-billing/src/client/locales.ts +45 -0
  110. package/packages/ui-billing/src/css-modules.d.ts +6 -0
  111. package/packages/ui-billing/src/index.ts +9 -0
  112. package/packages/ui-billing/src/invariant.ts +32 -0
  113. package/packages/ui-billing/tests/balance-badge.client.spec.tsx +267 -0
  114. package/packages/ui-billing/tests/browser-plugin.client.spec.ts +205 -0
  115. package/packages/ui-billing/tsconfig.json +36 -0
  116. package/packages/ui-billing/tsdown.config.ts +3 -0
@@ -0,0 +1,266 @@
1
+ /** Session-header billing badge: balance plus the current conversation's billed spend. */
2
+
3
+ import { Fragment, useEffect, useRef, useState } from 'react'
4
+ import type { DeepSeekBalance, DeepSeekSessionSpend, DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types'
5
+ import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
6
+ import { IconChevronDownOutline14, IconQuestionOutline14, IconRefreshOutline14, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
7
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
8
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
9
+ import { NS } from './locales.ts'
10
+ import css from './BalanceBadge.module.css'
11
+
12
+ /** Registration-side Remote face used by the header badge. */
13
+ export interface BalanceBadgeInjected {
14
+ /** Read the account balance; rejects with the Remote error message. */
15
+ getBalance: () => Promise<DeepSeekBalance>
16
+ /** Read one session's billed spend; rejects with the Remote error message. */
17
+ getSessionSpend: (sessionId: SessionId) => Promise<DeepSeekSessionSpend>
18
+ /** Read today's billed spend across every session; rejects with the Remote error message. */
19
+ getTodaySpend: () => Promise<DeepSeekTodaySpend>
20
+ }
21
+
22
+ /** Full props assembled by the header utilities slot renderer. */
23
+ export type BalanceBadgeProps =
24
+ PropsRuntime<'conversation.session.header.utilities'>
25
+ & InjectFace<BalanceBadgeInjected>
26
+ & PropsLocale<typeof NS>
27
+
28
+ /** Currency prefix for one balance line; unknown codes render as a literal prefix. */
29
+ function currencySymbol(currency: string): string {
30
+ if (currency === 'CNY') return '¥'
31
+ if (currency === 'USD') return '$'
32
+ return `${currency} `
33
+ }
34
+
35
+ /** The primary balance line, or undefined when the provider reports none. */
36
+ function primaryLine(balance: DeepSeekBalance): { symbol: string; total: string } | undefined {
37
+ const line = balance.lines[0]
38
+ if (line === undefined) return undefined
39
+ return { symbol: currencySymbol(line.currency), total: line.total }
40
+ }
41
+
42
+ /** CNY amount, up to four decimals with trailing zeros trimmed. */
43
+ function formatSpend(amount: number): string {
44
+ return `¥${amount.toFixed(4).replace(/\.?0+$/, '')}`
45
+ }
46
+
47
+ /**
48
+ * Render the billing badge in the session-header utilities row. The trigger
49
+ * shows the remaining balance and this conversation's billed spend and opens
50
+ * a label box with the amount, this session's spend with its cache-hit /
51
+ * cache-miss-input / output cost breakdown per model, today's spend across
52
+ * every session, a refresh action, and a spend disclaimer. Refreshing keeps
53
+ * the last values visible rather than blanking them.
54
+ *
55
+ * The spend follows the conversation: a new message in the current session
56
+ * recomputes only the (local, network-free) session spend and today's spend
57
+ * through `getSessionSpend` / `getTodaySpend`; the balance stays a
58
+ * manual-refresh snapshot and is never refetched on its own.
59
+ * @param props - Remote face, locale, and the standard session-header runtime share.
60
+ * @returns the badge, or null until the first balance fetch settles.
61
+ */
62
+ export function BalanceBadge({ getBalance, getSessionSpend, getTodaySpend, sessionId, useSession, t }: BalanceBadgeProps) {
63
+ const [balance, setBalance] = useState<DeepSeekBalance | null>(null)
64
+ const [spend, setSpend] = useState<DeepSeekSessionSpend | null>(null)
65
+ const [todaySpend, setTodaySpend] = useState<DeepSeekTodaySpend | null>(null)
66
+ const [error, setError] = useState<string | null>(null)
67
+ const [refreshing, setRefreshing] = useState(false)
68
+ const [open, setOpen] = useState(false)
69
+ const [request, setRequest] = useState(0)
70
+ const rootRef = useRef<HTMLDivElement>(null)
71
+
72
+ // The in-window message count is the "new message" signal: it changes when a
73
+ // message lands in the current session, letting the spend-only effect below
74
+ // react without touching the account balance.
75
+ const messageCount = useSession(snapshot => snapshot.chat.order.length)
76
+
77
+ // The last message count this component already priced, so the spend-only
78
+ // effect skips the initial mount (the mount effect already fetched).
79
+ const pricedMessageCountRef = useRef(messageCount)
80
+
81
+ useEffect(() => {
82
+ let current = true
83
+ // A refresh (values already present) keeps the previous values on screen;
84
+ // the first load has nothing to keep, so it stays on the loading render.
85
+ setRefreshing(balance !== null)
86
+ void Promise.resolve().then(() => {
87
+ // Each line settles on its own: the badge renders from the balance and
88
+ // the panel rows from their own spend, so a slow aggregate (today's
89
+ // spend scans every session) delays neither the balance nor the session
90
+ // spend.
91
+ const balanceRequest = Promise.resolve().then(getBalance).then(
92
+ (value) => {
93
+ if (!current) return
94
+ setBalance(value)
95
+ setError(null)
96
+ },
97
+ (reason: unknown) => {
98
+ if (!current) return
99
+ // A refresh failure keeps the last good value instead of blanking it.
100
+ if (balance === null) setError(reason instanceof Error ? reason.message : String(reason))
101
+ },
102
+ )
103
+ const sessionSpendRequest = Promise.resolve().then(() => getSessionSpend(sessionId)).then(
104
+ (value) => {
105
+ if (!current) return
106
+ setSpend(value)
107
+ },
108
+ () => {
109
+ // A failed refetch keeps the last good value instead of blanking it.
110
+ },
111
+ )
112
+ const todaySpendRequest = Promise.resolve().then(getTodaySpend).then(
113
+ (value) => {
114
+ if (!current) return
115
+ setTodaySpend(value)
116
+ },
117
+ () => {
118
+ // A failed refetch keeps the last good value instead of blanking it.
119
+ },
120
+ )
121
+ // The refresh spinner covers the whole refresh, whatever settles last.
122
+ void Promise.allSettled([balanceRequest, sessionSpendRequest, todaySpendRequest]).then(() => {
123
+ if (current) setRefreshing(false)
124
+ })
125
+ })
126
+ return () => { current = false }
127
+ }, [getBalance, getSessionSpend, getTodaySpend, sessionId, request])
128
+
129
+ // A new message lands: recompute this session's spend and today's spend
130
+ // across every session. The balance is account-level and stays a manual
131
+ // snapshot — never refetched here.
132
+ useEffect(() => {
133
+ if (messageCount === pricedMessageCountRef.current) return
134
+ pricedMessageCountRef.current = messageCount
135
+ let current = true
136
+ void Promise.resolve().then(() => {
137
+ // Each spend line updates on its own: the slow all-session aggregate
138
+ // does not delay the session line.
139
+ void Promise.resolve().then(() => getSessionSpend(sessionId)).then(
140
+ (value) => {
141
+ if (!current) return
142
+ setSpend(value)
143
+ },
144
+ () => {
145
+ // A failed recompute keeps the last good value.
146
+ },
147
+ )
148
+ void Promise.resolve().then(getTodaySpend).then(
149
+ (value) => {
150
+ if (!current) return
151
+ setTodaySpend(value)
152
+ },
153
+ () => {
154
+ // A failed recompute keeps the last good value.
155
+ },
156
+ )
157
+ })
158
+ return () => { current = false }
159
+ }, [getSessionSpend, getTodaySpend, sessionId, messageCount])
160
+
161
+ // A pointer press outside the label box closes it.
162
+ useEffect(() => {
163
+ if (!open) return
164
+ const closeOutside = (event: PointerEvent): void => {
165
+ if (event.target instanceof Node && !rootRef.current?.contains(event.target)) setOpen(false)
166
+ }
167
+ document.addEventListener('pointerdown', closeOutside)
168
+ return () => { document.removeEventListener('pointerdown', closeOutside) }
169
+ }, [open])
170
+
171
+ if (balance === null) {
172
+ if (error === null) return null
173
+ return (
174
+ <Tooltip label={error} delayMs={500}>
175
+ <button type="button" className={css.trigger} onClick={() => { setRequest(value => value + 1) }} aria-label={t('action.refresh')}>
176
+ <span className={css.unavailable}>{t('state.unavailable')}</span>
177
+ <IconRefreshOutline14 className={css.inlineIcon} />
178
+ </button>
179
+ </Tooltip>
180
+ )
181
+ }
182
+
183
+ const refresh = (): void => { setRequest(value => value + 1) }
184
+
185
+ const line = primaryLine(balance)
186
+ const amount = line === undefined ? '—' : `${line.symbol}${line.total}`
187
+ const spendLine = spend !== null && spend.models.length > 0
188
+ ? t('trigger.conversationSpend', { amount: formatSpend(spend.total) })
189
+ : undefined
190
+
191
+ return (
192
+ <div ref={rootRef} className={css.root}>
193
+ <button
194
+ type="button"
195
+ className={css.trigger}
196
+ aria-expanded={open}
197
+ aria-label={t('badge.aria', { amount })}
198
+ onClick={() => { setOpen(value => !value) }}
199
+ >
200
+ <span className={css.triggerLines}>
201
+ <span className={css.linePrimary}>{t('trigger.balance', { amount })}</span>
202
+ {spendLine !== undefined && <span className={css.lineSecondary}>{spendLine}</span>}
203
+ </span>
204
+ <IconChevronDownOutline14 className={open ? css.chevronOpen : undefined} />
205
+ </button>
206
+ {open
207
+ ? (
208
+ <div className={css.panel} role="dialog" aria-label={t('panel.aria')}>
209
+ <div className={css.amountRow}>
210
+ <span className={css.amountLabel}>{t('label.amount', { amount })}</span>
211
+ <span className={css.amountActions}>
212
+ <Tooltip label={t('info.hint')} delayMs={200} maxWidth={340}>
213
+ <button type="button" className={css.infoButton} aria-label={t('info.aria')}>
214
+ <IconQuestionOutline14 className={css.inlineIcon} />
215
+ </button>
216
+ </Tooltip>
217
+ <button
218
+ type="button"
219
+ className={css.refreshButton}
220
+ onClick={refresh}
221
+ aria-label={t('action.refresh')}
222
+ data-refreshing={refreshing || undefined}
223
+ >
224
+ <IconRefreshOutline14 className={refreshing ? css.spinning : css.inlineIcon} />
225
+ </button>
226
+ </span>
227
+ </div>
228
+ <div className={css.spendRow}>
229
+ <span className={css.amountLabel}>{t('label.sessionSpend', {
230
+ amount: spend === null
231
+ ? '—'
232
+ : spend.models.length === 0
233
+ ? t('stat.none')
234
+ : formatSpend(spend.total),
235
+ })}</span>
236
+ <span className={css.amountLabel}>{t('label.todaySpend', {
237
+ amount: todaySpend === null
238
+ ? '—'
239
+ : todaySpend.models.length === 0
240
+ ? t('stat.none')
241
+ : formatSpend(todaySpend.total),
242
+ })}</span>
243
+ </div>
244
+ {spend?.models.map(model => (
245
+ <Fragment key={model.model}>
246
+ <div className={css.modelRow}>
247
+ <span className={css.modelName}>{model.displayName}</span>
248
+ <span className={css.tasks}>{formatSpend(model.cost)}</span>
249
+ </div>
250
+ <div className={css.costRow}>
251
+ <span className={css.costBreakdown}>
252
+ {t('label.cost.hit', { amount: formatSpend(model.cacheHitInputCost) })}
253
+ {' · '}
254
+ {t('label.cost.input', { amount: formatSpend(model.cacheMissInputCost) })}
255
+ {' · '}
256
+ {t('label.cost.output', { amount: formatSpend(model.outputCost) })}
257
+ </span>
258
+ </div>
259
+ </Fragment>
260
+ ))}
261
+ </div>
262
+ )
263
+ : null}
264
+ </div>
265
+ )
266
+ }
@@ -0,0 +1,80 @@
1
+ /** DeepSeek account-balance badge, browser half: one session-header utility entry. */
2
+
3
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
4
+ import billingRemote from '@rayadesu/dsh-llm-billing/remote'
5
+ import type {} from '@rayadesu/dsh-llm-billing/remote'
6
+ import type { TypertRemoteNamespaceMap } from '@deepseek-ai/dsh-typert-protocol'
7
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
8
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
9
+ import { BalanceBadge, type BalanceBadgeInjected } from './BalanceBadge.tsx'
10
+ import { en, NS, zh, type BillingKey } from './locales.ts'
11
+
12
+ export type { BalanceBadgeInjected, BalanceBadgeProps } from './BalanceBadge.tsx'
13
+ export type { BillingKey } from './locales.ts'
14
+
15
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
16
+ interface LocaleNamespaceMap {
17
+ /** DeepSeek account-balance copy. */
18
+ 'billing': BillingKey
19
+ }
20
+ }
21
+
22
+ /** Services required for locale registration, the Remote face, and the header slot. */
23
+ export const inject = ['slots', 'locale', 'remote']
24
+
25
+ /** The mounted `billing` namespace surface, selected from the generated Remote map. */
26
+ type BillingNamespace = TypertRemoteNamespaceMap['billing']
27
+
28
+ /**
29
+ * Client plugin body: mount the `billing` Remote, register the dictionaries,
30
+ * and contribute the header utility badge.
31
+ * @param ctx - client root context.
32
+ */
33
+ export async function apply(ctx: ClientContext): Promise<void> {
34
+ // Mount the Remote owned by this plugin; the host half is `dsh-llm-billing`.
35
+ await ctx.remote.$mount(billingRemote)
36
+ // The mounted namespace is read through the explicit service lookup. The
37
+ // property access `ctx.remote.billing` would require `remote.billing` in this
38
+ // fiber's inject list, which deadlocks: this apply mounts that namespace, so
39
+ // it cannot also wait for it before running.
40
+ const billing = ctx.get('remote.billing') as BillingNamespace | undefined
41
+ if (billing === undefined) {
42
+ throw new Error('ui-billing: billing Remote namespace did not mount')
43
+ }
44
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-billing: dictionaries')
45
+
46
+ const injected = (): BalanceBadgeInjected => ({
47
+ getBalance: async () => {
48
+ const result = await billing.getBalance()
49
+ if (!result.ok) {
50
+ throw new Error(`billing.getBalance failed: ${result.error.code}: ${result.error.message}`)
51
+ }
52
+ return result.value
53
+ },
54
+ getSessionSpend: async (sessionId) => {
55
+ const result = await billing.getSessionSpend(sessionId)
56
+ if (!result.ok) {
57
+ throw new Error(`billing.getSessionSpend failed: ${result.error.code}: ${result.error.message}`)
58
+ }
59
+ return result.value
60
+ },
61
+ getTodaySpend: async () => {
62
+ const result = await billing.getTodaySpend()
63
+ if (!result.ok) {
64
+ throw new Error(`billing.getTodaySpend failed: ${result.error.code}: ${result.error.message}`)
65
+ }
66
+ return result.value
67
+ },
68
+ })
69
+
70
+ ctx.slots.inject(
71
+ 'conversation.session.header.utilities',
72
+ () => ctx.slots.register({
73
+ name: 'conversation.session.header.utilities',
74
+ id: 'billing-balance',
75
+ order: 30,
76
+ locale: NS,
77
+ inject: injected,
78
+ }, BalanceBadge),
79
+ )
80
+ }
@@ -0,0 +1,45 @@
1
+ /** `billing` namespace dictionaries. */
2
+
3
+ /** Dictionary namespace owned by this plugin. */
4
+ export const NS = 'billing'
5
+
6
+ /** Simplified Chinese dictionary (the key-set source of truth). */
7
+ export const zh = {
8
+ 'trigger.balance': '剩余额度:{amount}',
9
+ 'trigger.conversationSpend': '本轮对话花费:{amount}',
10
+ 'label.amount': 'API 剩余金额:{amount}',
11
+ 'label.sessionSpend': '本会话花费:{amount}',
12
+ 'label.todaySpend': '今日共花费:{amount}',
13
+ 'label.cost.hit': '缓存命中 {amount}',
14
+ 'label.cost.input': '未命中输入 {amount}',
15
+ 'label.cost.output': '输出 {amount}',
16
+ 'stat.none': '暂无消耗记录',
17
+ 'state.unavailable': '额度不可用',
18
+ 'action.refresh': '刷新',
19
+ 'info.aria': '花费说明',
20
+ 'info.hint': '仅能预估 DeepSeek 相关模型。本会话花费按每条消息的发生时刻(北京时间)所在峰谷时段单价计费:缓存命中输入、未命中输入(含缓存写入)、输出(含推理)分别计价。费用按 8 月 17 日实行的标准。',
21
+ 'badge.aria': 'DeepSeek 额度:{amount}',
22
+ 'panel.aria': 'DeepSeek 额度详情',
23
+ } as const
24
+
25
+ /** English dictionary, key-identical to the Chinese source of truth. */
26
+ export const en: Record<BillingKey, string> = {
27
+ 'trigger.balance': 'Balance: {amount}',
28
+ 'trigger.conversationSpend': 'This conversation: {amount}',
29
+ 'label.amount': 'API balance: {amount}',
30
+ 'label.sessionSpend': 'This session: {amount}',
31
+ 'label.todaySpend': 'Today total: {amount}',
32
+ 'label.cost.hit': 'Cache hit {amount}',
33
+ 'label.cost.input': 'Missed input {amount}',
34
+ 'label.cost.output': 'Output {amount}',
35
+ 'stat.none': 'No usage recorded',
36
+ 'state.unavailable': 'Balance unavailable',
37
+ 'action.refresh': 'Refresh',
38
+ 'info.aria': 'About this spend',
39
+ 'info.hint': "Only DeepSeek models are estimated. This session's spend is priced per message at the rate of its Beijing-time peak/off-peak hour: cache-hit input, cache-miss input (including cache writes), and output (including reasoning) are billed separately. Pricing follows the August 17 rates.",
40
+ 'badge.aria': 'DeepSeek balance {amount}',
41
+ 'panel.aria': 'DeepSeek balance details',
42
+ }
43
+
44
+ /** Key domain of the `billing` namespace (zh is the source of truth). */
45
+ export type BillingKey = keyof typeof zh
@@ -0,0 +1,6 @@
1
+ declare module '*.module.css' {
2
+ const classes: Record<string, string>
3
+ export default classes
4
+ }
5
+
6
+ declare module '*.css'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Account-balance badge plugin, node half. Pure UI plugin: the empty apply
3
+ * exists so the plugin appears in the host cordis.yml / Loader; the browser
4
+ * half ships via exports["./client"], discovered through the package.json
5
+ * dshClient declaration.
6
+ */
7
+
8
+ /** Host plugin body — no host-side behavior for this source plugin. */
9
+ export function apply(): void {}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Package-owned invariant companion for `@rayadesu/dsh-client-ui-billing`.
3
+ * @module @rayadesu/dsh-client-ui-billing/invariant
4
+ */
5
+
6
+ /* jscpd:ignore-start */
7
+ import type { Context } from '@deepseek-ai/cordis'
8
+ import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
9
+
10
+ const PACKAGE_NAME = '@rayadesu/dsh-client-ui-billing'
11
+
12
+ /** Cordis companion plugin name. */
13
+ export const name = 'client-ui-billing-invariant'
14
+ /** Service required before the companion can reserve package ownership. */
15
+ export const inject = ['invariants']
16
+
17
+ /**
18
+ * No runtime invariant: this package is a read-only projection of one Remote
19
+ * snapshot onto one header slot entry. It emits no cordis events, owns no
20
+ * cross-plugin mutable state, and its single slot registration proves disposal
21
+ * through the HMR-safety spec.
22
+ */
23
+ const install: InvariantInstaller = () => {}
24
+
25
+ /**
26
+ * Register this package's invariant companion.
27
+ * @param ctx - Cordis context carrying the invariant service.
28
+ * @returns the installed registration's disposer after setup succeeds.
29
+ */
30
+ export const apply = (ctx: Context): Promise<() => void> =>
31
+ Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
32
+ /* jscpd:ignore-end */