@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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session spend pricing: pricing resolution, peak-hour classification, and the
|
|
3
|
+
* per-session spend conversion. Pure functions only, so the suite is keyless
|
|
4
|
+
* and deterministic.
|
|
5
|
+
*/
|
|
6
|
+
import type { TokenUsage } from '@deepseek-ai/dsh-llm'
|
|
7
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
8
|
+
import { describe, expect, it } from 'vitest'
|
|
9
|
+
import {
|
|
10
|
+
computeSessionSpend,
|
|
11
|
+
computeTodaySpend,
|
|
12
|
+
isPeak,
|
|
13
|
+
resolveBilling,
|
|
14
|
+
} from '../src/billing.ts'
|
|
15
|
+
|
|
16
|
+
function assistantMessage(model: string, usage: TokenUsage, time = 0): SessionEvent {
|
|
17
|
+
return {
|
|
18
|
+
type: 'assistant/message',
|
|
19
|
+
seq: 0,
|
|
20
|
+
time,
|
|
21
|
+
data: {
|
|
22
|
+
turn: 0,
|
|
23
|
+
step: 0,
|
|
24
|
+
message: {
|
|
25
|
+
id: 'm' as never,
|
|
26
|
+
role: 'assistant',
|
|
27
|
+
content: [],
|
|
28
|
+
source: { kind: 'model', provider: 'deepseek-official', model },
|
|
29
|
+
},
|
|
30
|
+
usage,
|
|
31
|
+
},
|
|
32
|
+
} as unknown as SessionEvent
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FLASH = 'deepseek-v4-flash'
|
|
36
|
+
const PRO = 'deepseek-v4-pro'
|
|
37
|
+
const CATALOG = [
|
|
38
|
+
{ id: FLASH, name: 'DeepSeek-V4-Flash' },
|
|
39
|
+
{ id: PRO, name: 'DeepSeek-V4-Pro' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
describe('resolveBilling', () => {
|
|
43
|
+
it('uses the published defaults when no config is supplied', () => {
|
|
44
|
+
const billing = resolveBilling(undefined)
|
|
45
|
+
expect(billing.peakHours).toEqual([{ start: 9, end: 12 }, { start: 14, end: 18 }])
|
|
46
|
+
expect(billing.models.get(FLASH)?.peak).toEqual({ cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 })
|
|
47
|
+
expect(billing.models.get(PRO)?.offPeak).toEqual({ cacheHitInput: 0.15, cacheMissInput: 4.5, output: 13.5 })
|
|
48
|
+
// deepseek-v4-flash-vision-exp bills at the same published rates as flash.
|
|
49
|
+
expect(billing.models.get('deepseek-v4-flash-vision-exp')?.peak)
|
|
50
|
+
.toEqual({ cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 })
|
|
51
|
+
expect(billing.models.get('deepseek-v4-flash-vision-exp')?.offPeak)
|
|
52
|
+
.toEqual({ cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('overrides a model when an explicit row is supplied', () => {
|
|
56
|
+
const billing = resolveBilling({
|
|
57
|
+
models: [{
|
|
58
|
+
model: FLASH,
|
|
59
|
+
peak: { cacheHitInput: 1, cacheMissInput: 2, output: 3 },
|
|
60
|
+
offPeak: { cacheHitInput: 0.5, cacheMissInput: 1, output: 1.5 },
|
|
61
|
+
}],
|
|
62
|
+
})
|
|
63
|
+
expect(billing.models.get(FLASH)?.peak).toEqual({ cacheHitInput: 1, cacheMissInput: 2, output: 3 })
|
|
64
|
+
expect(billing.models.has(PRO)).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('falls back to the defaults for empty arrays (schemastery materializes absent z.array as [])', () => {
|
|
68
|
+
const billing = resolveBilling({ models: [], peakHours: [] })
|
|
69
|
+
expect(billing.models.get(FLASH)?.peak).toEqual({ cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 })
|
|
70
|
+
expect(billing.models.get(PRO)?.offPeak).toEqual({ cacheHitInput: 0.15, cacheMissInput: 4.5, output: 13.5 })
|
|
71
|
+
expect(billing.peakHours).toEqual([{ start: 9, end: 12 }, { start: 14, end: 18 }])
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('isPeak', () => {
|
|
76
|
+
it('classifies Beijing daytime hours as peak and others as off-peak', () => {
|
|
77
|
+
const billing = resolveBilling(undefined)
|
|
78
|
+
// 10:00 Beijing = 02:00 UTC; 20:00 Beijing = 12:00 UTC.
|
|
79
|
+
expect(isPeak(billing, new Date('2026-08-20T02:00:00Z'))).toBe(true)
|
|
80
|
+
expect(isPeak(billing, new Date('2026-08-20T12:00:00Z'))).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('computeSessionSpend', () => {
|
|
85
|
+
// 02:00Z is 10:00 Beijing (peak); 12:00Z is 20:00 Beijing (off-peak).
|
|
86
|
+
const PEAK = Date.parse('2026-08-20T02:00:00Z')
|
|
87
|
+
const OFF_PEAK = Date.parse('2026-08-20T12:00:00Z')
|
|
88
|
+
// Flash peak: hit 0.10, miss 3.0, output 9.0 CNY/M; off-peak: 0.05, 1.5, 4.5.
|
|
89
|
+
const USAGE: TokenUsage = { inputTokens: 1_000_000, outputTokens: 1_000_000, cacheReadTokens: 1_000_000, cacheWriteTokens: 500_000 }
|
|
90
|
+
|
|
91
|
+
it('prices cache hit, cache miss (including writes), and output at the peak rate for a peak-hour event', () => {
|
|
92
|
+
const spend = computeSessionSpend([assistantMessage(FLASH, USAGE, PEAK)], resolveBilling(undefined), CATALOG)
|
|
93
|
+
expect(spend.total).toBeCloseTo(0.10 + 4.50 + 9.00, 10)
|
|
94
|
+
expect(spend.models).toHaveLength(1)
|
|
95
|
+
const row = spend.models[0]
|
|
96
|
+
expect(row?.displayName).toBe('DeepSeek-V4-Flash')
|
|
97
|
+
expect(row?.cost).toBeCloseTo(13.60, 10)
|
|
98
|
+
expect(row?.peakCost).toBeCloseTo(13.60, 10)
|
|
99
|
+
expect(row?.offPeakCost).toBe(0)
|
|
100
|
+
expect(row?.cacheHitInputTokens).toBe(1_000_000)
|
|
101
|
+
expect(row?.cacheMissInputTokens).toBe(1_500_000)
|
|
102
|
+
expect(row?.outputTokens).toBe(1_000_000)
|
|
103
|
+
expect(row?.cacheHitInputCost).toBeCloseTo(0.10, 10)
|
|
104
|
+
expect(row?.cacheMissInputCost).toBeCloseTo(4.50, 10)
|
|
105
|
+
expect(row?.outputCost).toBeCloseTo(9.00, 10)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('prices the same usage at the off-peak rate for an off-peak-hour event', () => {
|
|
109
|
+
const spend = computeSessionSpend([assistantMessage(FLASH, USAGE, OFF_PEAK)], resolveBilling(undefined), CATALOG)
|
|
110
|
+
expect(spend.total).toBeCloseTo(0.05 + 2.25 + 4.50, 10)
|
|
111
|
+
expect(spend.models[0]?.offPeakCost).toBeCloseTo(6.80, 10)
|
|
112
|
+
expect(spend.models[0]?.peakCost).toBe(0)
|
|
113
|
+
expect(spend.models[0]?.cacheHitInputCost).toBeCloseTo(0.05, 10)
|
|
114
|
+
expect(spend.models[0]?.cacheMissInputCost).toBeCloseTo(2.25, 10)
|
|
115
|
+
expect(spend.models[0]?.outputCost).toBeCloseTo(4.50, 10)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('splits peak and off-peak portions across mixed-hour events', () => {
|
|
119
|
+
const spend = computeSessionSpend(
|
|
120
|
+
[assistantMessage(FLASH, USAGE, PEAK), assistantMessage(FLASH, USAGE, OFF_PEAK)],
|
|
121
|
+
resolveBilling(undefined),
|
|
122
|
+
CATALOG,
|
|
123
|
+
)
|
|
124
|
+
expect(spend.total).toBeCloseTo(13.60 + 6.80, 10)
|
|
125
|
+
expect(spend.models[0]?.peakCost).toBeCloseTo(13.60, 10)
|
|
126
|
+
expect(spend.models[0]?.offPeakCost).toBeCloseTo(6.80, 10)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('omits a model that reported usage but has no pricing row', () => {
|
|
130
|
+
const spend = computeSessionSpend(
|
|
131
|
+
[assistantMessage('other-model', USAGE, PEAK)],
|
|
132
|
+
resolveBilling(undefined),
|
|
133
|
+
CATALOG,
|
|
134
|
+
)
|
|
135
|
+
expect(spend.total).toBe(0)
|
|
136
|
+
expect(spend.models).toEqual([])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('returns a zero total for a session without billed usage', () => {
|
|
140
|
+
const spend = computeSessionSpend(
|
|
141
|
+
[{ type: 'turn/start', seq: 0, time: PEAK, data: { turn: 0 } }],
|
|
142
|
+
resolveBilling(undefined),
|
|
143
|
+
CATALOG,
|
|
144
|
+
)
|
|
145
|
+
expect(spend.total).toBe(0)
|
|
146
|
+
expect(spend.models).toEqual([])
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('computeTodaySpend', () => {
|
|
151
|
+
// 2026-08-20 04:00Z is 2026-08-20 12:00 Beijing (off-peak; day 2026-08-20).
|
|
152
|
+
const NOW = Date.parse('2026-08-20T04:00:00Z')
|
|
153
|
+
// 02:00Z is 10:00 Beijing (peak), same Beijing day as NOW.
|
|
154
|
+
const PEAK = Date.parse('2026-08-20T02:00:00Z')
|
|
155
|
+
// 12:00Z is 20:00 Beijing (off-peak), same Beijing day as NOW.
|
|
156
|
+
const OFF_PEAK = Date.parse('2026-08-20T12:00:00Z')
|
|
157
|
+
// 16:30Z is 2026-08-21 00:30 Beijing — a different Beijing calendar day.
|
|
158
|
+
const NEXT_DAY = Date.parse('2026-08-20T16:30:00Z')
|
|
159
|
+
// 2026-08-19 20:00Z is 2026-08-20 04:00 Beijing — the previous UTC day but
|
|
160
|
+
// the same Beijing calendar day as NOW.
|
|
161
|
+
const PREVIOUS_UTC_DAY = Date.parse('2026-08-19T20:00:00Z')
|
|
162
|
+
const USAGE: TokenUsage = { inputTokens: 1_000_000, outputTokens: 1_000_000, cacheReadTokens: 1_000_000, cacheWriteTokens: 500_000 }
|
|
163
|
+
|
|
164
|
+
it('prices every priced event on the reference Beijing calendar day, from any session', () => {
|
|
165
|
+
const spend = computeTodaySpend(
|
|
166
|
+
[
|
|
167
|
+
assistantMessage(FLASH, USAGE, PEAK),
|
|
168
|
+
assistantMessage(FLASH, USAGE, OFF_PEAK),
|
|
169
|
+
assistantMessage(PRO, USAGE, PREVIOUS_UTC_DAY),
|
|
170
|
+
],
|
|
171
|
+
resolveBilling(undefined),
|
|
172
|
+
CATALOG,
|
|
173
|
+
new Date(NOW),
|
|
174
|
+
)
|
|
175
|
+
// Flash peak 13.60 + Flash off-peak 6.80 + Pro off-peak 20.40.
|
|
176
|
+
expect(spend.total).toBeCloseTo(13.60 + 6.80 + 20.40, 10)
|
|
177
|
+
expect(spend.models).toHaveLength(2)
|
|
178
|
+
const flash = spend.models.find(row => row.model === FLASH)
|
|
179
|
+
expect(flash?.peakCost).toBeCloseTo(13.60, 10)
|
|
180
|
+
expect(flash?.offPeakCost).toBeCloseTo(6.80, 10)
|
|
181
|
+
const pro = spend.models.find(row => row.model === PRO)
|
|
182
|
+
expect(pro?.cost).toBeCloseTo(20.40, 10)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('ignores events on other Beijing calendar days', () => {
|
|
186
|
+
const spend = computeTodaySpend(
|
|
187
|
+
[
|
|
188
|
+
assistantMessage(FLASH, USAGE, PEAK),
|
|
189
|
+
// 00:30 Beijing the next day — not today.
|
|
190
|
+
assistantMessage(FLASH, USAGE, NEXT_DAY),
|
|
191
|
+
],
|
|
192
|
+
resolveBilling(undefined),
|
|
193
|
+
CATALOG,
|
|
194
|
+
new Date(NOW),
|
|
195
|
+
)
|
|
196
|
+
expect(spend.total).toBeCloseTo(13.60, 10)
|
|
197
|
+
expect(spend.models[0]?.cost).toBeCloseTo(13.60, 10)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('returns a zero total when nothing priced falls on the reference day', () => {
|
|
201
|
+
const spend = computeTodaySpend(
|
|
202
|
+
[assistantMessage(FLASH, USAGE, NEXT_DAY)],
|
|
203
|
+
resolveBilling(undefined),
|
|
204
|
+
CATALOG,
|
|
205
|
+
new Date(NOW),
|
|
206
|
+
)
|
|
207
|
+
expect(spend.total).toBe(0)
|
|
208
|
+
expect(spend.models).toEqual([])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('defaults the reference moment to now', () => {
|
|
212
|
+
const spend = computeTodaySpend([], resolveBilling(undefined), CATALOG)
|
|
213
|
+
expect(spend.total).toBe(0)
|
|
214
|
+
expect(spend.models).toEqual([])
|
|
215
|
+
})
|
|
216
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "lib/types"
|
|
6
|
+
},
|
|
7
|
+
"include": [
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"references": [
|
|
11
|
+
{
|
|
12
|
+
"path": "../../../vendor/cosmokit"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "../../../vendor/cordis"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "../../../vendor/schemastery"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "../../llm/llm"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "../../credentials/credentials"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "../../util/launch-environment"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"path": "../../core/session"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "../../session/session-persistence"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"path": "../../runtime-diagnostics/invariants"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "../../typert/protocol"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record: the git blob hash of each side as of the
|
|
2
|
+
# last confirmed-consistent state. Both languages carry equal authority; after
|
|
3
|
+
# editing either side, bring the other along and re-record both hashes with:
|
|
4
|
+
# git hash-object README.md README.zh.md
|
|
5
|
+
README.md: 509fb81169ba38ef35a3a364eb88765456143443
|
|
6
|
+
README.zh.md: 19fa2ed3c3f4692c60945c614c44c5b832075100
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @rayadesu/dsh-client-ui-billing
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Web billing feature owner: contributes one entry to `conversation.session.header.utilities` that mounts its own `billing` Remote, reads the DeepSeek account balance, this session's billed spend, and today's spend across every session through `billing/getBalance`, `billing/getSessionSpend`, and `billing/getTodaySpend`, and renders them as a top-right label box. The host half of the capability lives in [`dsh-llm-billing`](../llm-billing/README.md), which owns the `/user/balance` transport, the peak/off-peak pricing table, and the `billing` Remote namespace; this package mounts that Remote and renders what it returns.
|
|
6
|
+
|
|
7
|
+
The trigger shows two lines — the remaining balance and this conversation's billed spend ("本轮对话花费 ¥X") — and renders nothing while the first fetch is in flight. Clicking it opens a label box with the remaining amount, this session's billed spend (本会话花费, priced per message at the official peak/off-peak rate) with today's all-session spend beside it (今日共花费), one priced row per model with the cache-hit / cache-miss-input / output cost breakdown ("缓存命中 ¥X · 未命中输入 ¥Y · 输出 ¥Z"), a manual refresh action, and a spend disclaimer. Refreshing keeps the last values visible rather than blanking them, and a refresh failure retains the last good value. A session or day without priced usage shows a "no usage recorded" word rather than a fabricated figure. A failure — no API key configured, a rejected credential, a transport error — renders a muted "Balance unavailable" word whose tooltip carries the Remote's own error message.
|
|
8
|
+
|
|
9
|
+
The badge is account-level even though the slot is session-scoped: the header utilities row is simply the one always-visible title-bar seat the shell offers, and the balance it shows does not vary by session — but the spend lines do, because they price the current session's own token usage through `billing/getSessionSpend` and every session's usage through `billing/getTodaySpend`. Copy goes through the package's own `billing` locale namespace; styling uses tokens only.
|
|
10
|
+
|
|
11
|
+
## Refresh mechanics
|
|
12
|
+
|
|
13
|
+
The spends follow the conversation and the balance stays a manual snapshot:
|
|
14
|
+
|
|
15
|
+
- **Spends follow new messages** — the component subscribes to the current session's message count through the framework `useSession` seat. When a new message lands, it recomputes this session's spend (`billing/getSessionSpend`) and today's spend (`billing/getTodaySpend`), local pricing passes with no network request, so the spend lines stay live during an ongoing conversation.
|
|
16
|
+
- **Balance is manual** — the account balance is fetched on mount, on session switch, and on the explicit refresh action (`billing/getBalance`). There is no polling and no automatic refetch: the balance line only changes when one of those events happens.
|
|
17
|
+
- **Refresh keeps the last values** — an in-flight refresh leaves the previous values on screen, and a failed refresh retains the last good value instead of blanking it.
|
|
18
|
+
|
|
19
|
+
## Model Experience
|
|
20
|
+
|
|
21
|
+
None, as this package renders account-level provider facts for a human and touches no prompt, message, schema, stream, or tool result. The model's own view of provider quota lives with the `QUOTA` / `INVALID_CREDENTIAL` error codes surfaced by [`dsh-llm`](https://github.com/deepseek-ai/deepseek-harness/tree/main/packages/llm/llm) on failed requests.
|
|
22
|
+
|
|
23
|
+
#### KV Cache effect
|
|
24
|
+
|
|
25
|
+
None; the package never assembles or sends provider requests, and its one RPC is a credential-authenticated account read that is not cached in the provider KV store.
|
|
26
|
+
|
|
27
|
+
## Known Limitations and Deferred Work
|
|
28
|
+
|
|
29
|
+
- **First balance line only** — the balance reads the primary (`balance_infos[0]`) currency line; other currency lines are not shown.
|
|
30
|
+
- **Balance does not follow automatically** — the balance is a manual snapshot (no polling): spending from another client does not move the shown value until a refresh or browser reload.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @rayadesu/dsh-client-ui-billing
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡献一个条目,自己挂载 `billing` Remote,通过 `billing/getBalance`、`billing/getSessionSpend` 与 `billing/getTodaySpend` 读取 DeepSeek 账户余额、本会话的计费花费与今日所有会话的计费花费,并在右上角渲染成一个标签框。该能力的宿主侧在 [`dsh-llm-billing`](../llm-billing/README.md),它拥有 `/user/balance` 传输、峰谷计价表与 `billing` Remote 命名空间;本包挂载那个 Remote 并渲染它返回的内容。
|
|
6
|
+
|
|
7
|
+
触发器显示两行——剩余额度与本轮对话的计费花费(「本轮对话花费 ¥X」)——首个请求在途时不渲染任何东西。点击后展开一个标签框:剩余金额、本会话的计费花费(本会话花费,按官方峰/谷单价逐条消息计价,右侧并列今日所有会话的合计「今日共花费」)、每个模型一行的花费及其缓存命中/未命中输入/输出分项(「缓存命中 ¥X · 未命中输入 ¥Y · 输出 ¥Z」)、手动刷新按钮与花费说明。刷新时旧值不消失,刷新失败保留上一次有效值。没有可计价消耗的会话或日子显示「暂无消耗记录」而不是编造数字。失败——未配置 API key、凭据被拒绝、传输错误——渲染弱化的「额度不可用」,其提示携带 Remote 自己的错误信息。
|
|
8
|
+
|
|
9
|
+
角标是账户级的,尽管插槽是会话作用域:顶栏工具行不过是外壳提供的那个始终可见的标题栏席位,余额并不随会话变化——但花费行会,因为它通过 `billing/getSessionSpend` 按当前会话自己的 token 用量计价、通过 `billing/getTodaySpend` 按所有会话的用量计价。文案走本包自己的 `billing` locale 命名空间;样式只用 token。
|
|
10
|
+
|
|
11
|
+
## 数据更新机制
|
|
12
|
+
|
|
13
|
+
花费跟随会话、额度保持手动快照:
|
|
14
|
+
|
|
15
|
+
- **花费随新消息自动更新** —— 组件通过框架 `useSession` 座位订阅当前会话的消息数。每到达一条新消息,就只重算**本会话的花费**(`billing/getSessionSpend`)与**今日共花费**(`billing/getTodaySpend`,纯本地计价、不发网络请求),连续对话时花费行实时跟随。
|
|
16
|
+
- **额度保持手动** —— 账户余额在挂载、切换会话、点刷新按钮时拉取(`billing/getBalance`)。**没有轮询、不会自动重拉**:余额行只在这几个时机变化。
|
|
17
|
+
- **刷新保留旧值** —— 刷新在途时旧值留在屏幕上,刷新失败保留上一次有效值,不清空。
|
|
18
|
+
|
|
19
|
+
## 模型体验
|
|
20
|
+
|
|
21
|
+
无,因为本包为人类渲染账户级的 provider 事实,不触及 prompt、消息、schema、流或工具结果。模型对 provider 额度的视角仍在 [`dsh-llm`](https://github.com/deepseek-ai/deepseek-harness/tree/main/packages/llm/llm) 在请求失败时抛出的 `QUOTA` / `INVALID_CREDENTIAL` 错误码里。
|
|
22
|
+
|
|
23
|
+
#### KV Cache effect
|
|
24
|
+
|
|
25
|
+
无;本包从不组装或发送 provider 请求,它唯一的 RPC 是一次带凭据的账户读取,不落在 provider 的 KV 缓存里。
|
|
26
|
+
|
|
27
|
+
## 已知限制与暂缓事项
|
|
28
|
+
|
|
29
|
+
- **只取首条余额线** —— 余额读取主要(`balance_infos[0]`)币种行;其它币种行不显示。
|
|
30
|
+
- **额度不自动跟随** —— 余额保持手动快照(无轮询):其他客户端产生的消耗不会让界面数值自动变化,需手动刷新或刷新浏览器。
|