@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.
- package/AGENTS.md +54 -0
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +118 -0
- package/README.zh.md +114 -0
- package/cordis.patch.yml +20 -0
- package/package.json +33 -0
- package/packages/llm-billing/README.i18n.yaml +6 -0
- package/packages/llm-billing/README.md +44 -0
- package/packages/llm-billing/README.zh.md +44 -0
- package/packages/llm-billing/lib/index.js +562 -0
- package/packages/llm-billing/lib/invariant.js +23 -0
- package/packages/llm-billing/lib/tsconfig.tsbuildinfo +1 -0
- package/packages/llm-billing/lib/typert.host.d.ts +3 -0
- package/packages/llm-billing/lib/typert.host.js +114 -0
- package/packages/llm-billing/lib/typert.remote-client.d.ts +27 -0
- package/packages/llm-billing/lib/typert.remote-client.d.ts.map +1 -0
- package/packages/llm-billing/lib/typert.remote-client.js +108 -0
- package/packages/llm-billing/lib/types/balance.d.ts +71 -0
- package/packages/llm-billing/lib/types/balance.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/balance.js +187 -0
- package/packages/llm-billing/lib/types/balance.js.map +1 -0
- package/packages/llm-billing/lib/types/billing.d.ts +106 -0
- package/packages/llm-billing/lib/types/billing.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/billing.js +163 -0
- package/packages/llm-billing/lib/types/billing.js.map +1 -0
- package/packages/llm-billing/lib/types/index.d.ts +49 -0
- package/packages/llm-billing/lib/types/index.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/index.js +155 -0
- package/packages/llm-billing/lib/types/index.js.map +1 -0
- package/packages/llm-billing/lib/types/invariant.d.ts +16 -0
- package/packages/llm-billing/lib/types/invariant.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/invariant.js +22 -0
- package/packages/llm-billing/lib/types/invariant.js.map +1 -0
- package/packages/llm-billing/lib/types/types/balance.d.ts +71 -0
- package/packages/llm-billing/lib/types/types/balance.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/types/balance.js +187 -0
- package/packages/llm-billing/lib/types/types/balance.js.map +1 -0
- package/packages/llm-billing/lib/types/types/billing.d.ts +106 -0
- package/packages/llm-billing/lib/types/types/billing.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/types/billing.js +170 -0
- package/packages/llm-billing/lib/types/types/billing.js.map +1 -0
- package/packages/llm-billing/lib/types/types/index.d.ts +49 -0
- package/packages/llm-billing/lib/types/types/index.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/types/index.js +156 -0
- package/packages/llm-billing/lib/types/types/index.js.map +1 -0
- package/packages/llm-billing/lib/types/types/invariant.d.ts +16 -0
- package/packages/llm-billing/lib/types/types/invariant.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/types/invariant.js +22 -0
- package/packages/llm-billing/lib/types/types/invariant.js.map +1 -0
- package/packages/llm-billing/lib/types/types/types.d.ts +63 -0
- package/packages/llm-billing/lib/types/types/types.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/types/types.js +7 -0
- package/packages/llm-billing/lib/types/types/types.js.map +1 -0
- package/packages/llm-billing/lib/types/types.d.ts +63 -0
- package/packages/llm-billing/lib/types/types.d.ts.map +1 -0
- package/packages/llm-billing/lib/types/types.js +7 -0
- package/packages/llm-billing/lib/types/types.js.map +1 -0
- package/packages/llm-billing/node_modules/.bin/cordis +43 -0
- package/packages/llm-billing/node_modules/.bin/cordis.CMD +12 -0
- package/packages/llm-billing/node_modules/.bin/cordis.ps1 +41 -0
- package/packages/llm-billing/package.json +75 -0
- package/packages/llm-billing/src/balance.ts +164 -0
- package/packages/llm-billing/src/billing.ts +251 -0
- package/packages/llm-billing/src/index.ts +219 -0
- package/packages/llm-billing/src/invariant.ts +30 -0
- package/packages/llm-billing/src/types.ts +67 -0
- package/packages/llm-billing/tests/balance.spec.ts +254 -0
- package/packages/llm-billing/tests/billing.spec.ts +216 -0
- package/packages/llm-billing/tsconfig.json +42 -0
- package/packages/ui-billing/README.i18n.yaml +6 -0
- package/packages/ui-billing/README.md +30 -0
- package/packages/ui-billing/README.zh.md +30 -0
- package/packages/ui-billing/lib/client.js +4466 -0
- package/packages/ui-billing/lib/client.js.map +1 -0
- package/packages/ui-billing/lib/index.js +11 -0
- package/packages/ui-billing/lib/invariant.js +25 -0
- package/packages/ui-billing/lib/tsconfig.tsbuildinfo +1 -0
- package/packages/ui-billing/lib/types/client/BalanceBadge.d.ts +33 -0
- package/packages/ui-billing/lib/types/client/BalanceBadge.d.ts.map +1 -0
- package/packages/ui-billing/lib/types/client/BalanceBadge.js +166 -0
- package/packages/ui-billing/lib/types/client/BalanceBadge.js.map +1 -0
- package/packages/ui-billing/lib/types/client/index.d.ts +20 -0
- package/packages/ui-billing/lib/types/client/index.d.ts.map +1 -0
- package/packages/ui-billing/lib/types/client/index.js +55 -0
- package/packages/ui-billing/lib/types/client/index.js.map +1 -0
- package/packages/ui-billing/lib/types/client/locales.d.ts +26 -0
- package/packages/ui-billing/lib/types/client/locales.d.ts.map +1 -0
- package/packages/ui-billing/lib/types/client/locales.js +40 -0
- package/packages/ui-billing/lib/types/client/locales.js.map +1 -0
- package/packages/ui-billing/lib/types/index.d.ts +9 -0
- package/packages/ui-billing/lib/types/index.d.ts.map +1 -0
- package/packages/ui-billing/lib/types/index.js +9 -0
- package/packages/ui-billing/lib/types/index.js.map +1 -0
- package/packages/ui-billing/lib/types/invariant.d.ts +16 -0
- package/packages/ui-billing/lib/types/invariant.d.ts.map +1 -0
- package/packages/ui-billing/lib/types/invariant.js +24 -0
- package/packages/ui-billing/lib/types/invariant.js.map +1 -0
- package/packages/ui-billing/node_modules/.bin/cordis +43 -0
- package/packages/ui-billing/node_modules/.bin/cordis.CMD +12 -0
- package/packages/ui-billing/node_modules/.bin/cordis.ps1 +41 -0
- package/packages/ui-billing/node_modules/.bin/vite +43 -0
- package/packages/ui-billing/node_modules/.bin/vite.CMD +12 -0
- package/packages/ui-billing/node_modules/.bin/vite.ps1 +41 -0
- package/packages/ui-billing/package.json +76 -0
- package/packages/ui-billing/src/client/BalanceBadge.module.css +184 -0
- package/packages/ui-billing/src/client/BalanceBadge.tsx +266 -0
- package/packages/ui-billing/src/client/index.ts +80 -0
- package/packages/ui-billing/src/client/locales.ts +45 -0
- package/packages/ui-billing/src/css-modules.d.ts +6 -0
- package/packages/ui-billing/src/index.ts +9 -0
- package/packages/ui-billing/src/invariant.ts +32 -0
- package/packages/ui-billing/tests/balance-badge.client.spec.tsx +267 -0
- package/packages/ui-billing/tests/browser-plugin.client.spec.ts +205 -0
- package/packages/ui-billing/tsconfig.json +36 -0
- 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,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 */
|