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.
- package/.gitattributes +1 -0
- package/.github/workflows/ci.yml +39 -0
- package/CHANGELOG.ja.md +39 -0
- package/CHANGELOG.ko.md +39 -0
- package/CHANGELOG.md +135 -0
- package/CHANGELOG.zh.md +39 -0
- package/CONTRIBUTING.md +22 -0
- package/README.ja.md +104 -0
- package/README.ko.md +103 -0
- package/README.md +89 -12
- package/README.zh.md +87 -12
- package/SECURITY.md +18 -0
- package/THIRD_PARTY_NOTICES.md +7 -31
- package/docs/installation.ja.md +76 -0
- package/docs/installation.ko.md +76 -0
- package/docs/installation.md +76 -0
- package/docs/installation.zh.md +76 -0
- package/docs/repair-log.md +582 -0
- package/eslint.config.js +30 -0
- package/package.json +85 -82
- package/packages/selector/LICENSE +21 -0
- package/packages/selector/README.md +26 -0
- package/packages/selector/README.zh.md +26 -0
- package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
- package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
- package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
- package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
- package/packages/selector/lib/style.css +308 -0
- package/packages/selector/package.json +115 -0
- package/{screenshots.json → packages/selector/screenshots.json} +6 -6
- package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
- package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
- package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
- package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
- package/packages/selector/src/client/EstimatorControls.tsx +281 -0
- package/packages/selector/src/client/decode.ts +49 -0
- package/packages/selector/src/client/index.ts +111 -0
- package/packages/selector/src/client/locales.ts +198 -0
- package/packages/selector/src/client/preset-options.ts +70 -0
- package/packages/selector/src/client/settings-section.tsx +126 -0
- package/packages/selector/src/css-modules.d.ts +6 -0
- package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
- package/packages/selector/src/estimator-catalog.ts +104 -0
- package/packages/selector/src/index.ts +327 -0
- package/packages/selector/src/invariant.ts +113 -0
- package/packages/selector/src/preset-overlay.ts +567 -0
- package/packages/selector/src/profiles.ts +342 -0
- package/packages/selector/src/pruner/content.ts +188 -0
- package/packages/selector/src/pruner/session.ts +94 -0
- package/packages/selector/src/pruner/state.ts +43 -0
- package/packages/selector/src/pruner/tuning.ts +23 -0
- package/packages/selector/src/pruner/types.ts +60 -0
- package/packages/selector/src/pruner.ts +2144 -0
- package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
- package/packages/selector/src/runtime/audit.ts +215 -0
- package/packages/selector/src/runtime/config.ts +613 -0
- package/packages/selector/src/runtime/custom-policy.ts +278 -0
- package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
- package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
- package/packages/selector/src/runtime/measurement.ts +403 -0
- package/packages/selector/src/runtime/reducers.ts +656 -0
- package/packages/selector/src/runtime/retrieve.ts +457 -0
- package/packages/selector/src/runtime/session-events.ts +17 -0
- package/packages/selector/src/runtime/tail-trim.ts +166 -0
- package/packages/selector/src/runtime/token-count.ts +72 -0
- package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
- package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
- package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
- package/packages/selector/src/runtime/types.ts +309 -0
- package/packages/selector/src/runtime/value.ts +48 -0
- package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
- package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
- package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
- package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
- package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
- package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
- package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
- package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
- package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
- package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
- package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
- package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
- package/packages/selector/tests/profiles.client.spec.tsx +434 -0
- package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
- package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
- package/packages/selector/tests/runtime/audit.spec.ts +129 -0
- package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
- package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
- package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
- package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
- package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
- package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
- package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
- package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
- package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
- package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
- package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
- package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
- package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
- package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
- package/packages/selector/tests/runtime/value.spec.ts +23 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
- package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
- package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
- package/packages/selector/tests/support/mock-adapter.ts +37 -0
- package/packages/selector/tests/support/ui-primitives.tsx +34 -0
- package/packages/selector/tsconfig.json +11 -0
- package/packages/selector/tsdown.client.config.ts +102 -0
- package/packages/selector/tsdown.config.ts +20 -0
- package/pnpm-workspace.yaml +19 -0
- package/scripts/capture-profile-baseline.ts +80 -0
- package/scripts/generate-tokenizer-fixtures.py +81 -0
- package/scripts/generate-vision-fixtures.py +208 -0
- package/scripts/packed-components-smoke.ts +713 -0
- package/scripts/packed-install-e2e.ts +1072 -0
- package/scripts/verify-release.ts +300 -0
- package/tests/TEST_INVENTORY.md +42 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +7 -0
- package/tsconfig.scripts.json +13 -0
- package/tsconfig.tests.json +15 -0
- package/vitest.built.config.ts +9 -0
- package/vitest.config.ts +43 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
- /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
- /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
- /package/{lib → packages/selector/lib}/client.d.ts +0 -0
- /package/{lib → packages/selector/lib}/client.js +0 -0
- /package/{lib → packages/selector/lib}/config.js +0 -0
- /package/{lib → packages/selector/lib}/index.d.ts +0 -0
- /package/{lib → packages/selector/lib}/index.js +0 -0
- /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
- /package/{lib → packages/selector/lib}/invariant.js +0 -0
- /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
- /package/{lib → packages/selector/lib}/pruner.js +0 -0
- /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
|
+
}
|