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,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
|
+
}
|