@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,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/mnt/d/code/DeepSeek-Harness-chat-billing/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e/node_modules/@deepseek-ai/cordis/node_modules:/mnt/d/code/DeepSeek-Harness-chat-billing/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e/node_modules:/mnt/d/code/DeepSeek-Harness-chat-billing/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/mnt/d/code/DeepSeek-Harness-chat-billing/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e/node_modules/@deepseek-ai/cordis/node_modules:/mnt/d/code/DeepSeek-Harness-chat-billing/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e/node_modules:/mnt/d/code/DeepSeek-Harness-chat-billing/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../@deepseek-ai/cordis/bin.js" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../@deepseek-ai/cordis/bin.js" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../@deepseek-ai/cordis/bin.js" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../@deepseek-ai/cordis/bin.js" "$@"
40
+ else
41
+ exec node "$basedir/../@deepseek-ai/cordis/bin.js" "$@"
42
+ fi
43
+ # cmd-shim-target=D:/code/DeepSeek-Harness-chat-billing/packages/llm-billing/node_modules/@deepseek-ai/cordis/bin.js
@@ -0,0 +1,12 @@
1
+ @SETLOCAL
2
+ @IF NOT DEFINED NODE_PATH (
3
+ @SET "NODE_PATH=D:\code\DeepSeek-Harness-chat-billing\node_modules\.pnpm\@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e\node_modules\@deepseek-ai\cordis\node_modules;D:\code\DeepSeek-Harness-chat-billing\node_modules\.pnpm\@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e\node_modules;D:\code\DeepSeek-Harness-chat-billing\node_modules\.pnpm\node_modules"
4
+ ) ELSE (
5
+ @SET "NODE_PATH=D:\code\DeepSeek-Harness-chat-billing\node_modules\.pnpm\@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e\node_modules\@deepseek-ai\cordis\node_modules;D:\code\DeepSeek-Harness-chat-billing\node_modules\.pnpm\@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e\node_modules;D:\code\DeepSeek-Harness-chat-billing\node_modules\.pnpm\node_modules;%NODE_PATH%"
6
+ )
7
+ @IF EXIST "%~dp0\node.exe" (
8
+ "%~dp0\node.exe" "%~dp0\..\@deepseek-ai\cordis\bin.js" %*
9
+ ) ELSE (
10
+ @SET PATHEXT=%PATHEXT:;.JS;=;%
11
+ node "%~dp0\..\@deepseek-ai\cordis\bin.js" %*
12
+ )
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env pwsh
2
+ $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
3
+
4
+ $exe=""
5
+ $pathsep=":"
6
+ $env_node_path=$env:NODE_PATH
7
+ $new_node_path="D:\code\DeepSeek-Harness-chat-billing\node_modules\.pnpm\@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e\node_modules\@deepseek-ai\cordis\node_modules;D:\code\DeepSeek-Harness-chat-billing\node_modules\.pnpm\@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e\node_modules;D:\code\DeepSeek-Harness-chat-billing\node_modules\.pnpm\node_modules"
8
+ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
9
+ # Fix case when both the Windows and Linux builds of Node
10
+ # are installed in the same directory
11
+ $exe=".exe"
12
+ $pathsep=";"
13
+ } else {
14
+ $new_node_path="/mnt/d/code/DeepSeek-Harness-chat-billing/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e/node_modules/@deepseek-ai/cordis/node_modules:/mnt/d/code/DeepSeek-Harness-chat-billing/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@_d53e407b0055a51d18c4da8d739fec0e/node_modules:/mnt/d/code/DeepSeek-Harness-chat-billing/node_modules/.pnpm/node_modules"
15
+ }
16
+ if ([string]::IsNullOrEmpty($env_node_path)) {
17
+ $env:NODE_PATH=$new_node_path
18
+ } else {
19
+ $env:NODE_PATH="$new_node_path$pathsep$env_node_path"
20
+ }
21
+
22
+ $ret=0
23
+ if (Test-Path "$basedir/node$exe") {
24
+ # Support pipeline input
25
+ if ($MyInvocation.ExpectingInput) {
26
+ $input | & "$basedir/node$exe" "$basedir/../@deepseek-ai/cordis/bin.js" $args
27
+ } else {
28
+ & "$basedir/node$exe" "$basedir/../@deepseek-ai/cordis/bin.js" $args
29
+ }
30
+ $ret=$LASTEXITCODE
31
+ } else {
32
+ # Support pipeline input
33
+ if ($MyInvocation.ExpectingInput) {
34
+ $input | & "node$exe" "$basedir/../@deepseek-ai/cordis/bin.js" $args
35
+ } else {
36
+ & "node$exe" "$basedir/../@deepseek-ai/cordis/bin.js" $args
37
+ }
38
+ $ret=$LASTEXITCODE
39
+ }
40
+ $env:NODE_PATH=$env_node_path
41
+ exit $ret
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@rayadesu/dsh-llm-billing",
3
+ "description": "Standalone DeepSeek account-balance and session-spend provider exposed through the billing Remote",
4
+ "version": "0.1.0-rc.8",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/rayadesune/DeepSeek-Harness-chat-billing.git",
11
+ "directory": "packages/llm-billing"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./types": {
26
+ "types": "./lib/types/types.d.ts",
27
+ "default": "./lib/types/types.js"
28
+ },
29
+ "./typert": {
30
+ "types": "./lib/typert.host.d.ts",
31
+ "default": "./lib/typert.host.js"
32
+ },
33
+ "./remote": {
34
+ "types": "./lib/typert.remote-client.d.ts",
35
+ "default": "./lib/typert.remote-client.js"
36
+ },
37
+ "./src/*": "./src/*",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "lib/index.js",
42
+ "lib/invariant.js",
43
+ "lib/types/**/*.js",
44
+ "lib/types/**/*.d.ts",
45
+ "lib/typert.host.js",
46
+ "lib/typert.host.d.ts",
47
+ "lib/typert.remote-client.js",
48
+ "lib/typert.remote-client.d.ts"
49
+ ],
50
+ "license": "MIT",
51
+ "peerDependencies": {
52
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
53
+ "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8",
54
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
55
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
56
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
57
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8",
58
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8",
59
+ "@deepseek-ai/cordis": "^4.0.1"
60
+ },
61
+ "dependencies": {
62
+ "@deepseek-ai/schemastery": "^3.18.1",
63
+ "zod": "^4.4.3"
64
+ },
65
+ "devDependencies": {
66
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
67
+ "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8",
68
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
69
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
70
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
71
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8",
72
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8",
73
+ "@deepseek-ai/cordis": "^4.0.1"
74
+ }
75
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * DeepSeek account-balance capability: the `GET /user/balance` transport and
3
+ * the Remote gateway that exposes one snapshot to trusted clients. The fetch
4
+ * takes an already-resolved endpoint and bearer token so the registering
5
+ * plugin stays the one owner of credential policy; the gateway carries only a
6
+ * `fetchBalance` thunk for the same reason.
7
+ * @module @rayadesu/dsh-llm-billing/balance
8
+ */
9
+
10
+ import type { Context } from '@deepseek-ai/cordis'
11
+ import { LlmError } from '@deepseek-ai/dsh-llm'
12
+ import type { SessionId } from '@deepseek-ai/dsh-session'
13
+ import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
14
+ // Typert-generated ./typert and ./remote artifacts import Zod at runtime.
15
+ import type {} from 'zod'
16
+ import type { DeepSeekBalance, DeepSeekBalanceLine, DeepSeekSessionSpend, DeepSeekTodaySpend } from './types.ts'
17
+
18
+ /** Map a balance HTTP status to a stable LlmError code. */
19
+ function httpErrorCode(status: number): string {
20
+ if (status === 401 || status === 403) return 'AUTH'
21
+ if (status === 429) return 'RATE_LIMIT'
22
+ if (status >= 500) return 'SERVER'
23
+ return `HTTP_${status}`
24
+ }
25
+
26
+ /**
27
+ * Parse and validate the DeepSeek balance response at the wire boundary.
28
+ * @param body - decoded JSON response body.
29
+ * @returns the validated, detached balance snapshot.
30
+ * @throws {@link LlmError} with code `TRANSPORT` when the body is malformed.
31
+ */
32
+ export function parseDeepSeekBalance(body: unknown): DeepSeekBalance {
33
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) {
34
+ throw new LlmError('DeepSeek balance response was not a JSON object', 'TRANSPORT')
35
+ }
36
+ const response = body as Record<string, unknown>
37
+ const isAvailable = response['is_available']
38
+ const infos = response['balance_infos']
39
+ if (typeof isAvailable !== 'boolean' || !Array.isArray(infos)) {
40
+ throw new LlmError('DeepSeek balance response is missing is_available or balance_infos', 'TRANSPORT')
41
+ }
42
+ const lines: DeepSeekBalanceLine[] = infos.map((info, index) => {
43
+ if (typeof info !== 'object' || info === null || Array.isArray(info)) {
44
+ throw new LlmError(`DeepSeek balance line ${index} is malformed`, 'TRANSPORT')
45
+ }
46
+ const line = info as Record<string, unknown>
47
+ const currency = line['currency']
48
+ const total = line['total_balance']
49
+ const granted = line['granted_balance']
50
+ const toppedUp = line['topped_up_balance']
51
+ if (typeof currency !== 'string' || currency.length === 0
52
+ || typeof total !== 'string'
53
+ || typeof granted !== 'string'
54
+ || typeof toppedUp !== 'string') {
55
+ throw new LlmError(`DeepSeek balance line ${index} has missing or invalid fields`, 'TRANSPORT')
56
+ }
57
+ return { currency, total, granted, toppedUp }
58
+ })
59
+ return { isAvailable, lines }
60
+ }
61
+
62
+ /**
63
+ * Fetch one account-balance snapshot from `{baseURL}/user/balance`.
64
+ * @param baseURL - resolved endpoint base; `/user/balance` is appended.
65
+ * @param apiKey - resolved bearer token for this endpoint.
66
+ * @param signal - optional cancellation.
67
+ * @returns the validated balance snapshot.
68
+ * @throws {@link LlmError} for transport, HTTP, or malformed-response failures.
69
+ */
70
+ export async function fetchDeepSeekBalance(
71
+ baseURL: string,
72
+ apiKey: string,
73
+ signal?: AbortSignal,
74
+ ): Promise<DeepSeekBalance> {
75
+ let response: Response
76
+ try {
77
+ response = await fetch(`${baseURL}/user/balance`, {
78
+ method: 'GET',
79
+ headers: {
80
+ 'authorization': `Bearer ${apiKey}`,
81
+ 'accept': 'application/json',
82
+ },
83
+ ...(signal === undefined ? {} : { signal }),
84
+ })
85
+ } catch (error: unknown) {
86
+ if (signal?.aborted) throw new LlmError('DeepSeek balance request aborted by caller', 'ABORTED', { cause: error })
87
+ throw new LlmError(`DeepSeek balance request to ${baseURL} failed`, 'TRANSPORT', { cause: error })
88
+ }
89
+ if (!response.ok) {
90
+ throw new LlmError(
91
+ `DeepSeek balance request failed (HTTP ${response.status})`,
92
+ httpErrorCode(response.status),
93
+ { status: response.status },
94
+ )
95
+ }
96
+ let body: unknown
97
+ try {
98
+ body = await response.json()
99
+ } catch (error: unknown) {
100
+ throw new LlmError('DeepSeek balance response was not valid JSON', 'TRANSPORT', { cause: error })
101
+ }
102
+ return parseDeepSeekBalance(body)
103
+ }
104
+
105
+ /** Thunks the plugin binds to its own resolution and history access. */
106
+ export interface DeepSeekBalanceGatewayOptions {
107
+ /** Fetch one balance snapshot through the plugin's resolved facts. */
108
+ fetchBalance: () => Promise<DeepSeekBalance>
109
+ /** Compute one session's billed spend through the plugin's resolved facts. */
110
+ fetchSessionSpend: (sessionId: SessionId) => Promise<DeepSeekSessionSpend>
111
+ /** Compute today's billed spend across every session through the plugin's resolved facts. */
112
+ fetchTodaySpend: () => Promise<DeepSeekTodaySpend>
113
+ }
114
+
115
+ /**
116
+ * Remote-only service exposing the DeepSeek account balance and session spend.
117
+ * The plugin that owns connection, credential, and session-history resolution
118
+ * constructs it with the matching thunks, so the Remote boundary never sees an
119
+ * endpoint, key, or the session store.
120
+ */
121
+ export class DeepSeekBalanceGateway extends TypertRemoteService {
122
+ private readonly options: DeepSeekBalanceGatewayOptions
123
+
124
+ /**
125
+ * Register the balance Remote under the `billing` namespace.
126
+ * @param ctx - owning plugin context.
127
+ * @param options - balance and spend thunks bound to the plugin's facts.
128
+ */
129
+ constructor(ctx: Context, options: DeepSeekBalanceGatewayOptions) {
130
+ super(ctx, 'billing')
131
+ this.options = options
132
+ }
133
+
134
+ /**
135
+ * Read the current DeepSeek account balance.
136
+ * @returns the validated balance snapshot.
137
+ */
138
+ @Remote('getBalance')
139
+ getBalance(): Promise<DeepSeekBalance> {
140
+ return this.options.fetchBalance()
141
+ }
142
+
143
+ /**
144
+ * Read one session's billed spend, priced per event by its peak/off-peak hour.
145
+ * @param sessionId - the session whose spend to compute.
146
+ * @returns the session's total cost plus one row per priced model.
147
+ */
148
+ @Remote('getSessionSpend')
149
+ getSessionSpend(sessionId: SessionId): Promise<DeepSeekSessionSpend> {
150
+ return this.options.fetchSessionSpend(sessionId)
151
+ }
152
+
153
+ /**
154
+ * Read today's billed spend across every session, priced per event by its
155
+ * Beijing-time calendar day and peak/off-peak hour.
156
+ * @returns today's total cost plus one row per priced model.
157
+ */
158
+ @Remote('getTodaySpend')
159
+ getTodaySpend(): Promise<DeepSeekTodaySpend> {
160
+ return this.options.fetchTodaySpend()
161
+ }
162
+ }
163
+
164
+ export default DeepSeekBalanceGateway
@@ -0,0 +1,251 @@
1
+ /**
2
+ * DeepSeek billing: the peak/off-peak pricing table and the per-session spend
3
+ * pricing. Pure functions over session events and the pricing table, so the
4
+ * Remote gateway stays transport-free and the whole spend is testable without
5
+ * a key.
6
+ * @module @rayadesu/dsh-llm-billing/billing
7
+ */
8
+
9
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
10
+ import type { DeepSeekSessionSpend, DeepSeekTodaySpend } from './types.ts'
11
+
12
+ /** One token price point, in CNY per 1M tokens. */
13
+ export interface DeepSeekTokenPrice {
14
+ /** 1M input cache-hit tokens. */
15
+ cacheHitInput: number
16
+ /** 1M input cache-miss tokens (cache writes bill at this rate too). */
17
+ cacheMissInput: number
18
+ /** 1M output tokens. */
19
+ output: number
20
+ }
21
+
22
+ /** Peak and off-peak price pair for one model. */
23
+ export interface DeepSeekModelPricing {
24
+ /** Price during peak hours. */
25
+ peak: DeepSeekTokenPrice
26
+ /** Price during off-peak hours. */
27
+ offPeak: DeepSeekTokenPrice
28
+ }
29
+
30
+ /** One model's pricing-table row in configuration form. */
31
+ export interface BillingConfigModel {
32
+ /** Wire model id. */
33
+ model: string
34
+ /** Peak-hour price. */
35
+ peak: DeepSeekTokenPrice
36
+ /** Off-peak price. */
37
+ offPeak: DeepSeekTokenPrice
38
+ }
39
+
40
+ /** One peak-hour window on a 24h Beijing-time clock. */
41
+ export interface PeakHourWindow {
42
+ /** Inclusive start hour, `0`–`23`. */
43
+ start: number
44
+ /** Exclusive end hour, `1`–`24`. */
45
+ end: number
46
+ }
47
+
48
+ /** Optional billing configuration; omission uses the published defaults. */
49
+ export interface BillingConfig {
50
+ /** Peak-hour windows in Beijing time. */
51
+ peakHours?: PeakHourWindow[]
52
+ /** Per-model pricing rows; omission uses the V4 Flash, V4 Pro, and V4 Flash Vision defaults. */
53
+ models?: BillingConfigModel[]
54
+ }
55
+
56
+ /** Published peak-hour windows (Beijing time): 09:00–12:00 and 14:00–18:00. */
57
+ export const DEFAULT_PEAK_HOURS: { start: number; end: number }[] = [
58
+ { start: 9, end: 12 },
59
+ { start: 14, end: 18 },
60
+ ]
61
+
62
+ /** Official peak/off-peak rates (CNY per 1M tokens), effective 2026-08-17. */
63
+ export const DEFAULT_MODEL_PRICING: BillingConfigModel[] = [
64
+ {
65
+ model: 'deepseek-v4-flash',
66
+ peak: { cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 },
67
+ offPeak: { cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 },
68
+ },
69
+ {
70
+ model: 'deepseek-v4-pro',
71
+ peak: { cacheHitInput: 0.30, cacheMissInput: 9.0, output: 27.0 },
72
+ offPeak: { cacheHitInput: 0.15, cacheMissInput: 4.5, output: 13.5 },
73
+ },
74
+ // deepseek-v4-flash-vision-exp bills at the same rates as deepseek-v4-flash;
75
+ // images are converted to tokens at the same per-token price.
76
+ {
77
+ model: 'deepseek-v4-flash-vision-exp',
78
+ peak: { cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 },
79
+ offPeak: { cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 },
80
+ },
81
+ ]
82
+
83
+ /** Resolved billing configuration: a pricing table plus peak-hour windows. */
84
+ export interface ResolvedBilling {
85
+ peakHours: readonly { start: number; end: number }[]
86
+ models: ReadonlyMap<string, DeepSeekModelPricing>
87
+ }
88
+
89
+ /**
90
+ * Resolve optional configuration to a pricing table, defaulting omitted or
91
+ * empty rows to the published rates. Schemastery materializes an absent
92
+ * `z.array` as `[]` rather than `undefined`, so emptiness — not just absence —
93
+ * selects the defaults. Explicit non-empty rows override the same model; a
94
+ * supplied non-empty `models` list is authoritative.
95
+ * @param config - optional raw billing configuration.
96
+ * @returns the resolved table and peak-hour windows.
97
+ */
98
+ export function resolveBilling(config: BillingConfig | undefined): ResolvedBilling {
99
+ const peakHours = config?.peakHours !== undefined && config.peakHours.length > 0
100
+ ? config.peakHours
101
+ : DEFAULT_PEAK_HOURS
102
+ const rows = config?.models !== undefined && config.models.length > 0
103
+ ? config.models
104
+ : DEFAULT_MODEL_PRICING
105
+ const models = new Map<string, DeepSeekModelPricing>()
106
+ for (const row of rows) models.set(row.model, { peak: row.peak, offPeak: row.offPeak })
107
+ return { peakHours, models }
108
+ }
109
+
110
+ /** The Beijing (Asia/Shanghai, UTC+8, no DST) hour of a timestamp. */
111
+ function beijingHour(now: Date): number {
112
+ return new Date(now.getTime() + 8 * 3_600_000).getUTCHours()
113
+ }
114
+
115
+ /** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
116
+ function beijingDayKey(now: Date): string {
117
+ return new Date(now.getTime() + 8 * 3_600_000).toISOString().slice(0, 10)
118
+ }
119
+
120
+ /**
121
+ * Whether a timestamp falls inside any peak-hour window (Beijing time).
122
+ * @param billing - resolved pricing with peak-hour windows.
123
+ * @param now - the moment to classify.
124
+ * @returns true during peak hours.
125
+ */
126
+ export function isPeak(billing: ResolvedBilling, now: Date): boolean {
127
+ const hour = beijingHour(now)
128
+ return billing.peakHours.some(({ start, end }) => hour >= start && hour < end)
129
+ }
130
+
131
+ /** Running per-model spend accumulation for {@link computeSessionSpend}. */
132
+ interface SessionSpendRow {
133
+ cacheHitInputTokens: number
134
+ cacheMissInputTokens: number
135
+ outputTokens: number
136
+ cost: number
137
+ peakCost: number
138
+ offPeakCost: number
139
+ cacheHitInputCost: number
140
+ cacheMissInputCost: number
141
+ outputCost: number
142
+ }
143
+
144
+ /**
145
+ * Price a set of billed events at the official per-model rates, applying the
146
+ * peak/off-peak table per event by its Beijing-time hour. Each
147
+ * `assistant/message` event with usage contributes cache-hit input, cache-miss
148
+ * input (uncached input plus cache writes), and output (reasoning included)
149
+ * tokens at the rate of its own timestamp, with the three component costs
150
+ * carried separately; a model with usage but no pricing row is omitted (the
151
+ * published table prices only the two V4 rows).
152
+ * @param events - the events to price.
153
+ * @param billing - resolved pricing with peak-hour windows.
154
+ * @param catalog - model display rows, in presentation order.
155
+ * @returns the total cost plus one row per priced model.
156
+ */
157
+ function priceEvents(
158
+ events: readonly SessionEvent[],
159
+ billing: ResolvedBilling,
160
+ catalog: readonly { id: string; name: string }[],
161
+ ): DeepSeekSessionSpend {
162
+ const names = new Map(catalog.map(model => [model.id, model.name]))
163
+ const rows = new Map<string, SessionSpendRow>()
164
+ for (const event of events) {
165
+ if (event.type !== 'assistant/message') continue
166
+ const reported = event.data.usage
167
+ if (reported === undefined) continue
168
+ const model = event.data.message.source.model
169
+ const pricing = billing.models.get(model)
170
+ if (pricing === undefined) continue
171
+ const peak = isPeak(billing, new Date(event.time))
172
+ const price = peak ? pricing.peak : pricing.offPeak
173
+ const hit = reported.cacheReadTokens ?? 0
174
+ const miss = reported.inputTokens + (reported.cacheWriteTokens ?? 0)
175
+ const output = reported.outputTokens
176
+ const hitCost = (hit * price.cacheHitInput) / 1_000_000
177
+ const missCost = (miss * price.cacheMissInput) / 1_000_000
178
+ const outputCost = (output * price.output) / 1_000_000
179
+ const cost = hitCost + missCost + outputCost
180
+ let row = rows.get(model)
181
+ if (row === undefined) {
182
+ row = {
183
+ cacheHitInputTokens: 0, cacheMissInputTokens: 0, outputTokens: 0,
184
+ cost: 0, peakCost: 0, offPeakCost: 0,
185
+ cacheHitInputCost: 0, cacheMissInputCost: 0, outputCost: 0,
186
+ }
187
+ rows.set(model, row)
188
+ }
189
+ row.cacheHitInputTokens += hit
190
+ row.cacheMissInputTokens += miss
191
+ row.outputTokens += output
192
+ row.cost += cost
193
+ row.cacheHitInputCost += hitCost
194
+ row.cacheMissInputCost += missCost
195
+ row.outputCost += outputCost
196
+ if (peak) row.peakCost += cost
197
+ else row.offPeakCost += cost
198
+ }
199
+ const models = [...rows.entries()].map(([model, row]) => ({
200
+ model,
201
+ displayName: names.get(model) ?? model,
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
+ return {
213
+ total: models.reduce((sum, model) => sum + model.cost, 0),
214
+ models,
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Price one session's complete event log at the official per-model rates.
220
+ * @param events - one session's complete event log.
221
+ * @param billing - resolved pricing with peak-hour windows.
222
+ * @param catalog - model display rows, in presentation order.
223
+ * @returns the session's total cost plus one row per priced model.
224
+ */
225
+ export function computeSessionSpend(
226
+ events: readonly SessionEvent[],
227
+ billing: ResolvedBilling,
228
+ catalog: readonly { id: string; name: string }[],
229
+ ): DeepSeekSessionSpend {
230
+ return priceEvents(events, billing, catalog)
231
+ }
232
+
233
+ /**
234
+ * Price every event whose Beijing-time calendar day is the day of `now`,
235
+ * aggregating across every session's event log. Events from other Beijing
236
+ * days are ignored, so a caller passes the concatenated logs of all sessions.
237
+ * @param events - every session's complete event log, concatenated.
238
+ * @param billing - resolved pricing with peak-hour windows.
239
+ * @param catalog - model display rows, in presentation order.
240
+ * @param now - the reference moment whose Beijing-time calendar day is "today".
241
+ * @returns today's total cost plus one row per priced model.
242
+ */
243
+ export function computeTodaySpend(
244
+ events: readonly SessionEvent[],
245
+ billing: ResolvedBilling,
246
+ catalog: readonly { id: string; name: string }[],
247
+ now: Date = new Date(),
248
+ ): DeepSeekTodaySpend {
249
+ const day = beijingDayKey(now)
250
+ return priceEvents(events.filter(event => beijingDayKey(new Date(event.time)) === day), billing, catalog)
251
+ }