dsh-context 0.22.2 → 0.24.0
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/README.md +3 -1
- package/lib/client.js +384 -81
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +6 -0
- package/lib/index.js +150 -1
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -329,6 +329,12 @@ interface TimelineState {
|
|
|
329
329
|
cost?: SessionCostUsage;
|
|
330
330
|
/** Newest `gone` among archive entries dropped by the retention bounds. */
|
|
331
331
|
archiveFloor?: number;
|
|
332
|
+
/**
|
|
333
|
+
* Tool callId → name, armed by `tool/call` and DELETED when its
|
|
334
|
+
* `tool/result` folds in (one result per call, in log order) — the map
|
|
335
|
+
* stays at pending-call size instead of growing for the session's whole
|
|
336
|
+
* lifetime (it is persisted state, shallow-copied by every fold step).
|
|
337
|
+
*/
|
|
332
338
|
callNames: Record<string, string>;
|
|
333
339
|
/**
|
|
334
340
|
* Seq list of the surface nodes the next replacement will shadow, armed by
|
package/lib/index.js
CHANGED
|
@@ -39,11 +39,149 @@ function resolveBounds(config) {
|
|
|
39
39
|
return Config.parse(config ?? {});
|
|
40
40
|
}
|
|
41
41
|
//#endregion
|
|
42
|
+
//#region src/shared/imageTokens.ts
|
|
43
|
+
/**
|
|
44
|
+
* Per-image token estimate for DeepSeek's vision model — a faithful port of
|
|
45
|
+
* the official "图片 Token 计算器" (Image Token Calculator) shipped on the
|
|
46
|
+
* DeepSeek API docs (https://api-docs.deepseek.com/zh-cn/quick_start/token_usage),
|
|
47
|
+
* which implements the provider's own image→token conversion:
|
|
48
|
+
*
|
|
49
|
+
* - every image is aspect-preserved rescaled before entering the model:
|
|
50
|
+
* below ~384×384 total pixels it is enlarged, above it is shrunk;
|
|
51
|
+
* - tokens follow the patch grid (patch 14px, downsample 3), so every
|
|
52
|
+
* image costs at least 117 and at most ~384 tokens (the documented cap).
|
|
53
|
+
*
|
|
54
|
+
* Verified against the docs calculator itself: 2048×1365→313, 800×600→341,
|
|
55
|
+
* 2048×2048→349, 512×512→201, 100×100→117, 1920×1080→369, 400×900→249.
|
|
56
|
+
* The DSH request pipeline's own 640k-pixel pre-resize does not change the
|
|
57
|
+
* result (the provider formula rescales to the same patch grid), so the
|
|
58
|
+
* durable attachment dimensions can be fed in directly.
|
|
59
|
+
*
|
|
60
|
+
* Pure math shared by the Host fold (message pricing) and the Client
|
|
61
|
+
* (attachment card token badges) — no dependencies, never mutates.
|
|
62
|
+
*/
|
|
63
|
+
const PATCH_SIZE = 14;
|
|
64
|
+
const DOWNSAMPLE_RATIO = 3;
|
|
65
|
+
const MAX_WH_RATIO = 8;
|
|
66
|
+
/** ~384×384 total pixels: smaller images are enlarged before patching. */
|
|
67
|
+
const MIN_PIXELS = 147456;
|
|
68
|
+
const floorDiv = (a, b) => Math.floor(a / b);
|
|
69
|
+
const ceilDiv = (a, b) => Math.floor((a + b - 1) / b);
|
|
70
|
+
/** Token count of one patch grid (rows×cols) under the v4 layout rule. */
|
|
71
|
+
function gridTokens(rows, cols) {
|
|
72
|
+
let n = rows * (cols + 1) + 2;
|
|
73
|
+
if (rows % 2 === 1) n += cols + 1;
|
|
74
|
+
n += ceilDiv(rows, 2) * (cols + 1) % 2 * 2;
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
/** Solve the largest in-grid resize whose token count fits `budget`. */
|
|
78
|
+
function solveResizeRatio(height, width, budget) {
|
|
79
|
+
const ratio = height / width;
|
|
80
|
+
const gridW = Math.sqrt((budget - 2) / ratio + .25) - .5;
|
|
81
|
+
const gridH = gridW * ratio;
|
|
82
|
+
const unit = 42;
|
|
83
|
+
let bestHeight;
|
|
84
|
+
let bestWidth;
|
|
85
|
+
if (gridW < 1) {
|
|
86
|
+
let rows = floorDiv(budget - 2, 2);
|
|
87
|
+
if (rows % 2 === 1) rows -= 1;
|
|
88
|
+
bestWidth = unit;
|
|
89
|
+
bestHeight = rows * unit;
|
|
90
|
+
} else if (gridH < 2) {
|
|
91
|
+
const cols = floorDiv(budget - 2, 2) - 1;
|
|
92
|
+
if (cols <= 1) throw new Error("image tokens: budget too small to solve");
|
|
93
|
+
bestHeight = 84;
|
|
94
|
+
bestWidth = cols * unit;
|
|
95
|
+
} else {
|
|
96
|
+
const cols = Math.trunc(gridW);
|
|
97
|
+
let rows = Math.trunc(gridH);
|
|
98
|
+
if (rows % 2 === 1) rows -= 1;
|
|
99
|
+
const scale = Math.min(cols * unit / width, rows * unit / height);
|
|
100
|
+
bestWidth = Math.trunc(width * scale / PATCH_SIZE) * PATCH_SIZE;
|
|
101
|
+
bestHeight = Math.trunc(height * scale / PATCH_SIZE) * PATCH_SIZE;
|
|
102
|
+
}
|
|
103
|
+
const nLlmH = ceilDiv(floorDiv(bestHeight, PATCH_SIZE), DOWNSAMPLE_RATIO);
|
|
104
|
+
const nLlmW = ceilDiv(floorDiv(bestWidth, PATCH_SIZE), DOWNSAMPLE_RATIO);
|
|
105
|
+
return {
|
|
106
|
+
nLlmH,
|
|
107
|
+
nLlmW,
|
|
108
|
+
bestHeight,
|
|
109
|
+
bestWidth,
|
|
110
|
+
numTokens: gridTokens(nLlmH, nLlmW)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Resize so the patch grid fits the cap, then re-add the pad reserve. */
|
|
114
|
+
function safeResize(height, width, paddedHeight, paddedWidth) {
|
|
115
|
+
const nLlmH = ceilDiv(floorDiv(paddedHeight, PATCH_SIZE), DOWNSAMPLE_RATIO);
|
|
116
|
+
const nLlmW = ceilDiv(floorDiv(paddedWidth, PATCH_SIZE), DOWNSAMPLE_RATIO);
|
|
117
|
+
const pad = 3;
|
|
118
|
+
const budget = 381;
|
|
119
|
+
let result = {
|
|
120
|
+
nLlmH,
|
|
121
|
+
nLlmW,
|
|
122
|
+
bestHeight: paddedHeight,
|
|
123
|
+
bestWidth: paddedWidth,
|
|
124
|
+
numTokens: gridTokens(nLlmH, nLlmW)
|
|
125
|
+
};
|
|
126
|
+
if (result.numTokens > budget) {
|
|
127
|
+
result = solveResizeRatio(height, width, budget);
|
|
128
|
+
let nextBudget = budget;
|
|
129
|
+
while (result.numTokens > budget) {
|
|
130
|
+
nextBudget -= 1;
|
|
131
|
+
result = solveResizeRatio(height, width, nextBudget);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
result.numTokens += pad;
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
function calcResizeInner(width, height) {
|
|
138
|
+
let w = width;
|
|
139
|
+
let h = height;
|
|
140
|
+
if (w > h * MAX_WH_RATIO) w = h * MAX_WH_RATIO;
|
|
141
|
+
const pixels = w * h;
|
|
142
|
+
if (pixels < MIN_PIXELS && pixels > 0) {
|
|
143
|
+
const scale = Math.sqrt(MIN_PIXELS / pixels);
|
|
144
|
+
w = Math.trunc(w * scale);
|
|
145
|
+
h = Math.trunc(h * scale);
|
|
146
|
+
}
|
|
147
|
+
const paddedWidth = ceilDiv(w, PATCH_SIZE) * PATCH_SIZE;
|
|
148
|
+
const paddedHeight = ceilDiv(h, PATCH_SIZE) * PATCH_SIZE;
|
|
149
|
+
return safeResize(h, w, paddedHeight, paddedWidth);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Estimate the tokens one image consumes in a DeepSeek vision request, from
|
|
153
|
+
* its pixel dimensions. Returns null for non-positive/non-finite dimensions
|
|
154
|
+
* or when the official iteration fails to converge — callers fall back to
|
|
155
|
+
* the generic structural price.
|
|
156
|
+
*/
|
|
157
|
+
function estimateImageTokens(width, height) {
|
|
158
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
|
|
159
|
+
try {
|
|
160
|
+
let result = calcResizeInner(width, height);
|
|
161
|
+
for (let i = 1; i < 10; i++) {
|
|
162
|
+
const next = calcResizeInner(result.bestWidth, result.bestHeight);
|
|
163
|
+
if (next.nLlmH === result.nLlmH && next.nLlmW === result.nLlmW && next.bestHeight === result.bestHeight && next.bestWidth === result.bestWidth && next.numTokens === result.numTokens) return result.numTokens;
|
|
164
|
+
result = next;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
42
172
|
//#region src/host/pricing.ts
|
|
43
173
|
/**
|
|
44
174
|
* Token pricing — the same fixed-density heuristic as the harness's own
|
|
45
175
|
* token-meter (`dsh-token-meter/estimate.ts`): ~4 chars ≈ 1 token, +4 per
|
|
46
176
|
* content block, +4 role framing. Pure functions over message payloads.
|
|
177
|
+
*
|
|
178
|
+
* One deliberate refinement over the meter: `image` blocks. The meter prices
|
|
179
|
+
* them through its generic JSON branch (~40 tokens for the durable ref),
|
|
180
|
+
* while DeepSeek's vision model actually bills 117-384 tokens per image by
|
|
181
|
+
* pixel dimensions (https://api-docs.deepseek.com/zh-cn/guides/vision/).
|
|
182
|
+
* Image blocks therefore price through the official docs calculator port
|
|
183
|
+
* (shared/imageTokens.ts), falling back to the meter's JSON price when the
|
|
184
|
+
* attachment's dimensions are unknown.
|
|
47
185
|
*/
|
|
48
186
|
const CHARS_PER_TOKEN = 4;
|
|
49
187
|
const BLOCK_OVERHEAD = 4;
|
|
@@ -66,6 +204,12 @@ function estimateBlocks(blocks) {
|
|
|
66
204
|
case "tool-result":
|
|
67
205
|
tokens += estimateBlocks(block.content) + BLOCK_OVERHEAD;
|
|
68
206
|
break;
|
|
207
|
+
case "image": {
|
|
208
|
+
const ref = block.attachment;
|
|
209
|
+
const priced = ref !== null && typeof ref === "object" && typeof ref.width === "number" && typeof ref.height === "number" ? estimateImageTokens(ref.width, ref.height) : null;
|
|
210
|
+
tokens += (priced ?? Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN)) + BLOCK_OVERHEAD;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
69
213
|
default: tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN);
|
|
70
214
|
}
|
|
71
215
|
return tokens;
|
|
@@ -304,6 +448,11 @@ function applySurface(st, ev, type, data, message) {
|
|
|
304
448
|
const blockId = (message?.content?.[0])?.toolCallId;
|
|
305
449
|
if (srcName) node.tool = srcName;
|
|
306
450
|
else if (typeof blockId === "string") node.tool = st.callNames[blockId];
|
|
451
|
+
if (typeof srcId === "string" || typeof blockId === "string") {
|
|
452
|
+
const kept = {};
|
|
453
|
+
for (const k in st.callNames) if (k !== srcId && k !== blockId) kept[k] = st.callNames[k];
|
|
454
|
+
st.callNames = kept;
|
|
455
|
+
}
|
|
307
456
|
if (data?.error) node.err = true;
|
|
308
457
|
} else if (source?.kind === "skill-invocation") node.skill = typeof source.name === "string" ? source.name : "?";
|
|
309
458
|
else if (source?.kind === "plugin") {
|
|
@@ -791,7 +940,7 @@ function createContextTimelineDefinition(config) {
|
|
|
791
940
|
},
|
|
792
941
|
init: () => createTimelineState(),
|
|
793
942
|
apply: (state, event) => applyTimeline(state, event, bounds),
|
|
794
|
-
stateVersion:
|
|
943
|
+
stateVersion: 6
|
|
795
944
|
};
|
|
796
945
|
}
|
|
797
946
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.",
|
|
5
5
|
"author": "bowenliang123",
|
|
6
6
|
"repository": {
|