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,254 @@
1
+ /**
2
+ * Exact DeepSeek V4 Flash Vision image-token arithmetic.
3
+ *
4
+ * Every rule in this module is a line-by-line port of the official
5
+ * `inference/image_processor.py` published by
6
+ * `deepseek-ai/DeepSeek-V4-Flash-Vision-Exp` at the pinned immutable revision
7
+ * recorded below. The golden fixtures in `tests/fixtures/vision-golden.json`
8
+ * are generated by executing that official implementation, so any change here
9
+ * must keep the Node counts byte-identical to the reference output.
10
+ *
11
+ * These arithmetic results back intrinsic-grid estimates, never exact counts
12
+ * of the final request. The official expansion
13
+ * depends on the absolute serialized prompt position (system prompt,
14
+ * chat-template framing, adapter image handles) and on the adapter's final
15
+ * request-image projection — including per-route pixel-budget or image-detail
16
+ * overrides and byte-cap reprojection — none of which is published through a
17
+ * public API, and a projected image can count FEWER tokens than its intrinsic
18
+ * grid suggests. The measurement layer therefore labels image-bearing nodes
19
+ * as estimates and keeps them outside exact rewrite proofs; the 640,000-pixel
20
+ * budget below documents the adapter default rather than establishing
21
+ * exactness.
22
+ */
23
+
24
+ /** Official projection parameters pinned from the model repository config. */
25
+ export const DEEPSEEK_VISION_PROJECTION = Object.freeze({
26
+ sourceRepository: 'deepseek-ai/DeepSeek-V4-Flash-Vision-Exp',
27
+ sourceRevision: '6821d6ad3681a4b137b066b76094fa82ebd0a380',
28
+ /** `vision_patch_size` from the official config. */
29
+ visionPatchSize: 14,
30
+ /** `vision_downsample_ratio` from the official config. */
31
+ visionDownsampleRatio: 3,
32
+ /** `vision_max_n_token`: post-preprocessing cap per image, not a fixed value. */
33
+ visionMaxNTokens: 384,
34
+ /** `vision_min_pixels`: tiny images are upscaled before patching. */
35
+ visionMinPixels: 147_456,
36
+ /** `vision_max_wh_ratio`: wider-than-ratio images are width-clamped. */
37
+ visionMaxWhRatio: 8,
38
+ /**
39
+ * Default per-image pixel budget used by the DeepSeek adapter's normal
40
+ * attachment projection (`DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET` in
41
+ * `@deepseek-ai/dsh-llm-deepseek`). Route overrides remain unobservable to
42
+ * this estimator.
43
+ */
44
+ requestImagePixelBudget: 640_000,
45
+ })
46
+
47
+ /**
48
+ * Fallback charged when durable image metadata cannot be evaluated. It sits
49
+ * near the middle of the official 384-token image budget so one malformed or
50
+ * provider-projected image never makes the surrounding surface unmeasurable.
51
+ */
52
+ export const DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS = 256
53
+
54
+ /** Stable identity for the deliberately approximate image-token counter. */
55
+ export const DEEPSEEK_VISION_IMAGE_ESTIMATOR = Object.freeze({
56
+ id: `${DEEPSEEK_VISION_PROJECTION.sourceRepository}/image-token-estimate`,
57
+ revision: `${DEEPSEEK_VISION_PROJECTION.sourceRevision}:v1`,
58
+ })
59
+
60
+ /** Official `COMPRESS_PAD_TO` alignment constant from image_processor.py. */
61
+ const COMPRESS_PAD_TO = 4
62
+
63
+ /** Aligner-grid facts the official pipeline derives from one image. */
64
+ export interface DeepSeekVisionImageGrid {
65
+ readonly nLlmH: number
66
+ readonly nLlmW: number
67
+ readonly bestHeight: number
68
+ readonly bestWidth: number
69
+ }
70
+
71
+ /** One bounded estimate over the four possible stream-alignment residues. */
72
+ export interface DeepSeekVisionImageTokenEstimate {
73
+ readonly tokens: number
74
+ readonly upperBoundTokens: number
75
+ readonly source: 'intrinsic-grid' | 'default'
76
+ readonly paddingMinimumTokens?: number
77
+ readonly paddingMaximumTokens?: number
78
+ }
79
+
80
+ /**
81
+ * Whether one image's intrinsic metadata fits the adapter's documented
82
+ * default request pixel budget. This is informational only: estimation also
83
+ * accepts larger valid dimensions because the official processor enforces its
84
+ * own 384-token grid budget.
85
+ */
86
+ export function isWithinDeepSeekRequestPixelBudget(width: number, height: number): boolean {
87
+ return width > 0 && height > 0 && width * height <= DEEPSEEK_VISION_PROJECTION.requestImagePixelBudget
88
+ }
89
+
90
+ /**
91
+ * Expand one image into its official token count at a given stream position.
92
+ *
93
+ * The count includes the position-dependent alignment padding from the
94
+ * official `build_image_block`: leading compress pads, the image start and end
95
+ * sentinels, one newline token per grid row, the odd-row pad, and the final
96
+ * two-token alignment pad. `startTokenPos` is the number of tokens already in
97
+ * the stream where this image's block begins, mirroring `len(tokens)` in the
98
+ * official `prepare_vl_inputs`.
99
+ */
100
+ export function deepSeekVisionImageTokens(width: number, height: number, startTokenPos: number): number {
101
+ const grid = deepSeekVisionImageGrid(width, height)
102
+ return deepSeekVisionImageBlockTokens(grid.nLlmH, grid.nLlmW, startTokenPos)
103
+ }
104
+
105
+ /**
106
+ * Estimate one image without claiming an exact serialized position or final
107
+ * adapter projection. Valid intrinsic dimensions use the midpoint of the four
108
+ * possible alignment residues. Invalid or unsafe metadata uses the documented
109
+ * fixed fallback. In both cases the official per-image budget is retained as
110
+ * a conservative upper bound.
111
+ */
112
+ export function estimateDeepSeekVisionImageTokens(
113
+ width: number,
114
+ height: number,
115
+ ): DeepSeekVisionImageTokenEstimate {
116
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)
117
+ || width <= 0 || height <= 0 || !Number.isSafeInteger(width * height)) {
118
+ return Object.freeze({
119
+ tokens: DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS,
120
+ upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens,
121
+ source: 'default',
122
+ })
123
+ }
124
+ try {
125
+ const grid = deepSeekVisionImageGrid(width, height)
126
+ const counts = [0, 1, 2, 3].map(position => (
127
+ deepSeekVisionImageBlockTokens(grid.nLlmH, grid.nLlmW, position)
128
+ ))
129
+ const paddingMinimumTokens = Math.min(...counts)
130
+ const paddingMaximumTokens = Math.max(...counts)
131
+ return Object.freeze({
132
+ tokens: Math.round((paddingMinimumTokens + paddingMaximumTokens) / 2),
133
+ upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens,
134
+ source: 'intrinsic-grid',
135
+ paddingMinimumTokens,
136
+ paddingMaximumTokens,
137
+ })
138
+ } catch {
139
+ return Object.freeze({
140
+ tokens: DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS,
141
+ upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens,
142
+ source: 'default',
143
+ })
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Resolve the aligner grid for one image's intrinsic dimensions.
149
+ *
150
+ * Port of the arithmetic path of the official `load_image`: aspect-ratio
151
+ * clamp, minimum-pixel upscale, patch-grid ceiling, and the `safe_resize`
152
+ * budget loop.
153
+ */
154
+ export function deepSeekVisionImageGrid(width: number, height: number): DeepSeekVisionImageGrid {
155
+ const { visionPatchSize: patch, visionMaxWhRatio, visionMinPixels } = DEEPSEEK_VISION_PROJECTION
156
+ let effectiveWidth: number = width
157
+ let effectiveHeight: number = height
158
+ if (visionMaxWhRatio !== undefined && effectiveWidth > effectiveHeight * visionMaxWhRatio) {
159
+ effectiveWidth = effectiveHeight * visionMaxWhRatio
160
+ }
161
+ if (effectiveWidth * effectiveHeight > 0 && effectiveWidth * effectiveHeight < visionMinPixels) {
162
+ const ratio = (visionMinPixels / (effectiveWidth * effectiveHeight)) ** 0.5
163
+ effectiveWidth = Math.trunc(effectiveWidth * ratio)
164
+ effectiveHeight = Math.trunc(effectiveHeight * ratio)
165
+ }
166
+ const bestWidth = Math.ceil(effectiveWidth / patch) * patch
167
+ const bestHeight = Math.ceil(effectiveHeight / patch) * patch
168
+ const resolved = safeResize(effectiveHeight, effectiveWidth, bestHeight, bestWidth)
169
+ return Object.freeze({ ...resolved })
170
+ }
171
+
172
+ /**
173
+ * Token count of one expanded image block, port of `build_image_block` length.
174
+ * @internal exported for direct golden-fixture comparison.
175
+ */
176
+ export function deepSeekVisionImageBlockTokens(nLlmH: number, nLlmW: number, startTokenPos: number): number {
177
+ const compressPad = COMPRESS_PAD_TO - 1 - startTokenPos % COMPRESS_PAD_TO
178
+ const padH = nLlmH % 2
179
+ const rows = nLlmH + padH
180
+ const rowLen = nLlmW + 1
181
+ const padLast = (Math.floor(rows / 2) * rowLen % 2) * 2
182
+ return compressPad + 1 + rows * rowLen + padLast + 1
183
+ }
184
+
185
+ /** Port of the official `grid_tokens` N-layout occupancy check. */
186
+ function gridTokens(bestHeight: number, bestWidth: number): { nLlmH: number, nLlmW: number, numTokens: number } {
187
+ const { visionPatchSize: patch, visionDownsampleRatio: downsample } = DEEPSEEK_VISION_PROJECTION
188
+ const nLlmH = Math.ceil(Math.floor(bestHeight / patch) / downsample)
189
+ const nLlmW = Math.ceil(Math.floor(bestWidth / patch) / downsample)
190
+ let numTokens = nLlmH * (nLlmW + 1) + 2
191
+ if (nLlmH % 2 === 1) {
192
+ numTokens += nLlmW + 1
193
+ }
194
+ numTokens += (Math.floor((nLlmH + 1) / 2) * (nLlmW + 1) % 2) * 2
195
+ return { nLlmH, nLlmW, numTokens }
196
+ }
197
+
198
+ /** Port of the official `solve_resize_ratio` budget solver. */
199
+ function solveResizeRatio(
200
+ height: number,
201
+ width: number,
202
+ maxNTokens: number,
203
+ ): { nLlmH: number, nLlmW: number, bestHeight: number, bestWidth: number } {
204
+ const { visionPatchSize: patch, visionDownsampleRatio: downsample } = DEEPSEEK_VISION_PROJECTION
205
+ const ratio = height / width
206
+ const maxWFloat = Math.sqrt((maxNTokens - 2) / ratio + 0.25) - 0.5
207
+ const maxHFloat = maxWFloat * ratio
208
+ let bestWidth: number
209
+ let bestHeight: number
210
+ if (maxWFloat < 1.0) {
211
+ const maxW = 1
212
+ let maxH = Math.floor((maxNTokens - 2) / (maxW + 1))
213
+ if (maxH % 2 === 1) maxH -= 1
214
+ bestWidth = maxW * patch * downsample
215
+ bestHeight = maxH * patch * downsample
216
+ } else if (maxHFloat < 2.0) {
217
+ const maxH = 2
218
+ const maxW = Math.floor((maxNTokens - 2) / maxH) - 1
219
+ if (maxW <= 1) throw new Error('DeepSeek vision resize solver produced an invalid width')
220
+ bestWidth = maxW * patch * downsample
221
+ bestHeight = maxH * patch * downsample
222
+ } else {
223
+ const maxW = Math.floor(maxWFloat)
224
+ let maxH = Math.floor(maxHFloat)
225
+ if (maxH % 2 === 1) maxH -= 1
226
+ const beta = Math.min(maxW * patch * downsample / width, maxH * patch * downsample / height)
227
+ bestWidth = Math.floor(width * beta / patch) * patch
228
+ bestHeight = Math.floor(height * beta / patch) * patch
229
+ }
230
+ const grid = gridTokens(bestHeight, bestWidth)
231
+ return { nLlmH: grid.nLlmH, nLlmW: grid.nLlmW, bestHeight, bestWidth }
232
+ }
233
+
234
+ /** Port of the official `safe_resize` loop with the compress-pad budget. */
235
+ function safeResize(
236
+ height: number,
237
+ width: number,
238
+ initialBestHeight: number,
239
+ initialBestWidth: number,
240
+ ): { nLlmH: number, nLlmW: number, bestHeight: number, bestWidth: number } {
241
+ const { visionMaxNTokens } = DEEPSEEK_VISION_PROJECTION
242
+ let budget = visionMaxNTokens - (COMPRESS_PAD_TO - 1)
243
+ let grid = gridTokens(initialBestHeight, initialBestWidth)
244
+ let bestHeight = initialBestHeight
245
+ let bestWidth = initialBestWidth
246
+ while (grid.numTokens > budget) {
247
+ const solved = solveResizeRatio(height, width, budget)
248
+ grid = gridTokens(solved.bestHeight, solved.bestWidth)
249
+ bestHeight = solved.bestHeight
250
+ bestWidth = solved.bestWidth
251
+ budget -= 1
252
+ }
253
+ return { nLlmH: grid.nLlmH, nLlmW: grid.nLlmW, bestHeight, bestWidth }
254
+ }
@@ -0,0 +1,403 @@
1
+ /** Public-API-only measurement adapter for the standalone runtime. */
2
+
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import type { ContentBlock, TokenUsage } from '@deepseek-ai/dsh-llm'
5
+ import { deriveEventMessage } from '@deepseek-ai/dsh-session'
6
+ import type { Session } from '@deepseek-ai/dsh-session'
7
+ import type {} from '@deepseek-ai/dsh-token-meter'
8
+ import type { TokenMeasurement } from '@deepseek-ai/dsh-token-meter'
9
+ import {
10
+ DEEPSEEK_VISION_TOKENIZER_ARTIFACT,
11
+ deepSeekV4TokenizerForModel,
12
+ } from '../deepseek-v4-tokenizer.ts'
13
+ import {
14
+ DEEPSEEK_VISION_IMAGE_ESTIMATOR,
15
+ estimateDeepSeekVisionImageTokens,
16
+ } from './deepseek-v4-vision-tokens.ts'
17
+ import { unavailableTokenCount } from './token-count.ts'
18
+ import { sessionEvents } from './session-events.ts'
19
+ import type {
20
+ CanonicalTextTokenCounter,
21
+ ExactTokenizerTokenCount,
22
+ TokenCount,
23
+ TokenizerEstimateTokenCount,
24
+ } from './token-count.ts'
25
+
26
+ export type { TokenCount } from './token-count.ts'
27
+
28
+ /** Request identity retained only when every dimension is publicly known. */
29
+ export interface ProviderMeasurementKey {
30
+ readonly provider: string
31
+ readonly baseUrlClass: string
32
+ readonly apiRoute: string
33
+ readonly modelId: string
34
+ readonly requestTemplateRevision: string
35
+ readonly tokenizerRevision: string
36
+ readonly modality: string
37
+ }
38
+
39
+ /** Rich request observation used by Adaptive when a future public API supplies it. */
40
+ export interface ObservedPromptUsage {
41
+ readonly attemptId: string
42
+ readonly providerRequestOrdinal: number
43
+ readonly startedAtMs: number
44
+ readonly completedAtMs: number
45
+ readonly measurement: TokenCount
46
+ readonly observedPromptTokens: number
47
+ readonly observedOutputTokens?: number
48
+ readonly responseModelId?: string
49
+ readonly cacheStatus?: 'complete' | 'unknown'
50
+ readonly cacheReadTokens?: number
51
+ readonly cacheMissTokens?: number
52
+ readonly key?: ProviderMeasurementKey
53
+ }
54
+
55
+ /** Metadata-only image dimensions; the pixel payload is never read or logged. */
56
+ export interface CanonicalImageAttachment {
57
+ readonly width: number
58
+ readonly height: number
59
+ }
60
+
61
+ /**
62
+ * Intrinsic-grid diagnostic attached to nodes whose count estimates images.
63
+ * It reports ONLY the official block
64
+ * arithmetic evaluated on the attachment's intrinsic dimensions at the two
65
+ * alignment-padding extremes (compress-pad 0 and 3). It is NOT a request-token
66
+ * bound: the adapter may still re-project the image (per-route pixel-budget
67
+ * or image-detail overrides, byte-cap reprojection), which can move the real
68
+ * count below the diagnostic minimum. It never participates in exact gates,
69
+ * rewrite proofs, or any lossy decision.
70
+ */
71
+ export interface IntrinsicImageBlockDiagnostic {
72
+ readonly paddingMinimumTokens: number
73
+ readonly paddingMaximumTokens: number
74
+ }
75
+
76
+ /** One same-revision surface node with exact, estimated, or unavailable count. */
77
+ export interface MeasuredTokenSurfaceNode {
78
+ readonly seq: number
79
+ readonly count: TokenCount
80
+ /** Intrinsic-grid diagnostic when usable image dimensions were available. */
81
+ readonly intrinsicImageBlockEstimate?: IntrinsicImageBlockDiagnostic
82
+ }
83
+
84
+ /** Compression view derived only from published Session and TokenMeter methods. */
85
+ export interface CompactionTokenView extends TokenMeasurement {
86
+ readonly providerRoute?: string
87
+ readonly modelId?: string
88
+ readonly measuredNodes: readonly MeasuredTokenSurfaceNode[]
89
+ readonly currentSurface: TokenCount
90
+ /** Sum of per-node intrinsic padding minima; a diagnostic, not a token bound. */
91
+ readonly intrinsicImageBlockEstimateTokens: number
92
+ readonly latestEnvelopeKey?: ProviderMeasurementKey
93
+ readonly lastCompletedUsage?: ObservedPromptUsage
94
+ countCanonicalText(text: string): TokenCount
95
+ }
96
+
97
+ /** Text plus image gating for one durable request target. */
98
+ interface CanonicalCounter {
99
+ readonly countText: CanonicalTextTokenCounter
100
+ readonly countImage: (attachment: CanonicalImageAttachment) => TokenCount
101
+ }
102
+
103
+ const VISION_MODEL_ID = DEEPSEEK_VISION_TOKENIZER_ARTIFACT.modelIds[0] as string
104
+
105
+ /**
106
+ * Capture one route-bound view without calling patched Harness methods.
107
+ * Official `measure()` remains authoritative for request pressure; the bundled
108
+ * tokenizer supplies exact canonical content counts used by safe rewrites.
109
+ */
110
+ export function measureForCompaction(ctx: Context, session: Session): CompactionTokenView {
111
+ const header = session.requestHeader()
112
+ const measurement = ctx.tokenMeter.measure(session, header)
113
+ const target = header?.config
114
+ const counter = bindCounter(target?.provider, target?.model)
115
+ const events = sessionEvents(session)
116
+ const measuredNodes = measurement.nodes.map((node): MeasuredTokenSurfaceNode => {
117
+ const event = events[node.seq]
118
+ if (event === undefined) {
119
+ return { seq: node.seq, count: unavailableTokenCount(`surface node ${String(node.seq)} is missing`) }
120
+ }
121
+ const message = deriveEventMessage(event)
122
+ if (message === null) {
123
+ return { seq: node.seq, count: unavailableTokenCount(`surface node ${String(node.seq)} is not model-visible`) }
124
+ }
125
+ const count = countCanonicalContent(message.content, counter, `surface node ${String(node.seq)}`)
126
+ const intrinsicImageBlockEstimate = count.kind === 'tokenizer-estimate'
127
+ ? intrinsicImageDiagnostic(message.content, target)
128
+ : undefined
129
+ return {
130
+ seq: node.seq,
131
+ count,
132
+ ...intrinsicImageBlockEstimate === undefined ? {} : { intrinsicImageBlockEstimate },
133
+ }
134
+ })
135
+ const currentSurface = countSurfaceCounts(
136
+ measuredNodes.map(node => node.count),
137
+ 'current surface',
138
+ )
139
+ const intrinsicImageBlockEstimateTokens = measuredNodes.reduce(
140
+ (sum, node) => sum + (node.intrinsicImageBlockEstimate?.paddingMinimumTokens ?? 0),
141
+ 0,
142
+ )
143
+ return Object.freeze({
144
+ ...measurement,
145
+ ...(target === undefined ? {} : { providerRoute: target.provider, modelId: target.model }),
146
+ measuredNodes: Object.freeze(measuredNodes),
147
+ currentSurface,
148
+ intrinsicImageBlockEstimateTokens,
149
+ countCanonicalText: counter.countText,
150
+ })
151
+ }
152
+
153
+ /** Request-level usage exposed by official TokenMeter, without invented route attribution. */
154
+ export function officialRequestUsage(view: CompactionTokenView): Readonly<TokenUsage> | undefined {
155
+ return view.baseline.kind === 'usage' ? view.baseline.usage : undefined
156
+ }
157
+
158
+ /**
159
+ * Count one canonical content walk in canonical field order.
160
+ *
161
+ * Text, reasoning, tool-call names/arguments, and nested text tool results are
162
+ * counted exactly with one tokenizer identity. Image blocks produce a bounded
163
+ * estimate because the absolute prompt position and the adapter's final
164
+ * projection are not publicly observable. A mixed text/image node is therefore
165
+ * an estimate and never qualifies for an exact rewrite proof.
166
+ */
167
+ function countCanonicalContent(
168
+ blocks: readonly ContentBlock[],
169
+ counter: CanonicalCounter,
170
+ subject: string,
171
+ ): TokenCount {
172
+ let identity: ExactTokenizerTokenCount | undefined
173
+ let estimateIdentity: Pick<TokenizerEstimateTokenCount, 'estimatorId' | 'estimatorRevision'> | undefined
174
+ let tokens = 0
175
+ let upperBoundTokens = 0
176
+ let firstRefusal: TokenCount | undefined
177
+ const absorb = (count: TokenCount): boolean => {
178
+ if (count.kind === 'unavailable') {
179
+ firstRefusal ??= count
180
+ return false
181
+ }
182
+ if (count.kind === 'exact-tokenizer') {
183
+ if (identity !== undefined
184
+ && (identity.tokenizerId !== count.tokenizerId
185
+ || identity.tokenizerRevision !== count.tokenizerRevision)) {
186
+ firstRefusal ??= unavailableTokenCount(`${subject}: tokenizer identity changed within one measurement`)
187
+ return false
188
+ }
189
+ identity ??= count
190
+ tokens += count.tokens
191
+ upperBoundTokens += count.tokens
192
+ } else {
193
+ if (estimateIdentity !== undefined
194
+ && (estimateIdentity.estimatorId !== count.estimatorId
195
+ || estimateIdentity.estimatorRevision !== count.estimatorRevision)) {
196
+ firstRefusal ??= unavailableTokenCount(`${subject}: image estimator identity changed within one measurement`)
197
+ return false
198
+ }
199
+ estimateIdentity ??= {
200
+ estimatorId: count.estimatorId,
201
+ estimatorRevision: count.estimatorRevision,
202
+ }
203
+ tokens += count.tokens
204
+ upperBoundTokens += count.upperBoundTokens
205
+ }
206
+ return Number.isSafeInteger(tokens) && tokens >= 0
207
+ && Number.isSafeInteger(upperBoundTokens) && upperBoundTokens >= tokens
208
+ }
209
+ const walk = (content: readonly ContentBlock[]): boolean => {
210
+ for (const block of content) {
211
+ switch (block.type) {
212
+ case 'text':
213
+ case 'reasoning': {
214
+ if (!absorb(counter.countText(block.text))) return false
215
+ break
216
+ }
217
+ case 'tool-call': {
218
+ if (!absorb(counter.countText(block.name))) return false
219
+ if (!absorb(counter.countText(block.arguments))) return false
220
+ break
221
+ }
222
+ case 'tool-result': {
223
+ if (!walk(block.content)) return false
224
+ break
225
+ }
226
+ case 'image': {
227
+ if (!absorb(counter.countImage(block.attachment))) return false
228
+ break
229
+ }
230
+ default: {
231
+ firstRefusal ??= unavailableTokenCount(`${subject}: contains an unsupported content block`)
232
+ return false
233
+ }
234
+ }
235
+ }
236
+ return true
237
+ }
238
+ if (!walk(blocks)) {
239
+ if (firstRefusal?.kind === 'unavailable') {
240
+ return unavailableTokenCount(`${subject}: ${firstRefusal.reason}`)
241
+ }
242
+ return unavailableTokenCount(`${subject}: contains content the canonical counter cannot count exactly`)
243
+ }
244
+ if (identity === undefined && estimateIdentity === undefined) {
245
+ // Empty content is a legal durable shape; it keeps the exact-0 count with
246
+ // the bound tokenizer identity instead of fail-opening the whole surface.
247
+ const empty = counter.countText('')
248
+ if (empty.kind !== 'exact-tokenizer') return empty
249
+ return empty
250
+ }
251
+ if (!Number.isSafeInteger(tokens) || tokens < 0) {
252
+ return unavailableTokenCount(`${subject}: invalid token sum`)
253
+ }
254
+ if (estimateIdentity !== undefined) {
255
+ return Object.freeze({
256
+ kind: 'tokenizer-estimate',
257
+ ...estimateIdentity,
258
+ tokens,
259
+ upperBoundTokens,
260
+ })
261
+ }
262
+ if (identity === undefined) return unavailableTokenCount(`${subject}: no tokenizer identity`)
263
+ return Object.freeze({ ...identity, tokens })
264
+ }
265
+
266
+ function bindCounter(provider: string | undefined, model: string | undefined): CanonicalCounter {
267
+ if (provider === undefined || model === undefined) {
268
+ const unavailable = () => unavailableTokenCount('canonical text: no durable provider/model request header')
269
+ return { countText: unavailable, countImage: () => unavailableTokenCount('canonical image: no durable provider/model request header') }
270
+ }
271
+ if (provider !== 'deepseek' && provider !== 'deepseek-official') {
272
+ const reason = `canonical text: provider "${provider}" is not the supported DeepSeek route`
273
+ return { countText: () => unavailableTokenCount(reason), countImage: () => unavailableTokenCount(`canonical image: provider "${provider}" is not the supported DeepSeek route`) }
274
+ }
275
+ const tokenizer = deepSeekV4TokenizerForModel(model)
276
+ if (tokenizer === undefined) {
277
+ const reason = `canonical text: no verified bundled tokenizer for model "${model}"`
278
+ return {
279
+ countText: () => unavailableTokenCount(reason),
280
+ // Image arithmetic is independently pinned and does not need the text
281
+ // tokenizer asset. A pure-image node can therefore retain its estimate
282
+ // even when text measurement fails closed.
283
+ countImage: attachment => countCanonicalImage(model, attachment),
284
+ }
285
+ }
286
+ return {
287
+ countText: (text: string) => tokenizer.countText(text),
288
+ countImage: attachment => countCanonicalImage(model, attachment),
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Images never claim an exact count. The official expansion depends on the
294
+ * absolute prompt position (system prompt, chat-template framing, adapter
295
+ * image handles) and on the adapter's final request-image projection, neither
296
+ * of which is exposed through a public API; a route may even override the
297
+ * pixel budget or re-project under the byte cap. Valid dimensions therefore
298
+ * use the midpoint of the four alignment residues as a bounded estimate;
299
+ * malformed dimensions use a fixed default. Estimate-bearing nodes remain
300
+ * ineligible for exact rewrite proofs.
301
+ */
302
+ function countCanonicalImage(
303
+ model: string,
304
+ attachment: CanonicalImageAttachment,
305
+ ): TokenCount {
306
+ if (model !== VISION_MODEL_ID) {
307
+ return unavailableTokenCount(`canonical image: model "${model}" has no vision image counter`)
308
+ }
309
+ const estimate = estimateDeepSeekVisionImageTokens(attachment.width, attachment.height)
310
+ return Object.freeze({
311
+ kind: 'tokenizer-estimate',
312
+ tokens: estimate.tokens,
313
+ upperBoundTokens: estimate.upperBoundTokens,
314
+ estimatorId: DEEPSEEK_VISION_IMAGE_ESTIMATOR.id,
315
+ estimatorRevision: DEEPSEEK_VISION_IMAGE_ESTIMATOR.revision,
316
+ })
317
+ }
318
+
319
+ /**
320
+ * Intrinsic-grid diagnostic for one content walk: the official block
321
+ * arithmetic on intrinsic dimensions at both alignment extremes. Only images
322
+ * on the pinned DeepSeek vision route with usable metadata contribute.
323
+ */
324
+ function intrinsicImageDiagnostic(
325
+ blocks: readonly ContentBlock[],
326
+ target: { readonly provider: string, readonly model: string } | undefined,
327
+ ): IntrinsicImageBlockDiagnostic | undefined {
328
+ // Mirror the gating route: only the DeepSeek vision route carries the
329
+ // official-arithmetic bounds.
330
+ if (target === undefined
331
+ || (target.provider !== 'deepseek' && target.provider !== 'deepseek-official')
332
+ || target.model !== VISION_MODEL_ID) return undefined
333
+ let paddingMinimumTokens = 0
334
+ let paddingMaximumTokens = 0
335
+ let seen = false
336
+ const walk = (content: readonly ContentBlock[]): void => {
337
+ for (const block of content) {
338
+ if (block.type === 'image') {
339
+ const { width, height } = block.attachment
340
+ const estimate = estimateDeepSeekVisionImageTokens(width, height)
341
+ if (estimate.source !== 'intrinsic-grid'
342
+ || estimate.paddingMinimumTokens === undefined
343
+ || estimate.paddingMaximumTokens === undefined) continue
344
+ paddingMinimumTokens += estimate.paddingMinimumTokens
345
+ paddingMaximumTokens += estimate.paddingMaximumTokens
346
+ seen = true
347
+ } else if (block.type === 'tool-result') {
348
+ walk(block.content)
349
+ }
350
+ }
351
+ }
352
+ walk(blocks)
353
+ return seen ? Object.freeze({ paddingMinimumTokens, paddingMaximumTokens }) : undefined
354
+ }
355
+
356
+ function countSurfaceCounts(counts: readonly TokenCount[], subject: string): TokenCount {
357
+ if (counts.length === 0) return unavailableTokenCount(`${subject}: no surface nodes`)
358
+ let identity: Extract<TokenCount, { kind: 'exact-tokenizer' }> | undefined
359
+ let estimateIdentity: Pick<TokenizerEstimateTokenCount, 'estimatorId' | 'estimatorRevision'> | undefined
360
+ let tokens = 0
361
+ let upperBoundTokens = 0
362
+ for (const count of counts) {
363
+ if (count.kind === 'unavailable') {
364
+ return unavailableTokenCount(`${subject}: ${count.reason}`)
365
+ }
366
+ if (count.kind === 'exact-tokenizer') {
367
+ if (identity !== undefined
368
+ && (identity.tokenizerId !== count.tokenizerId
369
+ || identity.tokenizerRevision !== count.tokenizerRevision)) {
370
+ return unavailableTokenCount(`${subject}: tokenizer identity changed within one measurement`)
371
+ }
372
+ identity ??= count
373
+ tokens += count.tokens
374
+ upperBoundTokens += count.tokens
375
+ } else {
376
+ if (estimateIdentity !== undefined
377
+ && (estimateIdentity.estimatorId !== count.estimatorId
378
+ || estimateIdentity.estimatorRevision !== count.estimatorRevision)) {
379
+ return unavailableTokenCount(`${subject}: image estimator identity changed within one measurement`)
380
+ }
381
+ estimateIdentity ??= {
382
+ estimatorId: count.estimatorId,
383
+ estimatorRevision: count.estimatorRevision,
384
+ }
385
+ tokens += count.tokens
386
+ upperBoundTokens += count.upperBoundTokens
387
+ }
388
+ }
389
+ if (!Number.isSafeInteger(tokens) || tokens < 0
390
+ || !Number.isSafeInteger(upperBoundTokens) || upperBoundTokens < tokens) {
391
+ return unavailableTokenCount(`${subject}: invalid token sum`)
392
+ }
393
+ if (estimateIdentity !== undefined) {
394
+ return Object.freeze({
395
+ kind: 'tokenizer-estimate',
396
+ ...estimateIdentity,
397
+ tokens,
398
+ upperBoundTokens,
399
+ })
400
+ }
401
+ if (identity === undefined) return unavailableTokenCount(`${subject}: no tokenizer identity`)
402
+ return Object.freeze({ ...identity, tokens })
403
+ }