@rayadesu/dsh-llm-billing 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  /* Generated by @deepseek-ai/dsh-typert-generator from FaceModel — do not edit. */
2
2
  import { z } from 'zod'
3
3
 
4
- const _deepseek_ai_dsh_llm_billing_billing_getBalance_result$schema = z.object({
4
+ const _rayadesu_dsh_llm_billing_billing_getBalance_result$schema = z.object({
5
5
  'isAvailable': z.boolean(),
6
6
  'lines': z.array(z.object({
7
7
  'currency': z.string(),
@@ -10,8 +10,8 @@ const _deepseek_ai_dsh_llm_billing_billing_getBalance_result$schema = z.object({
10
10
  'toppedUp': z.string(),
11
11
  })),
12
12
  })
13
- const _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema = z.intersection(z.string(), z.unknown())
14
- const _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_result$schema = z.object({
13
+ const _rayadesu_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema = z.intersection(z.string(), z.unknown())
14
+ const _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema = z.object({
15
15
  'total': z.number(),
16
16
  'models': z.array(z.object({
17
17
  'model': z.string(),
@@ -27,7 +27,8 @@ const _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_result$schema = z.obj
27
27
  'outputCost': z.number(),
28
28
  })),
29
29
  })
30
- const _deepseek_ai_dsh_llm_billing_billing_getTodaySpend_result$schema = z.object({
30
+ const _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema = z.union([z.undefined(), z.literal(false), z.literal(true)])
31
+ const _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema = z.object({
31
32
  'total': z.number(),
32
33
  'models': z.array(z.object({
33
34
  'model': z.string(),
@@ -61,9 +62,9 @@ export const TYPERT = {
61
62
  result: {
62
63
  mode: 'strict',
63
64
  typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekBalance',
64
- schema: _deepseek_ai_dsh_llm_billing_billing_getBalance_result$schema,
65
+ schema: _rayadesu_dsh_llm_billing_billing_getBalance_result$schema,
65
66
  },
66
- sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":139,"column":3},
67
+ sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":143,"column":3},
67
68
  },
68
69
  {
69
70
  id: '@rayadesu/dsh-llm-billing#billing/getSessionSpend',
@@ -79,16 +80,16 @@ export const TYPERT = {
79
80
  codec: {
80
81
  mode: 'strict',
81
82
  typeSymbol: '@deepseek-ai/dsh-session/types#SessionId',
82
- schema: _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema,
83
+ schema: _rayadesu_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema,
83
84
  },
84
85
  },
85
86
  ],
86
87
  result: {
87
88
  mode: 'strict',
88
89
  typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekSessionSpend',
89
- schema: _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_result$schema,
90
+ schema: _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema,
90
91
  },
91
- sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":149,"column":3},
92
+ sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":154,"column":3},
92
93
  },
93
94
  {
94
95
  id: '@rayadesu/dsh-llm-billing#billing/getTodaySpend',
@@ -97,13 +98,24 @@ export const TYPERT = {
97
98
  method: 'getTodaySpend',
98
99
  invocation: { kind: 'direct' },
99
100
  parameters: [
101
+ {
102
+ name: 'force',
103
+ wire: 'force',
104
+ source: 'json',
105
+ acceptsUndefined: true,
106
+ codec: {
107
+ mode: 'strict',
108
+ typeSymbol: '@rayadesu/dsh-llm-billing#billing/getTodaySpend:force',
109
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema,
110
+ },
111
+ },
100
112
  ],
101
113
  result: {
102
114
  mode: 'strict',
103
115
  typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekTodaySpend',
104
- schema: _deepseek_ai_dsh_llm_billing_billing_getTodaySpend_result$schema,
116
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema,
105
117
  },
106
- sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":159,"column":3},
118
+ sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":167,"column":3},
107
119
  },
108
120
  ],
109
121
  model: {
@@ -3,19 +3,19 @@ import type {
3
3
  RemoteResult,
4
4
  TypertRemoteContribution,
5
5
  } from '@deepseek-ai/dsh-typert-protocol'
6
- import type { DeepSeekBalance, DeepSeekSessionSpend, DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types'
7
6
  import type { SessionId } from '@deepseek-ai/dsh-session/types'
7
+ import type { DeepSeekBalance, DeepSeekSessionSpend, DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types'
8
8
 
9
9
  declare module '@deepseek-ai/dsh-typert-protocol' {
10
10
  interface TypertRemoteNamespace$62696c6c696e67 {
11
11
  getBalance: () => Promise<RemoteResult<DeepSeekBalance>>
12
12
  getSessionSpend: (sessionId: SessionId) => Promise<RemoteResult<DeepSeekSessionSpend>>
13
- getTodaySpend: () => Promise<RemoteResult<DeepSeekTodaySpend>>
13
+ getTodaySpend: (force?: boolean) => Promise<RemoteResult<DeepSeekTodaySpend>>
14
14
  }
15
15
  interface TypertRemoteMap {
16
16
  'billing/getBalance': () => Promise<RemoteResult<DeepSeekBalance>>
17
17
  'billing/getSessionSpend': (sessionId: SessionId) => Promise<RemoteResult<DeepSeekSessionSpend>>
18
- 'billing/getTodaySpend': () => Promise<RemoteResult<DeepSeekTodaySpend>>
18
+ 'billing/getTodaySpend': (force?: boolean) => Promise<RemoteResult<DeepSeekTodaySpend>>
19
19
  }
20
20
  interface TypertRemoteNamespaceMap {
21
21
  'billing': TypertRemoteNamespace$62696c6c696e67
@@ -1,7 +1,7 @@
1
1
  /* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */
2
2
  import { z } from 'zod'
3
3
 
4
- const _deepseek_ai_dsh_llm_billing_billing_getBalance_result$schema = z.object({
4
+ const _rayadesu_dsh_llm_billing_billing_getBalance_result$schema = z.object({
5
5
  'isAvailable': z.boolean(),
6
6
  'lines': z.array(z.object({
7
7
  'currency': z.string(),
@@ -10,8 +10,8 @@ const _deepseek_ai_dsh_llm_billing_billing_getBalance_result$schema = z.object({
10
10
  'toppedUp': z.string(),
11
11
  })),
12
12
  })
13
- const _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema = z.intersection(z.string(), z.unknown())
14
- const _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_result$schema = z.object({
13
+ const _rayadesu_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema = z.intersection(z.string(), z.unknown())
14
+ const _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema = z.object({
15
15
  'total': z.number(),
16
16
  'models': z.array(z.object({
17
17
  'model': z.string(),
@@ -27,7 +27,8 @@ const _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_result$schema = z.obj
27
27
  'outputCost': z.number(),
28
28
  })),
29
29
  })
30
- const _deepseek_ai_dsh_llm_billing_billing_getTodaySpend_result$schema = z.object({
30
+ const _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema = z.union([z.undefined(), z.literal(false), z.literal(true)])
31
+ const _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema = z.object({
31
32
  'total': z.number(),
32
33
  'models': z.array(z.object({
33
34
  'model': z.string(),
@@ -58,9 +59,9 @@ export const TYPERT_REMOTE = {
58
59
  result: {
59
60
  mode: 'strict',
60
61
  typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekBalance',
61
- schema: _deepseek_ai_dsh_llm_billing_billing_getBalance_result$schema,
62
+ schema: _rayadesu_dsh_llm_billing_billing_getBalance_result$schema,
62
63
  },
63
- sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":139,"column":3},
64
+ sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":143,"column":3},
64
65
  },
65
66
  {
66
67
  id: '@rayadesu/dsh-llm-billing#billing/getSessionSpend',
@@ -76,16 +77,16 @@ export const TYPERT_REMOTE = {
76
77
  codec: {
77
78
  mode: 'strict',
78
79
  typeSymbol: '@deepseek-ai/dsh-session/types#SessionId',
79
- schema: _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema,
80
+ schema: _rayadesu_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema,
80
81
  },
81
82
  },
82
83
  ],
83
84
  result: {
84
85
  mode: 'strict',
85
86
  typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekSessionSpend',
86
- schema: _deepseek_ai_dsh_llm_billing_billing_getSessionSpend_result$schema,
87
+ schema: _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema,
87
88
  },
88
- sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":149,"column":3},
89
+ sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":154,"column":3},
89
90
  },
90
91
  {
91
92
  id: '@rayadesu/dsh-llm-billing#billing/getTodaySpend',
@@ -94,13 +95,24 @@ export const TYPERT_REMOTE = {
94
95
  method: 'getTodaySpend',
95
96
  invocation: { kind: 'direct' },
96
97
  parameters: [
98
+ {
99
+ name: 'force',
100
+ wire: 'force',
101
+ source: 'json',
102
+ acceptsUndefined: true,
103
+ codec: {
104
+ mode: 'strict',
105
+ typeSymbol: '@rayadesu/dsh-llm-billing#billing/getTodaySpend:force',
106
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema,
107
+ },
108
+ },
97
109
  ],
98
110
  result: {
99
111
  mode: 'strict',
100
112
  typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekTodaySpend',
101
- schema: _deepseek_ai_dsh_llm_billing_billing_getTodaySpend_result$schema,
113
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema,
102
114
  },
103
- sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":159,"column":3},
115
+ sourceLocation: {"file":"packages/llm/llm-billing/src/balance.ts","line":167,"column":3},
104
116
  },
105
117
  ],
106
118
  }
@@ -32,8 +32,12 @@ export interface DeepSeekBalanceGatewayOptions {
32
32
  fetchBalance: () => Promise<DeepSeekBalance>;
33
33
  /** Compute one session's billed spend through the plugin's resolved facts. */
34
34
  fetchSessionSpend: (sessionId: SessionId) => Promise<DeepSeekSessionSpend>;
35
- /** Compute today's billed spend across every session through the plugin's resolved facts. */
36
- fetchTodaySpend: () => Promise<DeepSeekTodaySpend>;
35
+ /**
36
+ * Compute today's billed spend across every session through the plugin's
37
+ * resolved facts. `force` bypasses the host-side time window (the manual
38
+ * refresh path); a `force` miss still reuses revision-gated increments.
39
+ */
40
+ fetchTodaySpend: (force?: boolean) => Promise<DeepSeekTodaySpend>;
37
41
  }
38
42
  /**
39
43
  * Remote-only service exposing the DeepSeek account balance and session spend.
@@ -64,9 +68,12 @@ export declare class DeepSeekBalanceGateway extends TypertRemoteService {
64
68
  /**
65
69
  * Read today's billed spend across every session, priced per event by its
66
70
  * Beijing-time calendar day, hour, and weekday (weekends are always off-peak).
71
+ * @param force - bypass the host-side 60s cache (manual refresh); omitted
72
+ * means a cached read. Remote parameters cannot carry default values, so
73
+ * the thunk receives `undefined` for an omitted argument.
67
74
  * @returns today's total cost plus one row per priced model.
68
75
  */
69
- getTodaySpend(): Promise<DeepSeekTodaySpend>;
76
+ getTodaySpend(force?: boolean): Promise<DeepSeekTodaySpend>;
70
77
  }
71
78
  export default DeepSeekBalanceGateway;
72
79
  //# sourceMappingURL=balance.d.ts.map
@@ -176,10 +176,13 @@ let DeepSeekBalanceGateway = (() => {
176
176
  /**
177
177
  * Read today's billed spend across every session, priced per event by its
178
178
  * Beijing-time calendar day, hour, and weekday (weekends are always off-peak).
179
+ * @param force - bypass the host-side 60s cache (manual refresh); omitted
180
+ * means a cached read. Remote parameters cannot carry default values, so
181
+ * the thunk receives `undefined` for an omitted argument.
179
182
  * @returns today's total cost plus one row per priced model.
180
183
  */
181
- getTodaySpend() {
182
- return this.options.fetchTodaySpend();
184
+ getTodaySpend(force) {
185
+ return this.options.fetchTodaySpend(force ?? false);
183
186
  }
184
187
  };
185
188
  })();
@@ -3,6 +3,12 @@
3
3
  * pricing. Pure functions over session events and the pricing table, so the
4
4
  * Remote gateway stays transport-free and the whole spend is testable without
5
5
  * a key.
6
+ *
7
+ * The per-event pricing lives in {@link priceEvent}, the one shared fold
8
+ * primitive: the events-scan paths ({@link computeSessionSpend},
9
+ * {@link computeTodaySpend}) and the session-projection unit
10
+ * (`billingTodaySpend` in projection.ts) all fold the same contribution, so a
11
+ * pricing-table change cannot drift one path from the others.
6
12
  * @module @rayadesu/dsh-llm-billing/billing
7
13
  */
8
14
  import type { SessionEvent } from '@deepseek-ai/dsh-session';
@@ -78,6 +84,8 @@ export interface ResolvedBilling {
78
84
  * @returns the resolved table and peak-hour windows.
79
85
  */
80
86
  export declare function resolveBilling(config: BillingConfig | undefined): ResolvedBilling;
87
+ /** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
88
+ export declare function beijingDayKey(now: Date): string;
81
89
  /**
82
90
  * Whether a timestamp falls inside any peak-hour window (Beijing time,
83
91
  * weekdays Monday–Friday only). Weekends (Saturday and Sunday) are always
@@ -87,6 +95,70 @@ export declare function resolveBilling(config: BillingConfig | undefined): Resol
87
95
  * @returns true during a weekday peak hour.
88
96
  */
89
97
  export declare function isPeak(billing: ResolvedBilling, now: Date): boolean;
98
+ /**
99
+ * One priced billed event's contribution: the model row plus the Beijing
100
+ * calendar day its timestamp falls on. `undefined` when the event carries no
101
+ * priced usage (not an `assistant/message`, no usage report, or a model
102
+ * without a pricing row).
103
+ */
104
+ export interface BillingEventContribution {
105
+ /** Beijing-time calendar-day key of the event's timestamp. */
106
+ dayKey: string;
107
+ /** Wire model id, e.g. `deepseek-v4-flash`. */
108
+ model: string;
109
+ /** Selector label for the model. */
110
+ displayName: string;
111
+ /** Billed cost in CNY (peak plus off-peak portions). */
112
+ cost: number;
113
+ /** Cost portion billed at peak rates. */
114
+ peakCost: number;
115
+ /** Cost portion billed at off-peak rates. */
116
+ offPeakCost: number;
117
+ /** Cache-hit input tokens billed at the hit rate. */
118
+ cacheHitInputTokens: number;
119
+ /** Cache-miss input tokens (uncached input plus cache writes), billed at the miss rate. */
120
+ cacheMissInputTokens: number;
121
+ /** Output tokens (reasoning included), billed at the output rate. */
122
+ outputTokens: number;
123
+ /** Billed cost of cache-hit input tokens in CNY. */
124
+ cacheHitInputCost: number;
125
+ /** Billed cost of cache-miss input tokens (uncached input plus cache writes) in CNY. */
126
+ cacheMissInputCost: number;
127
+ /** Billed cost of output tokens (reasoning included) in CNY. */
128
+ outputCost: number;
129
+ }
130
+ /**
131
+ * Price one event at the official per-model rates, applying the peak/off-peak
132
+ * table by its Beijing-time hour and weekday (peak windows apply Monday–Friday
133
+ * only; weekends are off-peak). Each `assistant/message` event with usage
134
+ * contributes cache-hit input, cache-miss input (uncached input plus cache
135
+ * writes), and output (reasoning included) tokens at the rate of its own
136
+ * timestamp; a model with usage but no pricing row contributes nothing (the
137
+ * published table prices only the two V4 rows).
138
+ * @param event - the event to price.
139
+ * @param billing - resolved pricing with peak-hour windows.
140
+ * @param names - model id → display label.
141
+ * @returns the priced contribution, or `undefined` when the event has no priced usage.
142
+ */
143
+ export declare function priceEvent(event: SessionEvent, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingEventContribution | undefined;
144
+ /** A spend with no priced usage. */
145
+ export declare function emptyTodaySpend(): DeepSeekTodaySpend;
146
+ /**
147
+ * Merge one priced event's contribution into an accumulator spend (pure:
148
+ * returns a new spend, never mutates its input).
149
+ * @param spend - the accumulator (per session and day, or across sessions).
150
+ * @param priced - the priced contribution to add.
151
+ * @returns the merged spend.
152
+ */
153
+ export declare function addEventContribution(spend: DeepSeekTodaySpend, priced: BillingEventContribution): DeepSeekTodaySpend;
154
+ /**
155
+ * Sum two spends (per session and day, or across sessions) into one (pure:
156
+ * returns a new spend, never mutates its inputs).
157
+ * @param target - the accumulator spend.
158
+ * @param source - the spend to add.
159
+ * @returns the summed spend.
160
+ */
161
+ export declare function mergeTodaySpend(target: DeepSeekTodaySpend, source: DeepSeekTodaySpend): DeepSeekTodaySpend;
90
162
  /**
91
163
  * Price one session's complete event log at the official per-model rates.
92
164
  * @param events - one session's complete event log.
@@ -3,6 +3,12 @@
3
3
  * pricing. Pure functions over session events and the pricing table, so the
4
4
  * Remote gateway stays transport-free and the whole spend is testable without
5
5
  * a key.
6
+ *
7
+ * The per-event pricing lives in {@link priceEvent}, the one shared fold
8
+ * primitive: the events-scan paths ({@link computeSessionSpend},
9
+ * {@link computeTodaySpend}) and the session-projection unit
10
+ * (`billingTodaySpend` in projection.ts) all fold the same contribution, so a
11
+ * pricing-table change cannot drift one path from the others.
6
12
  * @module @rayadesu/dsh-llm-billing/billing
7
13
  */
8
14
  /**
@@ -67,7 +73,7 @@ function beijingWeekday(now) {
67
73
  return new Date(now.getTime() + 8 * 3_600_000).getUTCDay();
68
74
  }
69
75
  /** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
70
- function beijingDayKey(now) {
76
+ export function beijingDayKey(now) {
71
77
  return new Date(now.getTime() + 8 * 3_600_000).toISOString().slice(0, 10);
72
78
  }
73
79
  /**
@@ -85,6 +91,127 @@ export function isPeak(billing, now) {
85
91
  const hour = beijingHour(now);
86
92
  return billing.peakHours.some(({ start, end }) => hour >= start && hour < end);
87
93
  }
94
+ /**
95
+ * Price one event at the official per-model rates, applying the peak/off-peak
96
+ * table by its Beijing-time hour and weekday (peak windows apply Monday–Friday
97
+ * only; weekends are off-peak). Each `assistant/message` event with usage
98
+ * contributes cache-hit input, cache-miss input (uncached input plus cache
99
+ * writes), and output (reasoning included) tokens at the rate of its own
100
+ * timestamp; a model with usage but no pricing row contributes nothing (the
101
+ * published table prices only the two V4 rows).
102
+ * @param event - the event to price.
103
+ * @param billing - resolved pricing with peak-hour windows.
104
+ * @param names - model id → display label.
105
+ * @returns the priced contribution, or `undefined` when the event has no priced usage.
106
+ */
107
+ export function priceEvent(event, billing, names) {
108
+ if (event.type !== 'assistant/message')
109
+ return undefined;
110
+ const reported = event.data.usage;
111
+ if (reported === undefined)
112
+ return undefined;
113
+ const model = event.data.message.source.model;
114
+ const pricing = billing.models.get(model);
115
+ if (pricing === undefined)
116
+ return undefined;
117
+ const time = new Date(event.time);
118
+ const peak = isPeak(billing, time);
119
+ const price = peak ? pricing.peak : pricing.offPeak;
120
+ const hit = reported.cacheReadTokens ?? 0;
121
+ const miss = reported.inputTokens + (reported.cacheWriteTokens ?? 0);
122
+ const output = reported.outputTokens;
123
+ const hitCost = (hit * price.cacheHitInput) / 1_000_000;
124
+ const missCost = (miss * price.cacheMissInput) / 1_000_000;
125
+ const outputCost = (output * price.output) / 1_000_000;
126
+ const cost = hitCost + missCost + outputCost;
127
+ return {
128
+ dayKey: beijingDayKey(time),
129
+ model,
130
+ displayName: names.get(model) ?? model,
131
+ cost,
132
+ peakCost: peak ? cost : 0,
133
+ offPeakCost: peak ? 0 : cost,
134
+ cacheHitInputTokens: hit,
135
+ cacheMissInputTokens: miss,
136
+ outputTokens: output,
137
+ cacheHitInputCost: hitCost,
138
+ cacheMissInputCost: missCost,
139
+ outputCost,
140
+ };
141
+ }
142
+ /** A spend with no priced usage. */
143
+ export function emptyTodaySpend() {
144
+ return { total: 0, models: [] };
145
+ }
146
+ /** The today-spend shape of a single priced contribution. */
147
+ function contributionModel(priced) {
148
+ return {
149
+ model: priced.model,
150
+ displayName: priced.displayName,
151
+ cost: priced.cost,
152
+ peakCost: priced.peakCost,
153
+ offPeakCost: priced.offPeakCost,
154
+ cacheHitInputTokens: priced.cacheHitInputTokens,
155
+ cacheMissInputTokens: priced.cacheMissInputTokens,
156
+ outputTokens: priced.outputTokens,
157
+ cacheHitInputCost: priced.cacheHitInputCost,
158
+ cacheMissInputCost: priced.cacheMissInputCost,
159
+ outputCost: priced.outputCost,
160
+ };
161
+ }
162
+ /**
163
+ * Merge one priced event's contribution into an accumulator spend (pure:
164
+ * returns a new spend, never mutates its input).
165
+ * @param spend - the accumulator (per session and day, or across sessions).
166
+ * @param priced - the priced contribution to add.
167
+ * @returns the merged spend.
168
+ */
169
+ export function addEventContribution(spend, priced) {
170
+ const rows = spend.models.map(row => row.model === priced.model
171
+ ? {
172
+ ...row,
173
+ cost: row.cost + priced.cost,
174
+ peakCost: row.peakCost + priced.peakCost,
175
+ offPeakCost: row.offPeakCost + priced.offPeakCost,
176
+ cacheHitInputTokens: row.cacheHitInputTokens + priced.cacheHitInputTokens,
177
+ cacheMissInputTokens: row.cacheMissInputTokens + priced.cacheMissInputTokens,
178
+ outputTokens: row.outputTokens + priced.outputTokens,
179
+ cacheHitInputCost: row.cacheHitInputCost + priced.cacheHitInputCost,
180
+ cacheMissInputCost: row.cacheMissInputCost + priced.cacheMissInputCost,
181
+ outputCost: row.outputCost + priced.outputCost,
182
+ }
183
+ : row);
184
+ if (!rows.some(row => row.model === priced.model))
185
+ rows.push(contributionModel(priced));
186
+ return { total: spend.total + priced.cost, models: rows };
187
+ }
188
+ /**
189
+ * Sum two spends (per session and day, or across sessions) into one (pure:
190
+ * returns a new spend, never mutates its inputs).
191
+ * @param target - the accumulator spend.
192
+ * @param source - the spend to add.
193
+ * @returns the summed spend.
194
+ */
195
+ export function mergeTodaySpend(target, source) {
196
+ let merged = target;
197
+ for (const row of source.models) {
198
+ merged = addEventContribution(merged, {
199
+ dayKey: '',
200
+ model: row.model,
201
+ displayName: row.displayName,
202
+ cost: row.cost,
203
+ peakCost: row.peakCost,
204
+ offPeakCost: row.offPeakCost,
205
+ cacheHitInputTokens: row.cacheHitInputTokens,
206
+ cacheMissInputTokens: row.cacheMissInputTokens,
207
+ outputTokens: row.outputTokens,
208
+ cacheHitInputCost: row.cacheHitInputCost,
209
+ cacheMissInputCost: row.cacheMissInputCost,
210
+ outputCost: row.outputCost,
211
+ });
212
+ }
213
+ return merged;
214
+ }
88
215
  /**
89
216
  * Price a set of billed events at the official per-model rates, applying the
90
217
  * peak/off-peak table per event by its Beijing-time hour and weekday (peak
@@ -101,64 +228,14 @@ export function isPeak(billing, now) {
101
228
  */
102
229
  function priceEvents(events, billing, catalog) {
103
230
  const names = new Map(catalog.map(model => [model.id, model.name]));
104
- const rows = new Map();
231
+ let spend = { total: 0, models: [] };
105
232
  for (const event of events) {
106
- if (event.type !== 'assistant/message')
107
- continue;
108
- const reported = event.data.usage;
109
- if (reported === undefined)
233
+ const priced = priceEvent(event, billing, names);
234
+ if (priced === undefined)
110
235
  continue;
111
- const model = event.data.message.source.model;
112
- const pricing = billing.models.get(model);
113
- if (pricing === undefined)
114
- continue;
115
- const peak = isPeak(billing, new Date(event.time));
116
- const price = peak ? pricing.peak : pricing.offPeak;
117
- const hit = reported.cacheReadTokens ?? 0;
118
- const miss = reported.inputTokens + (reported.cacheWriteTokens ?? 0);
119
- const output = reported.outputTokens;
120
- const hitCost = (hit * price.cacheHitInput) / 1_000_000;
121
- const missCost = (miss * price.cacheMissInput) / 1_000_000;
122
- const outputCost = (output * price.output) / 1_000_000;
123
- const cost = hitCost + missCost + outputCost;
124
- let row = rows.get(model);
125
- if (row === undefined) {
126
- row = {
127
- cacheHitInputTokens: 0, cacheMissInputTokens: 0, outputTokens: 0,
128
- cost: 0, peakCost: 0, offPeakCost: 0,
129
- cacheHitInputCost: 0, cacheMissInputCost: 0, outputCost: 0,
130
- };
131
- rows.set(model, row);
132
- }
133
- row.cacheHitInputTokens += hit;
134
- row.cacheMissInputTokens += miss;
135
- row.outputTokens += output;
136
- row.cost += cost;
137
- row.cacheHitInputCost += hitCost;
138
- row.cacheMissInputCost += missCost;
139
- row.outputCost += outputCost;
140
- if (peak)
141
- row.peakCost += cost;
142
- else
143
- row.offPeakCost += cost;
236
+ spend = addEventContribution(spend, priced);
144
237
  }
145
- const models = [...rows.entries()].map(([model, row]) => ({
146
- model,
147
- displayName: names.get(model) ?? model,
148
- cost: row.cost,
149
- peakCost: row.peakCost,
150
- offPeakCost: row.offPeakCost,
151
- cacheHitInputTokens: row.cacheHitInputTokens,
152
- cacheMissInputTokens: row.cacheMissInputTokens,
153
- outputTokens: row.outputTokens,
154
- cacheHitInputCost: row.cacheHitInputCost,
155
- cacheMissInputCost: row.cacheMissInputCost,
156
- outputCost: row.outputCost,
157
- }));
158
- return {
159
- total: models.reduce((sum, model) => sum + model.cost, 0),
160
- models,
161
- };
238
+ return spend;
162
239
  }
163
240
  /**
164
241
  * Price one session's complete event log at the official per-model rates.
@@ -182,6 +259,14 @@ export function computeSessionSpend(events, billing, catalog) {
182
259
  */
183
260
  export function computeTodaySpend(events, billing, catalog, now = new Date()) {
184
261
  const day = beijingDayKey(now);
185
- return priceEvents(events.filter(event => beijingDayKey(new Date(event.time)) === day), billing, catalog);
262
+ const names = new Map(catalog.map(model => [model.id, model.name]));
263
+ let spend = emptyTodaySpend();
264
+ for (const event of events) {
265
+ const priced = priceEvent(event, billing, names);
266
+ if (priced === undefined || priced.dayKey !== day)
267
+ continue;
268
+ spend = addEventContribution(spend, priced);
269
+ }
270
+ return spend;
186
271
  }
187
272
  //# sourceMappingURL=billing.js.map
@@ -4,15 +4,28 @@
4
4
  * the credential/environment seams, prices each session's billed usage with the
5
5
  * peak/off-peak table, and exposes the `billing` Remote (`getBalance`, the
6
6
  * per-session `getSessionSpend`, and the all-sessions `getTodaySpend`).
7
+ *
8
+ * Today's spend never scans every session log per request: a 60-second
9
+ * Beijing-day cache with in-flight coalescing serves the message-triggered
10
+ * reads, the manual refresh may bypass the time window (`force`), and the
11
+ * computation behind a miss reads only sessions whose persisted revision
12
+ * changed since the last resolution (see today-spend.ts). When the
13
+ * session-projection registry is composed, the plugin additionally registers
14
+ * the `billingTodaySpend` projection unit, which folds each session's spend
15
+ * eagerly and lets cold reads ride the projection-cache ladder.
7
16
  * @module @rayadesu/dsh-llm-billing
8
17
  */
9
18
  import type { Context } from '@deepseek-ai/cordis';
10
19
  import z from '@deepseek-ai/schemastery';
11
20
  import type { BillingConfig } from './billing.ts';
12
21
  export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from './balance.ts';
13
- export { computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, isPeak, resolveBilling, } from './billing.ts';
14
- export type { BillingConfig, BillingConfigModel, DeepSeekModelPricing, DeepSeekTokenPrice, PeakHourWindow, ResolvedBilling, } from './billing.ts';
22
+ export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyTodaySpend, isPeak, mergeTodaySpend, priceEvent, resolveBilling, } from './billing.ts';
23
+ export type { BillingConfig, BillingConfigModel, BillingEventContribution, DeepSeekModelPricing, DeepSeekTokenPrice, PeakHourWindow, ResolvedBilling, } from './billing.ts';
15
24
  export type * from './types.ts';
25
+ export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit } from './projection.ts';
26
+ export type { BillingUnitState } from './projection.ts';
27
+ export { TodaySpendCache, TodaySpendScanner } from './today-spend.ts';
28
+ export type { ScannerPersistedHeader, ScannerSession, TodaySpendScannerDeps } from './today-spend.ts';
16
29
  export declare const name = "llm-billing";
17
30
  /** Public API default; deployments may point elsewhere via $DEEPSEEK_BASE_URL. */
18
31
  export declare const PUBLIC_BASE_URL = "https://api.deepseek.com";
@@ -40,6 +53,10 @@ export interface Config {
40
53
  billing?: BillingConfig;
41
54
  }
42
55
  export declare const Config: z<Config>;
56
+ /** How often a Beijing-day "today spend" value may be recomputed (60s). */
57
+ export declare const TODAY_SPEND_CACHE_MS = 60000;
58
+ /** Hard cap on today's events collected by the events scan path. */
59
+ export declare const TODAY_SPEND_MAX_EVENTS = 200000;
43
60
  /**
44
61
  * Register the `billing` Remote under the `billing` namespace.
45
62
  * @param ctx - owning plugin context.