@goodandready/dsh-usage-guard 0.1.14 → 0.1.15
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/lib/index.js +3 -16
- package/lib/updater.js +9 -4
- package/lib/usage.js +38 -12
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import z from '@deepseek-ai/schemastery'
|
|
14
14
|
|
|
15
15
|
import { patchRegistry } from './patch.js'
|
|
16
|
-
import { telemetry } from './telemetry.js'
|
|
16
|
+
import { telemetry, registerTelemetryRoute } from './telemetry.js'
|
|
17
17
|
import { registerPluginUpdater } from './updater.js'
|
|
18
18
|
import { complaint, damage, healed, usageOf } from './usage.js'
|
|
19
19
|
|
|
@@ -97,21 +97,8 @@ export function apply(ctx, config) {
|
|
|
97
97
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
98
98
|
}), 'dsh-usage-guard: plugin updater route')
|
|
99
99
|
|
|
100
|
-
wctx.effect(() => wctx
|
|
101
|
-
|
|
102
|
-
path: '/api/dsh-usage-guard/telemetry',
|
|
103
|
-
handler: async (req, res) => {
|
|
104
|
-
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
105
|
-
res.writeHead(200, {
|
|
106
|
-
'content-type': 'application/json; charset=utf-8',
|
|
107
|
-
'cache-control': 'no-store',
|
|
108
|
-
})
|
|
109
|
-
res.end(req.method === 'HEAD' ? undefined : JSON.stringify(telemetry.getSnapshot()))
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
res.writeHead(405, { allow: 'GET, HEAD' })
|
|
113
|
-
res.end()
|
|
114
|
-
},
|
|
100
|
+
wctx.effect(() => registerTelemetryRoute(wctx, {
|
|
101
|
+
endpoint: '/api/dsh-usage-guard/telemetry',
|
|
115
102
|
}), 'dsh-usage-guard: telemetry route')
|
|
116
103
|
} catch (err) {
|
|
117
104
|
const msg = `[dsh-usage-guard] failed to register routes on webServer: ${err?.message || err}`
|
package/lib/updater.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Implements canonical DSH plugin-updater specification.
|
|
3
3
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
@@ -55,11 +55,16 @@ function profileNameFromArgv(argv) {
|
|
|
55
55
|
return argv[2] === 'web' ? 'web' : undefined;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function findDshCliEntry() {
|
|
59
|
-
const value =
|
|
58
|
+
export function findDshCliEntry(argv1 = process.argv[1]) {
|
|
59
|
+
const value = argv1;
|
|
60
60
|
if (value === undefined || value === '') return undefined;
|
|
61
|
-
|
|
61
|
+
let entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value);
|
|
62
62
|
if (!existsSync(entry)) return undefined;
|
|
63
|
+
try {
|
|
64
|
+
entry = realpathSync(entry);
|
|
65
|
+
} catch {
|
|
66
|
+
// Ignore symlink resolution errors and continue with unresolved path
|
|
67
|
+
}
|
|
63
68
|
for (let directory = dirname(entry); ; directory = dirname(directory)) {
|
|
64
69
|
const manifestPath = resolve(directory, 'package.json');
|
|
65
70
|
if (existsSync(manifestPath)) {
|
package/lib/usage.js
CHANGED
|
@@ -102,10 +102,32 @@ export function usageOf(event) {
|
|
|
102
102
|
* and the harness automatically defaults them to zero. We only flag values that actually
|
|
103
103
|
* break arithmetic operations — missing required counters, non-numbers, non-integers, or negatives.
|
|
104
104
|
*/
|
|
105
|
+
const clampedMap = new WeakMap()
|
|
106
|
+
|
|
107
|
+
export function clampedDetails(usageOrBad, options = {}) {
|
|
108
|
+
if (Array.isArray(usageOrBad)) {
|
|
109
|
+
return clampedMap.get(usageOrBad) ?? null
|
|
110
|
+
}
|
|
111
|
+
const usage = usageOrBad
|
|
112
|
+
if (!usage || typeof usage !== 'object') return null
|
|
113
|
+
const maxStepTokens = typeof options === 'number' ? options : (Number(options?.maxStepTokens) > 0 ? Number(options.maxStepTokens) : 0)
|
|
114
|
+
if (maxStepTokens <= 0) return null
|
|
115
|
+
let clamped = null
|
|
116
|
+
for (const field of FIELDS) {
|
|
117
|
+
const value = usage[field]
|
|
118
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > maxStepTokens) {
|
|
119
|
+
if (!clamped) clamped = {}
|
|
120
|
+
clamped[field] = { from: value, to: maxStepTokens }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return clamped
|
|
124
|
+
}
|
|
125
|
+
|
|
105
126
|
export function damage(usage, options = {}) {
|
|
106
127
|
if (!usage || typeof usage !== 'object') return FIELDS.slice(0, 2)
|
|
107
128
|
const bad = []
|
|
108
129
|
const maxStepTokens = typeof options === 'number' ? options : (Number(options?.maxStepTokens) > 0 ? Number(options.maxStepTokens) : 0)
|
|
130
|
+
let clamped = null
|
|
109
131
|
|
|
110
132
|
for (const field of FIELDS) {
|
|
111
133
|
const value = usage[field]
|
|
@@ -115,10 +137,13 @@ export function damage(usage, options = {}) {
|
|
|
115
137
|
bad.push(field)
|
|
116
138
|
} else if (maxStepTokens > 0 && value > maxStepTokens) {
|
|
117
139
|
bad.push(field)
|
|
118
|
-
|
|
119
|
-
|
|
140
|
+
if (!clamped) clamped = {}
|
|
141
|
+
clamped[field] = { from: value, to: maxStepTokens }
|
|
120
142
|
}
|
|
121
143
|
}
|
|
144
|
+
if (clamped) {
|
|
145
|
+
clampedMap.set(bad, clamped)
|
|
146
|
+
}
|
|
122
147
|
return bad
|
|
123
148
|
}
|
|
124
149
|
|
|
@@ -129,30 +154,30 @@ export function damage(usage, options = {}) {
|
|
|
129
154
|
export function borrowed(usage, field) {
|
|
130
155
|
if (!usage || typeof usage !== 'object') return undefined
|
|
131
156
|
|
|
132
|
-
// 1.
|
|
157
|
+
// 1. Check the canonical field itself first if passed as a string or float (e.g. "123" or 15.4)
|
|
158
|
+
const selfNum = coerceNumber(usage[field])
|
|
159
|
+
if (selfNum !== undefined) return selfNum
|
|
160
|
+
|
|
161
|
+
// 2. Search known alias names
|
|
133
162
|
for (const alias of ALIASES[field] ?? []) {
|
|
134
163
|
const num = coerceNumber(usage[alias])
|
|
135
164
|
if (num !== undefined) return num
|
|
136
165
|
}
|
|
137
166
|
|
|
138
|
-
//
|
|
167
|
+
// 3. Search nested cache details (OpenAI-style: prompt_tokens_details.cached_tokens / cachedTokens)
|
|
139
168
|
if (field === 'cacheReadTokens' && usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
|
|
140
169
|
const details = usage.prompt_tokens_details
|
|
141
170
|
const num = coerceNumber(details.cached_tokens ?? details.cachedTokens ?? details.cache_read_tokens)
|
|
142
171
|
if (num !== undefined) return num
|
|
143
172
|
}
|
|
144
173
|
|
|
145
|
-
//
|
|
174
|
+
// 4. Search nested cache creation details (cache_creation_input_tokens)
|
|
146
175
|
if (field === 'cacheWriteTokens' && usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
|
|
147
176
|
const details = usage.prompt_tokens_details
|
|
148
177
|
const num = coerceNumber(details.cache_creation_tokens ?? details.cacheCreationTokens)
|
|
149
178
|
if (num !== undefined) return num
|
|
150
179
|
}
|
|
151
180
|
|
|
152
|
-
// 4. Check the field itself if passed as a string or float (e.g. "123" or 15.4)
|
|
153
|
-
const selfNum = coerceNumber(usage[field])
|
|
154
|
-
if (selfNum !== undefined) return selfNum
|
|
155
|
-
|
|
156
181
|
return undefined
|
|
157
182
|
}
|
|
158
183
|
|
|
@@ -223,12 +248,13 @@ export function complaint(found, bad, options = {}) {
|
|
|
223
248
|
const taken = Array.isArray(bad)
|
|
224
249
|
? bad
|
|
225
250
|
.map((field) => {
|
|
226
|
-
|
|
227
|
-
|
|
251
|
+
const clamped = options?.clamped ?? clampedMap.get(bad) ?? clampedDetails(usage, options)
|
|
252
|
+
if (clamped?.[field]) {
|
|
253
|
+
return `${field} clamped abnormally large (${clamped[field].from} -> ${clamped[field].to})`
|
|
228
254
|
}
|
|
229
255
|
const val = usage?.[field]
|
|
230
256
|
if (maxStepTokens > 0 && typeof val === 'number' && val > maxStepTokens) {
|
|
231
|
-
return `${field} clamped
|
|
257
|
+
return `${field} clamped abnormally large (${val} -> ${maxStepTokens})`
|
|
232
258
|
}
|
|
233
259
|
return usage && borrowed(usage, field) !== undefined ? `${field} recovered via alias` : `${field} zeroed`
|
|
234
260
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-usage-guard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Keeps a malformed token-usage sample from taking a whole session history down with it: a missing or non-numeric counter becomes zero before the harness adds it up, so the fold never yields NaN and already-broken sessions open again.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|