@goodandready/dsh-dsml-artifact-guard 0.2.13 → 0.2.14
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/CHANGELOG.md +23 -107
- package/README.md +22 -100
- package/README.ru.md +27 -81
- package/README.zh.md +33 -87
- package/lib/client.js +303 -20
- package/lib/index.js +118 -8
- package/lib/telemetry.js +128 -0
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery';
|
|
2
2
|
import { registerPluginUpdater } from './updater.js';
|
|
3
|
+
import { registerPluginTelemetry, recordArtifact } from './telemetry.js';
|
|
3
4
|
|
|
4
5
|
const TAIL = /(?:<\/|DSML|parameter>\s*)?(?:<\/|DSML|invoke>\s*)?<\/|DSML|tool_calls>\s*$/u;
|
|
6
|
+
const INCOMPLETE_TAIL = /(?:<\/|DSML|(?:parameter|invoke|tool_calls)>\s*)*(?:<\/|DSML(?:|(?:tool_calls|invoke|parameter)?)?>?)\s*$/u;
|
|
5
7
|
const OPEN_TAG = /<|DSML|(?:tool_calls|invoke|parameter)[^>]*>/gu;
|
|
6
8
|
const CLOSE_TAG = /<\/|DSML|(?:tool_calls|invoke|parameter)>/gu;
|
|
9
|
+
const OPEN_THINK = /<think(?:>|\s[^>]*>)/gi;
|
|
10
|
+
const CLOSE_THINK = /<\/think>/gi;
|
|
11
|
+
const TRAILING_THINK_MARKERS = /(?:<\/think>|<|end of sentence|>|<|thought|>|<\/|thought|>)\s*$/u;
|
|
7
12
|
const KEEP = 96;
|
|
8
13
|
|
|
9
14
|
export const name = '@goodandready/dsh-dsml-artifact-guard';
|
|
@@ -16,6 +21,7 @@ export const Config = Schema.object({
|
|
|
16
21
|
mode: Schema.string().default('sanitize').volatile().description('How to handle detected artifact markup'),
|
|
17
22
|
modelPattern: Schema.string().default('deepseek').volatile().description('Model name substring this plugin guards'),
|
|
18
23
|
providers: Schema.array(Schema.string()).default([]).volatile().description('Restrict guarding to these providers; empty means all'),
|
|
24
|
+
cleanThinkArtifacts: Schema.boolean().default(false).volatile().description('Clean leaked reasoning closing tags (</think>) and trailing incomplete tokens'),
|
|
19
25
|
providerId: Schema.string().volatile().description('Legacy exact provider id match'),
|
|
20
26
|
modelId: Schema.string().volatile().description('Legacy exact model id match'),
|
|
21
27
|
});
|
|
@@ -62,12 +68,20 @@ export function matchesScope(o, c) {
|
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
export async function* sanitizeDsmlArtifacts(s, {
|
|
71
|
+
export async function* sanitizeDsmlArtifacts(s, {
|
|
72
|
+
mode = 'sanitize',
|
|
73
|
+
cleanThinkArtifacts = false,
|
|
74
|
+
onArtifact,
|
|
75
|
+
provider = '',
|
|
76
|
+
model = '',
|
|
77
|
+
} = {}) {
|
|
66
78
|
let p = '';
|
|
67
79
|
let i;
|
|
68
80
|
let scanBuffer = '';
|
|
69
81
|
let totalOpen = 0;
|
|
70
82
|
let totalClose = 0;
|
|
83
|
+
let totalOpenThink = 0;
|
|
84
|
+
let totalCloseThink = 0;
|
|
71
85
|
|
|
72
86
|
const countTags = (str) => {
|
|
73
87
|
if (!str) return;
|
|
@@ -75,6 +89,12 @@ export async function* sanitizeDsmlArtifacts(s, { mode = 'sanitize', onArtifact
|
|
|
75
89
|
if (opens) totalOpen += opens.length;
|
|
76
90
|
const closes = str.match(CLOSE_TAG);
|
|
77
91
|
if (closes) totalClose += closes.length;
|
|
92
|
+
if (cleanThinkArtifacts) {
|
|
93
|
+
const thinkOpens = str.match(OPEN_THINK);
|
|
94
|
+
if (thinkOpens) totalOpenThink += thinkOpens.length;
|
|
95
|
+
const thinkCloses = str.match(CLOSE_THINK);
|
|
96
|
+
if (thinkCloses) totalCloseThink += thinkCloses.length;
|
|
97
|
+
}
|
|
78
98
|
};
|
|
79
99
|
|
|
80
100
|
const flush = async function* (end) {
|
|
@@ -85,15 +105,86 @@ export async function* sanitizeDsmlArtifacts(s, { mode = 'sanitize', onArtifact
|
|
|
85
105
|
countTags(scanBuffer);
|
|
86
106
|
scanBuffer = '';
|
|
87
107
|
}
|
|
108
|
+
|
|
109
|
+
// 1. Complete DSML closing tags tail
|
|
88
110
|
if (TAIL.test(text)) {
|
|
89
111
|
const tailMatch = text.match(TAIL);
|
|
90
112
|
const tailCloseCount = tailMatch ? (tailMatch[0].match(CLOSE_TAG) || []).length : 0;
|
|
91
113
|
const closedBeforeTail = totalClose - tailCloseCount;
|
|
92
114
|
const unclosedBeforeTail = totalOpen - closedBeforeTail;
|
|
93
115
|
if (unclosedBeforeTail <= 0) {
|
|
94
|
-
|
|
116
|
+
const removedSample = tailMatch ? tailMatch[0] : '';
|
|
117
|
+
onArtifact?.({
|
|
118
|
+
mode,
|
|
119
|
+
removed: tailCloseCount || 1,
|
|
120
|
+
sample: removedSample,
|
|
121
|
+
provider,
|
|
122
|
+
model,
|
|
123
|
+
type: 'dsml',
|
|
124
|
+
});
|
|
95
125
|
if (mode === 'sanitize') text = text.replace(TAIL, '');
|
|
96
126
|
}
|
|
127
|
+
} else if (INCOMPLETE_TAIL.test(text)) {
|
|
128
|
+
// 2. Trailing incomplete DSML closing tags at finish
|
|
129
|
+
const incompMatch = text.match(INCOMPLETE_TAIL);
|
|
130
|
+
const incompCloses = incompMatch ? (incompMatch[0].match(CLOSE_TAG) || []).length : 0;
|
|
131
|
+
const closedBefore = totalClose - incompCloses;
|
|
132
|
+
const unclosedBefore = totalOpen - closedBefore;
|
|
133
|
+
if (unclosedBefore <= 0) {
|
|
134
|
+
const removedSample = incompMatch ? incompMatch[0] : '';
|
|
135
|
+
onArtifact?.({
|
|
136
|
+
mode,
|
|
137
|
+
removed: Math.max(incompCloses, 1),
|
|
138
|
+
sample: removedSample,
|
|
139
|
+
provider,
|
|
140
|
+
model,
|
|
141
|
+
type: 'incomplete-dsml',
|
|
142
|
+
});
|
|
143
|
+
if (mode === 'sanitize') text = text.replace(INCOMPLETE_TAIL, '');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 3. Reasoning think artifacts (when cleanThinkArtifacts is enabled)
|
|
148
|
+
if (cleanThinkArtifacts) {
|
|
149
|
+
// A. Trailing think markers at end of stream
|
|
150
|
+
if (TRAILING_THINK_MARKERS.test(text)) {
|
|
151
|
+
const markerMatch = text.match(TRAILING_THINK_MARKERS);
|
|
152
|
+
const sample = markerMatch ? markerMatch[0] : '';
|
|
153
|
+
onArtifact?.({
|
|
154
|
+
mode,
|
|
155
|
+
removed: 1,
|
|
156
|
+
sample,
|
|
157
|
+
provider,
|
|
158
|
+
model,
|
|
159
|
+
type: 'think-marker',
|
|
160
|
+
});
|
|
161
|
+
if (mode === 'sanitize') text = text.replace(TRAILING_THINK_MARKERS, '');
|
|
162
|
+
}
|
|
163
|
+
// B. Orphan </think> when no <think> was opened in the stream
|
|
164
|
+
if (totalOpenThink === 0 && CLOSE_THINK.test(text)) {
|
|
165
|
+
const thinkMatches = text.match(CLOSE_THINK) || [];
|
|
166
|
+
onArtifact?.({
|
|
167
|
+
mode,
|
|
168
|
+
removed: thinkMatches.length,
|
|
169
|
+
sample: '</think>',
|
|
170
|
+
provider,
|
|
171
|
+
model,
|
|
172
|
+
type: 'think-orphan',
|
|
173
|
+
});
|
|
174
|
+
if (mode === 'sanitize') text = text.replace(/<\/think>\s*/gi, '');
|
|
175
|
+
}
|
|
176
|
+
// C. Unclosed <think> block at stream end: auto-close to heal Markdown
|
|
177
|
+
if (totalOpenThink > totalCloseThink) {
|
|
178
|
+
onArtifact?.({
|
|
179
|
+
mode,
|
|
180
|
+
removed: 0,
|
|
181
|
+
sample: 'auto-closed unclosed <think>',
|
|
182
|
+
provider,
|
|
183
|
+
model,
|
|
184
|
+
type: 'think-repair',
|
|
185
|
+
});
|
|
186
|
+
if (mode === 'sanitize') text += '\n</think>\n';
|
|
187
|
+
}
|
|
97
188
|
}
|
|
98
189
|
}
|
|
99
190
|
if (text) yield { type: 'text-delta', index: i, text };
|
|
@@ -191,6 +282,7 @@ export function apply(ctx, initialConfig = {}) {
|
|
|
191
282
|
if (ctx.settings && typeof ctx.settings.configure === 'function') {
|
|
192
283
|
ctx.effect(() => ctx.settings.configure({ auto: false }, ctx.fiber), 'dsh-dsml-artifact-guard: settings policy');
|
|
193
284
|
}
|
|
285
|
+
|
|
194
286
|
// Register one-click updater endpoint
|
|
195
287
|
if (ctx.webServer && typeof ctx.webServer.register === 'function') {
|
|
196
288
|
const mountUpdater = () => registerPluginUpdater(ctx, {
|
|
@@ -203,18 +295,36 @@ export function apply(ctx, initialConfig = {}) {
|
|
|
203
295
|
} else {
|
|
204
296
|
mountUpdater();
|
|
205
297
|
}
|
|
298
|
+
|
|
299
|
+
const mountTelemetry = () => registerPluginTelemetry(ctx, {
|
|
300
|
+
endpoint: '/api/dsh-dsml-artifact-guard/stats',
|
|
301
|
+
});
|
|
302
|
+
if (typeof ctx.effect === 'function') {
|
|
303
|
+
ctx.effect(mountTelemetry, 'dsh-dsml-artifact-guard: plugin telemetry');
|
|
304
|
+
} else {
|
|
305
|
+
mountTelemetry();
|
|
306
|
+
}
|
|
206
307
|
}
|
|
207
308
|
|
|
208
309
|
// Hook into llm/stream with ctx.effect lifecycle cleanup
|
|
209
310
|
ctx.effect(() => ctx.on('llm/stream', (o, next) => {
|
|
210
311
|
const current = getConfig();
|
|
211
312
|
const s = next(o);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
313
|
+
if (current.mode === 'disabled' || !matchesScope(o, current)) {
|
|
314
|
+
return s;
|
|
315
|
+
}
|
|
316
|
+
const provider = String(o?.providerId ?? o?.provider?.id ?? o?.provider ?? '').trim();
|
|
317
|
+
const model = String(o?.modelId ?? o?.model?.id ?? o?.model?.name ?? o?.model ?? '').trim();
|
|
318
|
+
return sanitizeDsmlArtifacts(s, {
|
|
319
|
+
mode: current.mode,
|
|
320
|
+
cleanThinkArtifacts: Boolean(current.cleanThinkArtifacts),
|
|
321
|
+
provider,
|
|
322
|
+
model,
|
|
323
|
+
onArtifact: (x) => {
|
|
324
|
+
recordArtifact(x);
|
|
325
|
+
ctx.logger?.info?.('DSML closing-tag artifact detected', x);
|
|
326
|
+
},
|
|
327
|
+
});
|
|
218
328
|
}));
|
|
219
329
|
}
|
|
220
330
|
|
package/lib/telemetry.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { checkRequestMethod } from './index.js';
|
|
2
|
+
import { isLoopback } from './updater.js';
|
|
3
|
+
|
|
4
|
+
const STATS_HEADER = 'x-dsh-plugin-stats';
|
|
5
|
+
const MAX_RECENT_EVENTS = 20;
|
|
6
|
+
|
|
7
|
+
let stats = {
|
|
8
|
+
totalCleaned: 0,
|
|
9
|
+
sessionsGuarded: 0,
|
|
10
|
+
lastCleanedAt: null,
|
|
11
|
+
lastModel: null,
|
|
12
|
+
lastProvider: null,
|
|
13
|
+
recent: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function recordArtifact({ mode = 'sanitize', removed = 1, sample = '', provider = '', model = '', type = 'dsml' } = {}) {
|
|
17
|
+
const count = Number(removed) > 0 ? Number(removed) : 1;
|
|
18
|
+
stats.totalCleaned += count;
|
|
19
|
+
stats.sessionsGuarded += 1;
|
|
20
|
+
stats.lastCleanedAt = new Date().toISOString();
|
|
21
|
+
if (model) stats.lastModel = String(model);
|
|
22
|
+
if (provider) stats.lastProvider = String(provider);
|
|
23
|
+
|
|
24
|
+
const event = {
|
|
25
|
+
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
26
|
+
timestamp: stats.lastCleanedAt,
|
|
27
|
+
mode: String(mode),
|
|
28
|
+
removed: count,
|
|
29
|
+
type: String(type),
|
|
30
|
+
sample: typeof sample === 'string' ? sample.slice(0, 160) : '',
|
|
31
|
+
provider: String(provider || ''),
|
|
32
|
+
model: String(model || ''),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
stats.recent.unshift(event);
|
|
36
|
+
if (stats.recent.length > MAX_RECENT_EVENTS) {
|
|
37
|
+
stats.recent.length = MAX_RECENT_EVENTS;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getTelemetryStats() {
|
|
42
|
+
return {
|
|
43
|
+
totalCleaned: stats.totalCleaned,
|
|
44
|
+
sessionsGuarded: stats.sessionsGuarded,
|
|
45
|
+
lastCleanedAt: stats.lastCleanedAt,
|
|
46
|
+
lastModel: stats.lastModel,
|
|
47
|
+
lastProvider: stats.lastProvider,
|
|
48
|
+
recent: [...stats.recent],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resetTelemetryStats() {
|
|
53
|
+
stats = {
|
|
54
|
+
totalCleaned: 0,
|
|
55
|
+
sessionsGuarded: 0,
|
|
56
|
+
lastCleanedAt: null,
|
|
57
|
+
lastModel: null,
|
|
58
|
+
lastProvider: null,
|
|
59
|
+
recent: [],
|
|
60
|
+
};
|
|
61
|
+
return getTelemetryStats();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function header(request, name) {
|
|
65
|
+
const value = request?.headers?.[name];
|
|
66
|
+
return Array.isArray(value) ? value[0] : value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isTrustedStatsRequest(request) {
|
|
70
|
+
if (header(request, STATS_HEADER) !== '1') return false;
|
|
71
|
+
const site = header(request, 'sec-fetch-site');
|
|
72
|
+
if (site !== undefined && site !== 'same-origin') return false;
|
|
73
|
+
const origin = header(request, 'origin');
|
|
74
|
+
const host = header(request, 'host');
|
|
75
|
+
if (origin === undefined || host === undefined) return false;
|
|
76
|
+
try {
|
|
77
|
+
const url = new URL(origin);
|
|
78
|
+
if (url.protocol === 'dsh-app:' && url.hostname === 'app') {
|
|
79
|
+
return isLoopback(request.socket?.remoteAddress);
|
|
80
|
+
}
|
|
81
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
|
|
82
|
+
if (url.host.toLowerCase() !== host.toLowerCase()) return false;
|
|
83
|
+
if (isLoopback(url.hostname)) {
|
|
84
|
+
return isLoopback(request.socket?.remoteAddress);
|
|
85
|
+
}
|
|
86
|
+
return site === 'same-origin';
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function registerPluginTelemetry(ctx, options = {}) {
|
|
93
|
+
const endpoint = options.endpoint || '/api/dsh-dsml-artifact-guard/stats';
|
|
94
|
+
return ctx.webServer.register({
|
|
95
|
+
kind: 'exact',
|
|
96
|
+
path: endpoint,
|
|
97
|
+
handler: async (req, res) => {
|
|
98
|
+
try {
|
|
99
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
100
|
+
const payload = getTelemetryStats();
|
|
101
|
+
res.writeHead(200, {
|
|
102
|
+
'content-type': 'application/json; charset=utf-8',
|
|
103
|
+
'cache-control': 'no-store',
|
|
104
|
+
});
|
|
105
|
+
res.end(req.method === 'HEAD' ? undefined : JSON.stringify(payload));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!checkRequestMethod(req, 'POST')) {
|
|
109
|
+
res.writeHead(405, { allow: 'GET, HEAD, POST' });
|
|
110
|
+
res.end();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!isTrustedStatsRequest(req)) {
|
|
114
|
+
res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
|
|
115
|
+
res.end(JSON.stringify({ ok: false, error: 'Untrusted or cross-origin request rejected.' }));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const updatedStats = resetTelemetryStats();
|
|
119
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
|
|
120
|
+
res.end(JSON.stringify({ ok: true, stats: updatedStats }));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
ctx.logger?.warn?.(`[dsh-dsml-artifact-guard] telemetry error: ${String(error)}`);
|
|
123
|
+
res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' });
|
|
124
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal telemetry error' }));
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-dsml-artifact-guard",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Sanitizes leaked DeepSeek DSML closing tags from model text streams",
|
|
3
|
+
"version": "0.2.14",
|
|
4
|
+
"description": "Sanitizes leaked DeepSeek DSML closing tags and reasoning artifacts from model text streams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"license": "MIT",
|