dsh-context-compression-improved 0.1.1 → 0.2.1

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 (148) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/workflows/ci.yml +39 -0
  3. package/CHANGELOG.ja.md +39 -0
  4. package/CHANGELOG.ko.md +39 -0
  5. package/CHANGELOG.md +135 -0
  6. package/CHANGELOG.zh.md +39 -0
  7. package/CONTRIBUTING.md +22 -0
  8. package/README.ja.md +104 -0
  9. package/README.ko.md +103 -0
  10. package/README.md +89 -12
  11. package/README.zh.md +87 -12
  12. package/SECURITY.md +18 -0
  13. package/THIRD_PARTY_NOTICES.md +7 -31
  14. package/docs/installation.ja.md +76 -0
  15. package/docs/installation.ko.md +76 -0
  16. package/docs/installation.md +76 -0
  17. package/docs/installation.zh.md +76 -0
  18. package/docs/repair-log.md +582 -0
  19. package/eslint.config.js +30 -0
  20. package/package.json +85 -82
  21. package/packages/selector/LICENSE +21 -0
  22. package/packages/selector/README.md +26 -0
  23. package/packages/selector/README.zh.md +26 -0
  24. package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
  25. package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
  26. package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
  27. package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
  28. package/packages/selector/lib/style.css +308 -0
  29. package/packages/selector/package.json +115 -0
  30. package/{screenshots.json → packages/selector/screenshots.json} +6 -6
  31. package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
  32. package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
  33. package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
  34. package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
  35. package/packages/selector/src/client/EstimatorControls.tsx +281 -0
  36. package/packages/selector/src/client/decode.ts +49 -0
  37. package/packages/selector/src/client/index.ts +111 -0
  38. package/packages/selector/src/client/locales.ts +198 -0
  39. package/packages/selector/src/client/preset-options.ts +70 -0
  40. package/packages/selector/src/client/settings-section.tsx +126 -0
  41. package/packages/selector/src/css-modules.d.ts +6 -0
  42. package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
  43. package/packages/selector/src/estimator-catalog.ts +104 -0
  44. package/packages/selector/src/index.ts +327 -0
  45. package/packages/selector/src/invariant.ts +113 -0
  46. package/packages/selector/src/preset-overlay.ts +567 -0
  47. package/packages/selector/src/profiles.ts +342 -0
  48. package/packages/selector/src/pruner/content.ts +188 -0
  49. package/packages/selector/src/pruner/session.ts +94 -0
  50. package/packages/selector/src/pruner/state.ts +43 -0
  51. package/packages/selector/src/pruner/tuning.ts +23 -0
  52. package/packages/selector/src/pruner/types.ts +60 -0
  53. package/packages/selector/src/pruner.ts +2144 -0
  54. package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
  55. package/packages/selector/src/runtime/audit.ts +215 -0
  56. package/packages/selector/src/runtime/config.ts +613 -0
  57. package/packages/selector/src/runtime/custom-policy.ts +278 -0
  58. package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
  59. package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
  60. package/packages/selector/src/runtime/measurement.ts +403 -0
  61. package/packages/selector/src/runtime/reducers.ts +656 -0
  62. package/packages/selector/src/runtime/retrieve.ts +457 -0
  63. package/packages/selector/src/runtime/session-events.ts +17 -0
  64. package/packages/selector/src/runtime/tail-trim.ts +166 -0
  65. package/packages/selector/src/runtime/token-count.ts +72 -0
  66. package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
  67. package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
  68. package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
  69. package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
  70. package/packages/selector/src/runtime/types.ts +309 -0
  71. package/packages/selector/src/runtime/value.ts +48 -0
  72. package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
  73. package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
  74. package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
  75. package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
  76. package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
  77. package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
  78. package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
  79. package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
  80. package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
  81. package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
  82. package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
  83. package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
  84. package/packages/selector/tests/profiles.client.spec.tsx +434 -0
  85. package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
  86. package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
  87. package/packages/selector/tests/runtime/audit.spec.ts +129 -0
  88. package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
  89. package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
  90. package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
  91. package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
  92. package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
  93. package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
  94. package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
  95. package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
  96. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
  97. package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
  98. package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
  99. package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
  100. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
  101. package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
  102. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
  103. package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
  104. package/packages/selector/tests/runtime/value.spec.ts +23 -0
  105. package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
  106. package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
  107. package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
  108. package/packages/selector/tests/support/mock-adapter.ts +37 -0
  109. package/packages/selector/tests/support/ui-primitives.tsx +34 -0
  110. package/packages/selector/tsconfig.json +11 -0
  111. package/packages/selector/tsdown.client.config.ts +102 -0
  112. package/packages/selector/tsdown.config.ts +20 -0
  113. package/pnpm-workspace.yaml +19 -0
  114. package/scripts/capture-profile-baseline.ts +80 -0
  115. package/scripts/generate-tokenizer-fixtures.py +81 -0
  116. package/scripts/generate-vision-fixtures.py +208 -0
  117. package/scripts/packed-components-smoke.ts +713 -0
  118. package/scripts/packed-install-e2e.ts +1072 -0
  119. package/scripts/verify-release.ts +300 -0
  120. package/tests/TEST_INVENTORY.md +42 -0
  121. package/tsconfig.base.json +18 -0
  122. package/tsconfig.json +7 -0
  123. package/tsconfig.scripts.json +13 -0
  124. package/tsconfig.tests.json +15 -0
  125. package/vitest.built.config.ts +9 -0
  126. package/vitest.config.ts +43 -0
  127. /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
  128. /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
  129. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
  130. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
  131. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
  132. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
  133. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
  134. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
  135. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
  136. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
  137. /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
  138. /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
  139. /package/{lib → packages/selector/lib}/client.d.ts +0 -0
  140. /package/{lib → packages/selector/lib}/client.js +0 -0
  141. /package/{lib → packages/selector/lib}/config.js +0 -0
  142. /package/{lib → packages/selector/lib}/index.d.ts +0 -0
  143. /package/{lib → packages/selector/lib}/index.js +0 -0
  144. /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
  145. /package/{lib → packages/selector/lib}/invariant.js +0 -0
  146. /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
  147. /package/{lib → packages/selector/lib}/pruner.js +0 -0
  148. /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
@@ -0,0 +1,278 @@
1
+ /** Strict versioned Custom policy parsing and effective-token resolution. */
2
+
3
+ import z from '@deepseek-ai/schemastery'
4
+ import type {
5
+ CompressionPolicy,
6
+ CustomCompressionPolicy,
7
+ CustomCompressionPolicyV3,
8
+ } from './types.ts'
9
+ import { deepFreeze } from './value.ts'
10
+
11
+ const budgetSchema = z.object({
12
+ enabled: z.boolean().required(),
13
+ trigger: z.number().required(),
14
+ target: z.number().required(),
15
+ }).required()
16
+
17
+ const legacyHistorySchema = z.object({
18
+ enabled: z.boolean().required(),
19
+ trigger: z.number().required(),
20
+ keepRecentTurns: z.number().step(1).min(0).required(),
21
+ keepRecent: z.number().required(),
22
+ minReclaim: z.number().required(),
23
+ }).required()
24
+
25
+ const historySchema = z.object({
26
+ enabled: z.boolean().required(),
27
+ trigger: z.number().required(),
28
+ keepRecentToolCalls: z.number().step(1).min(0).required(),
29
+ keepRecentTokens: z.number().required(),
30
+ minReclaim: z.number().required(),
31
+ }).required()
32
+
33
+ const tailTrimSchema = z.object({
34
+ enabled: z.boolean().required(),
35
+ trigger: z.number().required(),
36
+ }).required()
37
+
38
+ const customCompressionPolicyV1InputSchema = z.object({
39
+ version: z.const(1).required(),
40
+ unit: z.union(['tokens', 'context-percent']).required(),
41
+ fresh: budgetSchema,
42
+ aggregate: budgetSchema,
43
+ history: legacyHistorySchema,
44
+ prefixPolicy: z.union(['preserve', 'pressure-break']).required(),
45
+ }).required()
46
+
47
+ const customCompressionPolicyV2InputSchema = z.object({
48
+ version: z.const(2).required(),
49
+ unit: z.union(['tokens', 'context-percent']).required(),
50
+ fresh: budgetSchema,
51
+ aggregate: budgetSchema,
52
+ history: legacyHistorySchema,
53
+ prefixPolicy: z.union(['preserve', 'pressure-break']).required(),
54
+ tailTrim: tailTrimSchema,
55
+ }).required()
56
+
57
+ const customCompressionPolicyV3InputSchema = z.object({
58
+ version: z.const(3).required(),
59
+ unit: z.union(['tokens', 'context-percent']).required(),
60
+ fresh: budgetSchema,
61
+ aggregate: budgetSchema,
62
+ history: historySchema,
63
+ prefixPolicy: z.union(['preserve', 'pressure-break']).required(),
64
+ tailTrim: tailTrimSchema,
65
+ }).required()
66
+
67
+ /** Canonical Custom document accepted by Host settings and the runtime resolver. */
68
+ export const CustomCompressionPolicySchema: z<CustomCompressionPolicy> = z.transform(
69
+ z.any().required(),
70
+ (value): CustomCompressionPolicy => {
71
+ assertExactPolicyShape(value)
72
+ const policy = value.version === 1
73
+ ? customCompressionPolicyV1InputSchema(value) as CustomCompressionPolicy
74
+ : value.version === 2
75
+ ? customCompressionPolicyV2InputSchema(value) as CustomCompressionPolicy
76
+ : customCompressionPolicyV3InputSchema(value) as CustomCompressionPolicy
77
+ const canonical = canonicalizeCustomPolicy(policy)
78
+ assertCanonicalRelations(canonical)
79
+ return deepFreeze(structuredClone(canonical))
80
+ },
81
+ ) as z<CustomCompressionPolicy>
82
+
83
+ /** Balanced-equivalent Custom policy stored as one token-canonical document. */
84
+ export const DEFAULT_CUSTOM_COMPRESSION_POLICY: CustomCompressionPolicyV3 = deepFreeze({
85
+ version: 3,
86
+ unit: 'tokens',
87
+ fresh: { enabled: true, trigger: 8_192, target: 3_072 },
88
+ aggregate: { enabled: true, trigger: 32_768, target: 12_288 },
89
+ history: {
90
+ enabled: true,
91
+ trigger: 500_000,
92
+ keepRecentToolCalls: 10,
93
+ keepRecentTokens: 64_000,
94
+ minReclaim: 96_000,
95
+ },
96
+ prefixPolicy: 'pressure-break',
97
+ tailTrim: { enabled: false, trigger: 700_000 },
98
+ })
99
+
100
+ /** Routed model facts needed only by context-percent Custom documents. */
101
+ export interface CustomPolicyResolutionOptions {
102
+ /** Positive resolved shared model context capacity. */
103
+ readonly contextWindowTokens?: number
104
+ /**
105
+ * Frozen Auto Compact threshold percent. Standard profiles use it to link
106
+ * History watermarks to the Auto Compact level; Custom resolution ignores it
107
+ * because Custom stays explicit-token manual.
108
+ */
109
+ readonly autoCompactThresholdPercent?: number
110
+ }
111
+
112
+ /**
113
+ * Resolve one validated Custom document to the same token policy used by public presets.
114
+ * @param value - untrusted or typed Custom settings value.
115
+ * @param options - routed model capacity for context-percent documents.
116
+ * @returns a detached deeply immutable effective token policy.
117
+ */
118
+ export function resolveCustomPolicy(
119
+ value: CustomCompressionPolicy,
120
+ options: CustomPolicyResolutionOptions = {},
121
+ ): CompressionPolicy {
122
+ const policy = canonicalizeCustomPolicy(CustomCompressionPolicySchema(value))
123
+ const effective = (name: string, amount: number): number => {
124
+ if (policy.unit === 'tokens') return amount
125
+ const contextWindow = options.contextWindowTokens
126
+ if (!Number.isSafeInteger(contextWindow) || contextWindow === undefined || contextWindow <= 0) {
127
+ throw new Error('Custom context-percent policy requires a resolved positive model context window')
128
+ }
129
+ const tokens = Math.floor(contextWindow * amount / 100)
130
+ if (!Number.isSafeInteger(tokens) || (amount > 0 && tokens <= 0)) {
131
+ throw new Error(`Custom ${name} has no valid effective token value for this model`)
132
+ }
133
+ return tokens
134
+ }
135
+ const resolved: CompressionPolicy = {
136
+ profile: 'custom',
137
+ nativeToolResultEnabled: false,
138
+ freshEnabled: policy.fresh.enabled,
139
+ aggregateEnabled: policy.aggregate.enabled,
140
+ historyMode: !policy.history.enabled
141
+ ? 'disabled'
142
+ : policy.prefixPolicy === 'preserve' ? 'capacity-pressure' : 'routine',
143
+ nativeTriggerTokens: Number.MAX_SAFE_INTEGER,
144
+ nativeTargetTokens: Number.MAX_SAFE_INTEGER,
145
+ freshTriggerTokens: effective('Fresh trigger', policy.fresh.trigger),
146
+ freshTargetTokens: effective('Fresh target', policy.fresh.target),
147
+ aggregateTriggerTokens: effective('Aggregate trigger', policy.aggregate.trigger),
148
+ aggregateTargetTokens: effective('Aggregate target', policy.aggregate.target),
149
+ historyTriggerTokens: effective('History trigger', policy.history.trigger),
150
+ historyKeepRecentToolCalls: policy.history.keepRecentToolCalls,
151
+ historyKeepRecentTokens: effective('History recent token tail', policy.history.keepRecentTokens),
152
+ historyMinReclaimTokens: effective('History min-reclaim', policy.history.minReclaim),
153
+ tailTrim: {
154
+ enabled: policy.tailTrim.enabled,
155
+ triggerTokens: effective('TailTrim trigger', policy.tailTrim.trigger),
156
+ },
157
+ }
158
+ assertEffectiveRelations(resolved)
159
+ return deepFreeze(resolved)
160
+ }
161
+
162
+ function canonicalizeCustomPolicy(policy: CustomCompressionPolicy): CustomCompressionPolicyV3 {
163
+ if (policy.version === 3) return policy
164
+ return {
165
+ version: 3,
166
+ unit: policy.unit,
167
+ fresh: policy.fresh,
168
+ aggregate: policy.aggregate,
169
+ history: {
170
+ enabled: policy.history.enabled,
171
+ trigger: policy.history.trigger,
172
+ keepRecentToolCalls: 10,
173
+ keepRecentTokens: policy.history.keepRecent,
174
+ minReclaim: policy.history.minReclaim,
175
+ },
176
+ prefixPolicy: policy.prefixPolicy,
177
+ tailTrim: policy.version === 1 ? { enabled: false, trigger: 700_000 } : policy.tailTrim,
178
+ }
179
+ }
180
+
181
+ function measuredValues(policy: CustomCompressionPolicyV3): readonly number[] {
182
+ return [
183
+ policy.fresh.trigger,
184
+ policy.fresh.target,
185
+ policy.aggregate.trigger,
186
+ policy.aggregate.target,
187
+ policy.history.trigger,
188
+ policy.history.keepRecentTokens,
189
+ policy.history.minReclaim,
190
+ policy.tailTrim.trigger,
191
+ ]
192
+ }
193
+
194
+ function validMeasuredValues(policy: CustomCompressionPolicyV3): boolean {
195
+ const values = measuredValues(policy)
196
+ const positive = [
197
+ policy.fresh.trigger,
198
+ policy.fresh.target,
199
+ policy.aggregate.trigger,
200
+ policy.aggregate.target,
201
+ policy.history.trigger,
202
+ policy.history.minReclaim,
203
+ policy.tailTrim.trigger,
204
+ ]
205
+ if (!positive.every(value => value > 0)
206
+ || policy.history.keepRecentTokens < 0
207
+ || !Number.isSafeInteger(policy.history.keepRecentToolCalls)
208
+ || policy.history.keepRecentToolCalls < 0) return false
209
+ return policy.unit === 'tokens'
210
+ ? values.every(Number.isSafeInteger)
211
+ : values.every(value => Number.isFinite(value) && value <= 100)
212
+ }
213
+
214
+ function assertCanonicalRelations(policy: CustomCompressionPolicyV3): void {
215
+ if (!validMeasuredValues(policy)) {
216
+ throw new TypeError('Custom measured values must use the selected canonical unit')
217
+ }
218
+ if (policy.fresh.target >= policy.fresh.trigger) {
219
+ throw new TypeError('Custom Fresh target must be below trigger')
220
+ }
221
+ if (policy.aggregate.target >= policy.aggregate.trigger) {
222
+ throw new TypeError('Custom Aggregate target must be below trigger')
223
+ }
224
+ if (policy.history.minReclaim > policy.history.trigger) {
225
+ throw new TypeError('Custom History min-reclaim must not exceed its trigger')
226
+ }
227
+ }
228
+
229
+ function assertExactPolicyShape(value: unknown): asserts value is Record<string, unknown> {
230
+ if (!isPlainRecord(value)) throw new TypeError('Custom must be a plain object')
231
+ if (value.version !== 1 && value.version !== 2 && value.version !== 3) {
232
+ throw new TypeError('Custom version must be 1, 2, or 3')
233
+ }
234
+ assertExactKeys(
235
+ value,
236
+ value.version === 1
237
+ ? ['version', 'unit', 'fresh', 'aggregate', 'history', 'prefixPolicy']
238
+ : ['version', 'unit', 'fresh', 'aggregate', 'history', 'prefixPolicy', 'tailTrim'],
239
+ 'Custom',
240
+ )
241
+ assertExactKeys(value.fresh, ['enabled', 'trigger', 'target'], 'Custom Fresh')
242
+ assertExactKeys(value.aggregate, ['enabled', 'trigger', 'target'], 'Custom Aggregate')
243
+ assertExactKeys(
244
+ value.history,
245
+ value.version === 3
246
+ ? ['enabled', 'trigger', 'keepRecentToolCalls', 'keepRecentTokens', 'minReclaim']
247
+ : ['enabled', 'trigger', 'keepRecentTurns', 'keepRecent', 'minReclaim'],
248
+ 'Custom History',
249
+ )
250
+ if (value.version !== 1) {
251
+ assertExactKeys(value.tailTrim, ['enabled', 'trigger'], 'Custom TailTrim')
252
+ }
253
+ }
254
+
255
+ function assertExactKeys(value: unknown, allowed: readonly string[], label: string): asserts value is Record<string, unknown> {
256
+ if (!isPlainRecord(value)) throw new TypeError(`${label} must be a plain object`)
257
+ const allowedKeys = new Set(allowed)
258
+ const unknown = Object.keys(value).find(key => !allowedKeys.has(key))
259
+ if (unknown !== undefined) throw new TypeError(`${label}: unknown key "${unknown}"`)
260
+ }
261
+
262
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
263
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
264
+ const prototype = Object.getPrototypeOf(value) as object | null
265
+ return prototype === Object.prototype || prototype === null
266
+ }
267
+
268
+ function assertEffectiveRelations(policy: CompressionPolicy): void {
269
+ if (policy.freshTargetTokens >= policy.freshTriggerTokens) {
270
+ throw new Error('Custom effective Fresh target must be below trigger')
271
+ }
272
+ if (policy.aggregateTargetTokens >= policy.aggregateTriggerTokens) {
273
+ throw new Error('Custom effective Aggregate target must be below trigger')
274
+ }
275
+ if (policy.historyMinReclaimTokens > policy.historyTriggerTokens) {
276
+ throw new Error('Custom effective History min-reclaim must not exceed its trigger')
277
+ }
278
+ }
@@ -0,0 +1,298 @@
1
+ /** Checked-in DeepSeek official prices and fixed-point provider-usage accounting. */
2
+
3
+ export const DEEPSEEK_OFFICIAL_PRICE_CATALOG_VERSION = 'deepseek-official-2026-08-25' as const
4
+ /** Wall-clock time at which the checked-in official price pages were verified. */
5
+ export const DEEPSEEK_OFFICIAL_PRICE_CHECKED_AT = '2026-08-25T00:10:20+08:00' as const
6
+
7
+ /** Currencies published by the checked-in DeepSeek price catalog. */
8
+ export type DeepSeekPriceCurrency = 'USD' | 'CNY'
9
+ /** Official dynamic-price schedule bands. */
10
+ export type DeepSeekPriceBand = 'peak' | 'off-peak'
11
+ /** DeepSeek API surfaces with explicit prompt usage semantics. */
12
+ export type DeepSeekPriceApiRoute = 'chat-completions' | 'responses'
13
+ /** Exact official V4 model ids covered by this catalog version. */
14
+ export type OfficialDeepSeekModelId =
15
+ | 'deepseek-v4-flash'
16
+ | 'deepseek-v4-pro'
17
+ | 'deepseek-v4-flash-vision-exp'
18
+
19
+ /** One immutable applicable official price tuple. */
20
+ export interface OfficialDeepSeekPriceRecord {
21
+ readonly catalogVersion: typeof DEEPSEEK_OFFICIAL_PRICE_CATALOG_VERSION
22
+ readonly checkedAt: typeof DEEPSEEK_OFFICIAL_PRICE_CHECKED_AT
23
+ readonly provider: 'deepseek-official'
24
+ readonly baseUrlClass: 'official-public'
25
+ readonly apiRoute: DeepSeekPriceApiRoute
26
+ readonly modelId: OfficialDeepSeekModelId
27
+ readonly modelVersion: string
28
+ readonly currency: DeepSeekPriceCurrency
29
+ readonly unitTokens: 1_000_000
30
+ readonly band: DeepSeekPriceBand
31
+ readonly inputCacheHit: string
32
+ readonly inputCacheMiss: string
33
+ readonly output: string
34
+ readonly sourceUrl: string
35
+ readonly sourceLocale: 'en' | 'zh-CN'
36
+ readonly peakRule: 'Asia/Shanghai Mon-Fri 09:00-12:00,14:00-18:00'
37
+ }
38
+
39
+ /** Applicable official price record or a fail-closed reason. */
40
+ export type OfficialDeepSeekPriceResolution =
41
+ | { readonly kind: 'priced'; readonly record: OfficialDeepSeekPriceRecord }
42
+ | { readonly kind: 'unpriced'; readonly reason: string }
43
+
44
+ interface ResolvePriceInput {
45
+ readonly provider: string
46
+ readonly baseUrlClass: string
47
+ readonly apiRoute: string
48
+ readonly modelId: string
49
+ readonly currency: string
50
+ readonly at: Date
51
+ }
52
+
53
+ interface ModelPrices {
54
+ readonly version: string
55
+ readonly USD: Readonly<Record<DeepSeekPriceBand, readonly [string, string, string]>>
56
+ readonly CNY: Readonly<Record<DeepSeekPriceBand, readonly [string, string, string]>>
57
+ }
58
+
59
+ const PRICES: Readonly<Record<OfficialDeepSeekModelId, ModelPrices>> = Object.freeze({
60
+ 'deepseek-v4-flash': modelPrices(
61
+ 'DeepSeek-V4-Flash-0731',
62
+ ['0.007', '0.22', '0.66'], ['0.014', '0.44', '1.32'],
63
+ ['0.05', '1.5', '4.5'], ['0.10', '3.0', '9.0'],
64
+ ),
65
+ 'deepseek-v4-pro': modelPrices(
66
+ 'DeepSeek-V4-Pro-0813',
67
+ ['0.022', '0.66', '1.98'], ['0.044', '1.32', '3.96'],
68
+ ['0.15', '4.5', '13.5'], ['0.30', '9.0', '27.0'],
69
+ ),
70
+ 'deepseek-v4-flash-vision-exp': modelPrices(
71
+ 'DeepSeek-V4-Flash-Vision-Exp',
72
+ ['0.007', '0.22', '0.66'], ['0.014', '0.44', '1.32'],
73
+ ['0.05', '1.5', '4.5'], ['0.10', '3.0', '9.0'],
74
+ ),
75
+ })
76
+
77
+ const PEAK_RULE = 'Asia/Shanghai Mon-Fri 09:00-12:00,14:00-18:00' as const
78
+ const USD_SOURCE = 'https://api-docs.deepseek.com/quick_start/pricing/'
79
+ const CNY_SOURCE = 'https://api-docs.deepseek.com/zh-cn/quick_start/pricing/'
80
+
81
+ /**
82
+ * Resolve one immutable official price record; aliases and compatible gateways fail closed.
83
+ * @param input - exact provider, endpoint, route, model, currency, and timestamp applicability.
84
+ * @returns An immutable price record or an explicit unpriced reason.
85
+ */
86
+ export function resolveOfficialDeepSeekPrice(input: ResolvePriceInput): OfficialDeepSeekPriceResolution {
87
+ if (input.provider !== 'deepseek-official') return unpriced('unknown provider route')
88
+ if (input.baseUrlClass !== 'official-public') return unpriced('unknown base-url applicability')
89
+ if (input.apiRoute !== 'chat-completions' && input.apiRoute !== 'responses') {
90
+ return unpriced('unknown API route')
91
+ }
92
+ if (!isOfficialModel(input.modelId)) return unpriced('unknown model id')
93
+ if (input.currency !== 'USD' && input.currency !== 'CNY') return unpriced('unknown currency')
94
+ const band = priceBandAt(input.at)
95
+ if (band === undefined) return unpriced('invalid price timestamp')
96
+ const model = PRICES[input.modelId]
97
+ const [inputCacheHit, inputCacheMiss, output] = model[input.currency][band]
98
+ return {
99
+ kind: 'priced',
100
+ record: Object.freeze({
101
+ catalogVersion: DEEPSEEK_OFFICIAL_PRICE_CATALOG_VERSION,
102
+ checkedAt: DEEPSEEK_OFFICIAL_PRICE_CHECKED_AT,
103
+ provider: 'deepseek-official',
104
+ baseUrlClass: 'official-public',
105
+ apiRoute: input.apiRoute,
106
+ modelId: input.modelId,
107
+ modelVersion: model.version,
108
+ currency: input.currency,
109
+ unitTokens: 1_000_000,
110
+ band,
111
+ inputCacheHit,
112
+ inputCacheMiss,
113
+ output,
114
+ sourceUrl: input.currency === 'USD' ? USD_SOURCE : CNY_SOURCE,
115
+ sourceLocale: input.currency === 'USD' ? 'en' : 'zh-CN',
116
+ peakRule: PEAK_RULE,
117
+ }),
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Classify a timestamp under the published Beijing peak schedule.
123
+ * @param at - absolute request time to interpret in Asia/Shanghai.
124
+ * @returns Peak/off-peak, or undefined for an invalid timestamp.
125
+ */
126
+ export function priceBandAt(at: Date): DeepSeekPriceBand | undefined {
127
+ if (!Number.isFinite(at.getTime())) return undefined
128
+ const parts = new Intl.DateTimeFormat('en-US', {
129
+ timeZone: 'Asia/Shanghai',
130
+ weekday: 'short',
131
+ hour: '2-digit',
132
+ minute: '2-digit',
133
+ second: '2-digit',
134
+ hourCycle: 'h23',
135
+ }).formatToParts(at)
136
+ const values = Object.fromEntries(parts.map(part => [part.type, part.value]))
137
+ const weekday = values.weekday
138
+ const hour = Number(values.hour)
139
+ const minute = Number(values.minute)
140
+ const second = Number(values.second)
141
+ if (weekday === undefined || !Number.isInteger(hour)
142
+ || !Number.isInteger(minute) || !Number.isInteger(second)) return undefined
143
+ const workday = weekday !== 'Sat' && weekday !== 'Sun'
144
+ const seconds = hour * 3_600 + minute * 60 + second
145
+ const peak = workday && (
146
+ (seconds >= 9 * 3_600 && seconds < 12 * 3_600)
147
+ || (seconds >= 14 * 3_600 && seconds < 18 * 3_600)
148
+ )
149
+ return peak ? 'peak' : 'off-peak'
150
+ }
151
+
152
+ interface PriceUsageInput extends Omit<ResolvePriceInput, 'at'> {
153
+ readonly startedAt: Date
154
+ readonly completedAt: Date
155
+ readonly usage: {
156
+ readonly cacheReadTokens: number
157
+ readonly cacheMissTokens: number
158
+ readonly outputTokens: number
159
+ }
160
+ }
161
+
162
+ interface MoneyAmount {
163
+ readonly femtoUnits: string
164
+ readonly decimal: string
165
+ }
166
+
167
+ /** Exact fixed-point provider cost, a cross-band range, or an unpriced reason. */
168
+ export type OfficialDeepSeekUsageCost =
169
+ | ({ readonly kind: 'exact'; readonly currency: DeepSeekPriceCurrency; readonly band: DeepSeekPriceBand } & MoneyAmount)
170
+ | {
171
+ readonly kind: 'range'
172
+ readonly currency: DeepSeekPriceCurrency
173
+ readonly bands: readonly [DeepSeekPriceBand, DeepSeekPriceBand]
174
+ readonly minimum: MoneyAmount
175
+ readonly maximum: MoneyAmount
176
+ }
177
+ | { readonly kind: 'unpriced'; readonly reason: string }
178
+
179
+ /**
180
+ * Price one completed request, returning a range when it spans a published band boundary.
181
+ * @param input - exact applicability, request interval, and complete disjoint usage buckets.
182
+ * @returns Fixed-point exact/range cost or an explicit unpriced reason.
183
+ */
184
+ export function priceOfficialDeepSeekUsage(input: PriceUsageInput): OfficialDeepSeekUsageCost {
185
+ for (const [name, value] of Object.entries(input.usage)) {
186
+ if (!Number.isSafeInteger(value) || value < 0) return unpriced(`invalid ${name}`)
187
+ }
188
+ if (input.completedAt.getTime() < input.startedAt.getTime()) {
189
+ return unpriced('completion timestamp precedes request start')
190
+ }
191
+ const start = resolveOfficialDeepSeekPrice({ ...input, at: input.startedAt })
192
+ if (start.kind === 'unpriced') return start
193
+ const end = resolveOfficialDeepSeekPrice({ ...input, at: input.completedAt })
194
+ if (end.kind === 'unpriced') return end
195
+ const startAmount = amountFor(start.record, input.usage)
196
+ if (startAmount === undefined) return unpriced('invalid decimal price record')
197
+ const crossesBoundary = spansPublishedPriceBoundary(input.startedAt, input.completedAt)
198
+ if (start.record.band === end.record.band && !crossesBoundary) {
199
+ return { kind: 'exact', currency: start.record.currency, band: start.record.band, ...startAmount }
200
+ }
201
+ const comparisonRecord = start.record.band === end.record.band
202
+ ? priceRecordInBand(start.record, start.record.band === 'peak' ? 'off-peak' : 'peak')
203
+ : end.record
204
+ const endAmount = amountFor(comparisonRecord, input.usage)
205
+ if (endAmount === undefined) return unpriced('invalid decimal price record')
206
+ const startFemto = BigInt(startAmount.femtoUnits)
207
+ const endFemto = BigInt(endAmount.femtoUnits)
208
+ return {
209
+ kind: 'range',
210
+ currency: start.record.currency,
211
+ bands: [start.record.band, comparisonRecord.band],
212
+ minimum: startFemto <= endFemto ? startAmount : endAmount,
213
+ maximum: startFemto <= endFemto ? endAmount : startAmount,
214
+ }
215
+ }
216
+
217
+ /** Detect any published UTC band boundary, even when both endpoints share a band. */
218
+ function spansPublishedPriceBoundary(startedAt: Date, completedAt: Date): boolean {
219
+ const start = startedAt.getTime()
220
+ const end = completedAt.getTime()
221
+ if (end <= start) return false
222
+ const dayMs = 24 * 60 * 60 * 1_000
223
+ if (end - start >= 7 * dayMs) return true
224
+ const firstDay = Math.floor(start / dayMs) * dayMs
225
+ for (let day = firstDay; day <= end; day += dayMs) {
226
+ const weekday = new Date(day).getUTCDay()
227
+ if (weekday === 0 || weekday === 6) continue
228
+ for (const hour of [1, 4, 6, 10]) {
229
+ const boundary = day + hour * 60 * 60 * 1_000
230
+ if (boundary > start && boundary <= end) return true
231
+ }
232
+ }
233
+ return false
234
+ }
235
+
236
+ function priceRecordInBand(
237
+ record: OfficialDeepSeekPriceRecord,
238
+ band: DeepSeekPriceBand,
239
+ ): OfficialDeepSeekPriceRecord {
240
+ const [inputCacheHit, inputCacheMiss, output] = PRICES[record.modelId][record.currency][band]
241
+ return Object.freeze({ ...record, band, inputCacheHit, inputCacheMiss, output })
242
+ }
243
+
244
+ /**
245
+ * Parse a non-negative decimal rate into nano-currency units, without Number arithmetic.
246
+ * @param value - canonical non-negative decimal with at most nine fractional digits.
247
+ * @returns Integer nano-units, or undefined when the decimal is invalid.
248
+ */
249
+ export function decimalRateNanoUnits(value: string): bigint | undefined {
250
+ const match = /^(0|[1-9]\d*)(?:\.(\d{1,9}))?$/u.exec(value)
251
+ if (match === null) return undefined
252
+ const whole = match[1] ?? '0'
253
+ const fraction = (match[2] ?? '').padEnd(9, '0')
254
+ return BigInt(whole) * 1_000_000_000n + BigInt(fraction || '0')
255
+ }
256
+
257
+ function amountFor(
258
+ record: OfficialDeepSeekPriceRecord,
259
+ usage: PriceUsageInput['usage'],
260
+ ): MoneyAmount | undefined {
261
+ const hit = decimalRateNanoUnits(record.inputCacheHit)
262
+ const miss = decimalRateNanoUnits(record.inputCacheMiss)
263
+ const output = decimalRateNanoUnits(record.output)
264
+ if (hit === undefined || miss === undefined || output === undefined) return undefined
265
+ const femtoUnits = BigInt(usage.cacheReadTokens) * hit
266
+ + BigInt(usage.cacheMissTokens) * miss
267
+ + BigInt(usage.outputTokens) * output
268
+ return { femtoUnits: femtoUnits.toString(), decimal: formatFemto(femtoUnits) }
269
+ }
270
+
271
+ function formatFemto(value: bigint): string {
272
+ const digits = value.toString().padStart(16, '0')
273
+ const whole = digits.slice(0, -15)
274
+ const fraction = digits.slice(-15).replace(/0+$/u, '')
275
+ return fraction.length === 0 ? whole : `${whole}.${fraction}`
276
+ }
277
+
278
+ function modelPrices(
279
+ version: string,
280
+ usdOffPeak: readonly [string, string, string],
281
+ usdPeak: readonly [string, string, string],
282
+ cnyOffPeak: readonly [string, string, string],
283
+ cnyPeak: readonly [string, string, string],
284
+ ): ModelPrices {
285
+ return Object.freeze({
286
+ version,
287
+ USD: Object.freeze({ 'off-peak': usdOffPeak, peak: usdPeak }),
288
+ CNY: Object.freeze({ 'off-peak': cnyOffPeak, peak: cnyPeak }),
289
+ })
290
+ }
291
+
292
+ function isOfficialModel(value: string): value is OfficialDeepSeekModelId {
293
+ return Object.prototype.hasOwnProperty.call(PRICES, value)
294
+ }
295
+
296
+ function unpriced(reason: string): { readonly kind: 'unpriced'; readonly reason: string } {
297
+ return { kind: 'unpriced', reason }
298
+ }