@goodandready/dsh-usage-guard 0.1.0 → 0.1.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/LICENSE +1 -1
- package/README.md +123 -97
- package/README.ru.md +156 -0
- package/README.zh.md +114 -0
- package/lib/index.js +17 -7
- package/lib/patch.js +5 -3
- package/lib/usage.js +87 -12
- package/package.json +4 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,133 +1,159 @@
|
|
|
1
|
-
# dsh-usage-guard
|
|
1
|
+
# 📦 @goodandready/dsh-usage-guard
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**one malformed token-usage sample should not take a whole session history down with it.**
|
|
3
|
+
<div align="center">
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
<h3>Session Token-Usage Sanitizer, History Crash Guard & Arithmetic Protection for DeepSeek Harness</h3>
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-usage-guard"><img src="https://img.shields.io/npm/v/@goodandready/dsh-usage-guard.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/All_Author_Projects-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="All Author Projects"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
|
+
</p>
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
because the session summary is recomputed by replaying the log every time.
|
|
24
|
+
</div>
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
---
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
the four:
|
|
28
|
+
## ⚡ The Root Problem: How Upstream Providers Poison Session History
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
In **DeepSeek Harness**, session projections aggregate cumulative token usage across four core buckets:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
uncachedInputTokens: usage.inputTokens, // No safety fallback in DSH core!
|
|
34
|
+
outputTokens: usage.outputTokens, // No safety fallback in DSH core!
|
|
35
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
36
|
+
cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
32
37
|
```
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
that are not guarded are just as optional in practice: providers disagree about
|
|
36
|
-
the shape of a usage report, and some send only part of it. One sample without
|
|
37
|
-
`inputTokens` turns the running total into `NaN`, the schema refuses it, and the
|
|
38
|
-
whole history goes with it.
|
|
39
|
+
While DSH core guards `cacheReadTokens` and `cacheWriteTokens` with `?? 0`, it takes `inputTokens` and `outputTokens` **as raw numbers without safety checks**.
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
When third-party providers, local inference servers, custom proxy gateways, or community routers return non-standard payloads, missing fields, or `NaN`, standard JavaScript arithmetic (`total += NaN`) instantly converts the session's cumulative token sum into `NaN`.
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
Subsequently, DSH schema validation fatally rejects the entire session digest:
|
|
44
|
+
```
|
|
45
|
+
history unavailable for session "<session-id>": expected number, received NaN
|
|
46
|
+
```
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
Because session history in DSH is computed dynamically by **replaying the event log**, a single malformed token packet permanently bricks the entire conversation history from being opened ever again.
|
|
49
|
+
|
|
50
|
+
```mermaid
|
|
51
|
+
graph LR
|
|
52
|
+
subgraph Malformed [Upstream Provider Stream]
|
|
53
|
+
API[LLM Output Stream] -->|Returns prompt_tokens / NaN / null| Event[Session Event Chunk]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
subgraph Unprotected [Without dsh-usage-guard]
|
|
57
|
+
Event --> DSHMath[DSH Cumulative Arithmetic]
|
|
58
|
+
DSHMath -->|total += NaN| Poison[🚨 Cumulative Total becomes NaN]
|
|
59
|
+
Poison --> SchemaFail[Schema Validation Rejection]
|
|
60
|
+
SchemaFail --> DeadHistory[💥 Session History Permanently Unreadable]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
subgraph Guarded [With dsh-usage-guard Active]
|
|
64
|
+
Event --> Patch[sessionProjections Interceptor]
|
|
65
|
+
Patch --> AliasCheck{Alias Borrowing Layer}
|
|
66
|
+
AliasCheck -->|Maps prompt_tokens -> inputTokens| Restored[Restored Number]
|
|
67
|
+
AliasCheck -->|If missing / NaN| ZeroFallback[Safe 0 Fallback]
|
|
68
|
+
Restored --> SafeMath[Clean Arithmetic Execution]
|
|
69
|
+
ZeroFallback --> SafeMath
|
|
70
|
+
SafeMath --> ValidHistory[✅ 100% Intact & Recovered Session History]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
style Malformed fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
|
|
74
|
+
style Unprotected fill:#311b1b,stroke:#f38ba8,stroke-width:2px,color:#cdd6f4
|
|
75
|
+
style Guarded fill:#181825,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
|
|
76
|
+
```
|
|
50
77
|
|
|
51
|
-
|
|
52
|
-
open again by themselves** — no session file is rewritten, nothing on disk is
|
|
53
|
-
touched, and removing the plugin puts everything back exactly as it was.
|
|
78
|
+
---
|
|
54
79
|
|
|
55
|
-
|
|
56
|
-
an exact total and an approximate one — it is between an approximate total and a
|
|
57
|
-
conversation you cannot read.
|
|
80
|
+
## ✨ Key Features & Architectural Defense
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
the
|
|
82
|
+
### 1. Instant Replay Recovery for Existing Corrupted Sessions
|
|
83
|
+
The plugin does **not** alter or rewrite log files on disk. Instead, it hooks the projection fold at runtime. Because session replay passes through this exact interception point, **all previously broken or locked sessions are instantly restored and readable immediately upon installing the plugin**.
|
|
61
84
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
arrived as: {"outputTokens":14,"cacheReadTokens":1024}; inputTokens zeroed
|
|
65
|
-
[dsh-usage-guard] usage sample out of shape — turn 3, step 1;
|
|
66
|
-
arrived as: {"input":22533,"output":20}; inputTokens taken from an alias; outputTokens taken from an alias
|
|
67
|
-
```
|
|
85
|
+
### 2. Comprehensive Alias Borrowing Lexicon (`borrowed`)
|
|
86
|
+
Before substituting zero, `dsh-usage-guard` scans an extensive dictionary of industry-standard field aliases:
|
|
68
87
|
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
| Target DSH Field | Recognized Vendor Aliases |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `inputTokens` | `input_tokens`, `input`, `promptTokens`, `prompt_tokens`, `promptTokenCount`, `prompt_eval_count` |
|
|
91
|
+
| `outputTokens` | `output_tokens`, `output`, `completionTokens`, `completion_tokens`, `candidatesTokenCount`, `eval_count` |
|
|
92
|
+
| `cacheReadTokens` | `cache_read_tokens`, `cachedTokens`, `cached_tokens`, `cache_read_input_tokens`, `cachedContentTokenCount`, `prompt_tokens_details.cached_tokens` |
|
|
93
|
+
| `cacheWriteTokens` | `cache_write_tokens`, `cacheCreationTokens`, `cache_creation_input_tokens` |
|
|
71
94
|
|
|
72
|
-
|
|
95
|
+
### 3. Finite Non-Negative Soundness Validation (`sound`)
|
|
96
|
+
Strictly validates `typeof value === 'number' && Number.isFinite(value) && value >= 0` to filter out `NaN`, `Infinity`, `null`, `undefined`, negative error codes (e.g. `-1`), and malformed strings before they reach arithmetic operations.
|
|
73
97
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```
|
|
98
|
+
### 4. Safe Zero Fallback (`repaired`)
|
|
99
|
+
If a counter cannot be resolved from aliases, it is safely initialized to `0`. The choice is not between exact and approximate numbers, but between approximate token counts and an unreadable dead session.
|
|
77
100
|
|
|
78
|
-
|
|
79
|
-
|
|
101
|
+
### 5. In-Memory Registry Monkey-Patching (`lib/patch.js`)
|
|
102
|
+
* **Pre-existing Projections**: Wraps all `.apply` methods currently registered in `sessionProjections.registrations` while preserving full `this` context.
|
|
103
|
+
* **Late-Binding Projections**: Traps future projection registrations via `map.set` wrapping, guaranteeing 100% coverage regardless of plugin loading order.
|
|
104
|
+
* **Universal Projection Protection**: Protects not only token counters, but also context pressure calculators and busy-state analyzers.
|
|
105
|
+
* **Zero Performance Overhead**: Uses shallow event cloning only along the usage path; all other event data references remain untouched.
|
|
80
106
|
|
|
81
|
-
|
|
107
|
+
### 6. Deduplicated Diagnostic Reporting (`told`)
|
|
108
|
+
Logs informative diagnostic warnings naming the exact session, turn, step, raw payload, and recovery action (e.g. `inputTokens borrowed from alias` vs `inputTokens zeroed`). Incidents are deduplicated in memory so logs are not flooded during replays, and the cache is capped at 1,000 items to prevent memory leaks.
|
|
82
109
|
|
|
83
|
-
|
|
110
|
+
---
|
|
84
111
|
|
|
85
|
-
|
|
86
|
-
|---|---|---|
|
|
87
|
-
| `repair` | `true` | Replace a missing or non-numeric counter with zero before the fold. Off means reporting only — the fold sees the sample as it came, and a broken one keeps the history unreadable. |
|
|
88
|
-
| `report` | `true` | Log the turn, the step and the raw sample when a damaged one arrives. Reported once per turn, step and field set, not once per replay. |
|
|
112
|
+
## 🚀 Changed in v0.1.1
|
|
89
113
|
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
* **Negative Number Protection (`nonnegative`)**:
|
|
115
|
+
In v0.1.0, negative counters like `-1` (sometimes returned by proxy gateways during rate limits or faults) passed finite-number checks and crashed DSH schema validation (`z.number().int().nonnegative()`). In v0.1.1, `sound()` strictly requires `value >= 0`. Any negative number is treated as corrupted and safely zeroed out.
|
|
116
|
+
* **Safe Coercion of Stringified Numbers**:
|
|
117
|
+
If an upstream provider delivers valid token counts formatted as strings (e.g. `inputTokens: "1540"`), v0.1.1 coerces them into true numbers rather than resetting them to zero.
|
|
118
|
+
* **Expanded Ecosystem Aliases**:
|
|
119
|
+
- **Google Gemini API**: added `promptTokenCount`, `candidatesTokenCount`, and `cachedContentTokenCount`.
|
|
120
|
+
- **Ollama native API**: added `prompt_eval_count` and `eval_count`.
|
|
121
|
+
- **OpenAI prompt caching**: added nested resolution of `prompt_tokens_details.cached_tokens`.
|
|
122
|
+
* **Preservation of `this` Context in Projections**:
|
|
123
|
+
In `lib/patch.js`, `wrapApply` now dispatches via `original.call(this, state, guard(event))`, ensuring complete compatibility with class-based projection handlers.
|
|
124
|
+
* **Crash-Proof Diagnostic Logging**:
|
|
125
|
+
In `complaint()`, object serialization is now safely protected with `try...catch` against circular structures and `BigInt` values.
|
|
126
|
+
* **Session-Aware Deduplication & Memory Bound**:
|
|
127
|
+
Warning deduplication now incorporates `sessionId` to avoid cross-session warning suppression. `told` cache size is bounded to 1,000 entries to prevent memory growth in long-running processes.
|
|
92
128
|
|
|
93
|
-
|
|
129
|
+
---
|
|
94
130
|
|
|
95
|
-
|
|
96
|
-
each entry holds the `apply` the harness actually calls. The plugin wraps those.
|
|
131
|
+
## 📦 Quick Installation
|
|
97
132
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
them trips over the same missing counter. Keeping a list of keys in step with
|
|
102
|
-
someone else's code is a standing debt; the guard is an identity for an event
|
|
103
|
-
that carries no damaged usage, so a projection that never looks at usage pays
|
|
104
|
-
one comparison and gets the very same event object it would have got anyway.
|
|
133
|
+
```bash
|
|
134
|
+
dsh plugin --profile web add @goodandready/dsh-usage-guard
|
|
135
|
+
```
|
|
105
136
|
|
|
106
|
-
|
|
107
|
-
|
|
137
|
+
> [!IMPORTANT]
|
|
138
|
+
> Restart DSH Web UI after installation (`systemctl --user restart dsh-web`) to activate protection and instantly revive any previously locked sessions.
|
|
108
139
|
|
|
109
|
-
|
|
110
|
-
plugin wraps rather than fighting with it. If a later version guards those two
|
|
111
|
-
fields itself, this plugin becomes a no-op that only reports — and can be
|
|
112
|
-
removed.
|
|
140
|
+
---
|
|
113
141
|
|
|
114
|
-
##
|
|
142
|
+
## ⚙️ Configuration Reference (`settings.yaml`)
|
|
115
143
|
|
|
144
|
+
```yaml
|
|
145
|
+
dsh-usage-guard:
|
|
146
|
+
repair: true
|
|
147
|
+
report: true
|
|
116
148
|
```
|
|
117
|
-
dsh-usage-guard/
|
|
118
|
-
├── package.json # dsh bundle metadata + peerDependencies
|
|
119
|
-
├── cordis.patch.yml # bundle layer: inserts the plugin row
|
|
120
|
-
├── lib/index.js # the plugin: settings, reporting, attachment
|
|
121
|
-
├── lib/usage.js # what counts as damaged and what it becomes — pure
|
|
122
|
-
├── lib/patch.js # how the guard gets in front of the fold — pure
|
|
123
|
-
├── test/ # unit tests, no harness required
|
|
124
|
-
├── README.md
|
|
125
|
-
└── LICENSE # MIT
|
|
126
|
-
```
|
|
127
149
|
|
|
128
|
-
|
|
129
|
-
|
|
150
|
+
| Parameter | Type | Default | Description |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| `repair` | `boolean` | `true` | Replace missing or non-numeric token counters with zero before arithmetic accumulation |
|
|
153
|
+
| `report` | `boolean` | `true` | Log diagnostic warning lines naming turn, step, and raw sample when damaged metrics arrive |
|
|
154
|
+
|
|
155
|
+
---
|
|
130
156
|
|
|
131
|
-
## License
|
|
157
|
+
## 📄 License
|
|
132
158
|
|
|
133
|
-
MIT
|
|
159
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-usage-guard
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>Санитайзер расхода токенов, защита от сбоев истории и сохранение сессий для DeepSeek Harness</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-usage-guard"><img src="https://img.shields.io/npm/v/@goodandready/dsh-usage-guard.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/Все_проекты_автора-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="Все проекты автора"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ⚡ В чём суть проблемы: как провайдеры ломают историю сессий
|
|
29
|
+
|
|
30
|
+
В ядре **DeepSeek Harness** подсчёт суммарного расхода токенов складывается из четырёх счётчиков:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
uncachedInputTokens: usage.inputTokens, // Без подстраховки в ядре DSH!
|
|
34
|
+
outputTokens: usage.outputTokens, // Без подстраховки в ядре DSH!
|
|
35
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
36
|
+
cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Поля кэша ядро подстраховывает через `?? 0`, а первые два берёт **как есть, без проверки на число**.
|
|
40
|
+
|
|
41
|
+
Когда сторонний провайдер, локальный движок инференса, шлюз или роутер присылает нестандартные названия полей, пустые значения или `NaN`, стандартное сложение (`total += usage.inputTokens`) превращает сумму расхода сессии в `NaN`.
|
|
42
|
+
|
|
43
|
+
После этого валидация схемы в ядре отвергает выжимку сессии:
|
|
44
|
+
```
|
|
45
|
+
history unavailable for session "<session-id>": expected number, received NaN
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Поскольку история сессий в DSH рассчитывается на лету **путём повторного проигрывания журнала событий**, одна-единственная битая порция токенов навсегда блокирует открытие диалога в веб-интерфейсе.
|
|
49
|
+
|
|
50
|
+
```mermaid
|
|
51
|
+
graph LR
|
|
52
|
+
subgraph Malformed [Поток данных провайдера]
|
|
53
|
+
API[Ответ модели] -->|Присылает prompt_tokens / NaN / null| Event[Событие сессии]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
subgraph Unprotected [Без dsh-usage-guard]
|
|
57
|
+
Event --> DSHMath[Сложение расхода в ядре DSH]
|
|
58
|
+
DSHMath -->|total += NaN| Poison[🚨 Сумма расхода становится NaN]
|
|
59
|
+
Poison --> SchemaFail[Отказ проверки схемы]
|
|
60
|
+
SchemaFail --> DeadHistory[💥 История сессии заблокирована навсегда]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
subgraph Guarded [С активным dsh-usage-guard]
|
|
64
|
+
Event --> Patch[Перехватчик sessionProjections]
|
|
65
|
+
Patch --> AliasCheck{Поиск по синонимам}
|
|
66
|
+
AliasCheck -->|prompt_tokens -> inputTokens| Restored[Восстановленное число]
|
|
67
|
+
AliasCheck -->|Если число отсутствует| ZeroFallback[Безопасный 0]
|
|
68
|
+
Restored --> SafeMath[Корректное сложение]
|
|
69
|
+
ZeroFallback --> SafeMath
|
|
70
|
+
SafeMath --> ValidHistory[✅ 100% Восстановленная и сохранная история]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
style Malformed fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
|
|
74
|
+
style Unprotected fill:#311b1b,stroke:#f38ba8,stroke-width:2px,color:#cdd6f4
|
|
75
|
+
style Guarded fill:#181825,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## ✨ Ключевые возможности и механизмы защиты
|
|
81
|
+
|
|
82
|
+
### 1. Мгновенное восстановление ранее сломанных сессий
|
|
83
|
+
Плагин **не модифицирует** файлы журналов на диске. Он встаёт перед операцией сложения в памяти. Так как проигрывание сессий идёт через эту же точку, **все ранее повреждённые сессии начинают открываться снова сразу после установки плагина**.
|
|
84
|
+
|
|
85
|
+
### 2. Полный словарь синонимов полей (`borrowed`)
|
|
86
|
+
Прежде чем подставлять ноль, плагин ищет значения в общепринятых полях других API:
|
|
87
|
+
|
|
88
|
+
| Целевое поле DSH | Распознаваемые синонимы |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `inputTokens` | `input_tokens`, `input`, `promptTokens`, `prompt_tokens`, `promptTokenCount`, `prompt_eval_count` |
|
|
91
|
+
| `outputTokens` | `output_tokens`, `output`, `completionTokens`, `completion_tokens`, `candidatesTokenCount`, `eval_count` |
|
|
92
|
+
| `cacheReadTokens` | `cache_read_tokens`, `cachedTokens`, `cached_tokens`, `cache_read_input_tokens`, `cachedContentTokenCount`, `prompt_tokens_details.cached_tokens` |
|
|
93
|
+
| `cacheWriteTokens` | `cache_write_tokens`, `cacheCreationTokens`, `cache_creation_input_tokens` |
|
|
94
|
+
|
|
95
|
+
### 3. Проверка на конечное неотрицательное число (`sound`)
|
|
96
|
+
Проверка `typeof value === 'number' && Number.isFinite(value) && value >= 0` гарантирует отсечение `NaN`, `Infinity`, `null`, `undefined`, отрицательных чисел (напр. `-1` при ошибках прокси) и строк.
|
|
97
|
+
|
|
98
|
+
### 4. Безопасная подстановка нуля (`repaired`)
|
|
99
|
+
Если число найти не удалось, подставляется `0`. Выбор делается в пользу приблизительного счёта вместо полностью заблокированной истории.
|
|
100
|
+
|
|
101
|
+
### 5. Безопасный перехват в памяти (`lib/patch.js`)
|
|
102
|
+
* **Существующие проекции**: оборачивает все методы `.apply` в `sessionProjections.registrations` с полным сохранением контекста `this`.
|
|
103
|
+
* **Поздние проекции**: перехватывает будущие регистрации через хук `map.set`.
|
|
104
|
+
* **Комплексная защита**: защищает не только токены, но и формулы давления на контекст и разбор занятости.
|
|
105
|
+
* **Нулевой оверхед**: поверхностное клонирование применяется только к объекту расхода.
|
|
106
|
+
|
|
107
|
+
### 6. Дедуплицированное журналирование (`told`)
|
|
108
|
+
Выводит предупреждение с идентификатором сессии, номером хода, шагом и способом починки (взят по синониму или обнулён). Одинаковые инциденты не спамят в лог при повторных проигрываниях, а размер кэша ограничен 1000 записей для предотвращения утечек памяти.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🚀 Изменения в версии v0.1.1 (Changed in v0.1.1)
|
|
113
|
+
|
|
114
|
+
* **Защита от отрицательных чисел (`nonnegative`)**:
|
|
115
|
+
В v0.1.0 значение `-1` (возвращаемое некоторыми шлюзами при ошибках) проходило проверку конечности числа и ломало схему DSH (`z.number().int().nonnegative()`). Начиная с v0.1.1 функция `sound()` строго требует `value >= 0`. Отрицательные значения теперь признаются повреждёнными и безопасно обнуляются.
|
|
116
|
+
* **Безопасная коэрция числовых строк (Stringified Numbers)**:
|
|
117
|
+
Если провайдер возвращает валидное количество токенов строкой (например, `inputTokens: "1540"`), плагин в v0.1.1 больше не сбрасывает его в ноль, а безопасно приводит к настоящему числу (`1540`).
|
|
118
|
+
* **Расширенная совместимость с экосистемами моделей**:
|
|
119
|
+
- **Google Gemini API**: добавлены `promptTokenCount`, `candidatesTokenCount`, `cachedContentTokenCount`.
|
|
120
|
+
- **Ollama native API**: добавлены `prompt_eval_count`, `eval_count`.
|
|
121
|
+
- **OpenAI prompt caching**: добавлена поддержка вложенного объекта `prompt_tokens_details.cached_tokens`.
|
|
122
|
+
* **Сохранение контекста вызова `this` в проекциях**:
|
|
123
|
+
В `lib/patch.js` метод `wrapApply` переписан с сохранением `this` (`original.call(this, state, guard(event))`), обеспечивая полную совместимость с объектно-ориентированными проекциями.
|
|
124
|
+
* **Отказоустойчивое логирование без риска краша**:
|
|
125
|
+
В функции `complaint()` сериализация данных экранирована `try...catch`, предотвращая падение рантайма при наличии циклических структур или `BigInt`.
|
|
126
|
+
* **Изолированная межсессионная дедупликация**:
|
|
127
|
+
Ключ дедупликации предупреждений дополнен контекстом сессии (`sessionId`), что предотвращает ложное подавление сообщений об ошибках в последующих диалогах. Множество `told` ограничено 1000 элементами для защиты от утечек памяти.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 📦 Быстрая установка
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
dsh plugin --profile web add @goodandready/dsh-usage-guard
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## ⚙️ Параметры конфигурации (`settings.yaml`)
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
dsh-usage-guard:
|
|
143
|
+
repair: true
|
|
144
|
+
report: true
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Параметр | Тип | По умолчанию | Описание |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| `repair` | `boolean` | `true` | Заменять пропущенные и нечисловые значения на ноль до сложения |
|
|
150
|
+
| `report` | `boolean` | `true` | Логировать информацию о поврежденных порциях в консоль |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 📄 Лицензия
|
|
155
|
+
|
|
156
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-usage-guard
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>DeepSeek Harness 会话用量数据清洗、历史记录防崩与 Token 算术保护插件</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-usage-guard"><img src="https://img.shields.io/npm/v/@goodandready/dsh-usage-guard.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者全部项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="作者全部项目"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ⚡ 核心痛点深度剖析:上游格式异常如何导致会话历史永久损坏
|
|
29
|
+
|
|
30
|
+
在 **DeepSeek Harness** 核心架构中,会话投影累加 4 类 Token 计数:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
uncachedInputTokens: usage.inputTokens, // 核心源码无默认兜底保护!
|
|
34
|
+
outputTokens: usage.outputTokens, // 核心源码无默认兜底保护!
|
|
35
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
36
|
+
cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
当第三方服务商、本地引擎或网关返回非标字段、空值或 `NaN` 时,累加逻辑 (`total += usage.inputTokens`) 会将累计用量污染为 `NaN`,导致架构模式校验致命拒绝:
|
|
40
|
+
```
|
|
41
|
+
history unavailable for session "<session-id>": expected number, received NaN
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
由于 DSH 历史记录是通过重放事件流动态计算生成的,**单个损坏的数据块会导致整个历史对话永久白屏且无法重新打开**。
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
graph LR
|
|
48
|
+
subgraph Malformed [上游返回异常格式]
|
|
49
|
+
API[模型输出流] -->|返回 prompt_tokens / NaN / null| Event[会话事件]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
subgraph Unprotected [未开启防护]
|
|
53
|
+
Event --> DSHMath[DSH 原生累加运算]
|
|
54
|
+
DSHMath -->|total += NaN| Poison[🚨 累计用量全变为 NaN]
|
|
55
|
+
Poison --> SchemaFail[模式校验致命失败]
|
|
56
|
+
SchemaFail --> DeadHistory[💥 历史记录永久锁定损坏]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
subgraph Guarded [开启 dsh-usage-guard]
|
|
60
|
+
Event --> Patch[sessionProjections 拦截器]
|
|
61
|
+
Patch --> AliasCheck{同义名字典检索}
|
|
62
|
+
AliasCheck -->|prompt_tokens -> inputTokens| Restored[恢复有效数字]
|
|
63
|
+
AliasCheck -->|缺失字段| ZeroFallback[安全 0 值兜底]
|
|
64
|
+
Restored --> SafeMath[安全累加计算]
|
|
65
|
+
ZeroFallback --> SafeMath
|
|
66
|
+
SafeMath --> ValidHistory[✅ 100% 修复并完好读取会话历史]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
style Malformed fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
|
|
70
|
+
style Unprotected fill:#311b1b,stroke:#f38ba8,stroke-width:2px,color:#cdd6f4
|
|
71
|
+
style Guarded fill:#181825,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## ✨ 核心亮点与保护机制
|
|
77
|
+
|
|
78
|
+
1. **已损坏历史记录免修文件即刻复活**:不修改磁盘日志,在内存重放链路拦截修复;
|
|
79
|
+
2. **主流别名字典智能提取 (`borrowed`)**:覆盖 `prompt_tokens`、`completion_tokens`、`cached_tokens`、`promptTokenCount`、`prompt_eval_count` 等;
|
|
80
|
+
3. **非负有限数字安全性校验 (`sound`)**:剔除 `NaN`、`Infinity`、负数错误码(如 `-1`)与非法格式;
|
|
81
|
+
4. **安全 0 值兜底 (`repaired`)**:彻底阻断算术污染;
|
|
82
|
+
5. **内存投影注册表动态切入 (`lib/patch.js`)**:无缝覆盖前置与后置投影,完整保留 `this` 上下文;
|
|
83
|
+
6. **防刷屏告警与内存保护 (`told`)**:集成会话识别与容量上限(1,000 项),杜绝内存泄漏与误抑制。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 🚀 v0.1.1 版本更新说明 (Changed in v0.1.1)
|
|
88
|
+
|
|
89
|
+
* **负数穿透防护 (`nonnegative`)**:
|
|
90
|
+
在 v0.1.0 中,部分代理网关返回的 `-1` 能通过有限数检测,进而导致 DSH 模式校验报错。v0.1.1 严格要求 `value >= 0`,负数将被识别为损坏并安全置 0。
|
|
91
|
+
* **数字字符串安全转换 (Stringified Numbers)**:
|
|
92
|
+
部分上游返回的字符串数字(如 `inputTokens: "1540"`)在 v0.1.1 中将安全转换为真正数值,不再误重置为 0。
|
|
93
|
+
* **主流模型生态别名扩充**:
|
|
94
|
+
- **Google Gemini API**:新增 `promptTokenCount`、`candidatesTokenCount`、`cachedContentTokenCount`。
|
|
95
|
+
- **Ollama native API**:新增 `prompt_eval_count`、`eval_count`。
|
|
96
|
+
- **OpenAI 缓存详情**:新增嵌套对象 `prompt_tokens_details.cached_tokens` 支持。
|
|
97
|
+
* **投影函数 `this` 上下文维持**:
|
|
98
|
+
修复 `wrapApply` 调用时缺失 `this` 的问题,确保与对象方法式投影兼容。
|
|
99
|
+
* **日志防崩防护与跨会话隔离**:
|
|
100
|
+
日志格式化引入 `try...catch` 兜底,防止循环引用导致服务崩溃;告警去重引入会话维度标记,杜绝跨会话误抑制。
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 📦 安装指南
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
dsh plugin --profile web add @goodandready/dsh-usage-guard
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 📄 开源协议
|
|
113
|
+
|
|
114
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/lib/index.js
CHANGED
|
@@ -56,10 +56,12 @@ export function apply(ctx, config) {
|
|
|
56
56
|
sctx.effect(() => scope.watch((next) => { live = next ?? live }))
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
// О каждом сочетании хода, шага и набора полей жалуемся один раз:
|
|
60
|
-
// считается проигрыванием журнала заново, и без этого одна и та же
|
|
61
|
-
// въезжала бы в журнал при каждом открытии истории.
|
|
59
|
+
// О каждом сочетании сессии, хода, шага и набора полей жалуемся один раз:
|
|
60
|
+
// выжимка считается проигрыванием журнала заново, и без этого одна и та же
|
|
61
|
+
// порция въезжала бы в журнал при каждом открытии истории.
|
|
62
|
+
// Ограничиваем размер (до 1000 записей), чтобы исключить утечку памяти.
|
|
62
63
|
const told = new Set()
|
|
64
|
+
const MAX_TOLD = 1000
|
|
63
65
|
|
|
64
66
|
const guard = (event) => {
|
|
65
67
|
const found = usageOf(event)
|
|
@@ -68,11 +70,19 @@ export function apply(ctx, config) {
|
|
|
68
70
|
if (bad.length === 0) return event
|
|
69
71
|
|
|
70
72
|
if (live.report !== false) {
|
|
71
|
-
const
|
|
73
|
+
const sessionTag = event?.sessionId ?? event?.data?.sessionId ?? event?.data?.turn ?? 'global'
|
|
74
|
+
const seen = `${String(sessionTag)}/${String(found.turn)}/${String(found.step)}/${bad.join(',')}`
|
|
72
75
|
if (!told.has(seen)) {
|
|
76
|
+
if (told.size >= MAX_TOLD) {
|
|
77
|
+
told.clear()
|
|
78
|
+
}
|
|
73
79
|
told.add(seen)
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
try {
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.warn(`[dsh-usage-guard] ${complaint(found, bad)}`)
|
|
83
|
+
} catch {
|
|
84
|
+
// Ошибка вывода не должна ломать обработку событий
|
|
85
|
+
}
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
|
|
@@ -85,4 +95,4 @@ export function apply(ctx, config) {
|
|
|
85
95
|
'dsh-usage-guard: обезвреживание порций расхода',
|
|
86
96
|
)
|
|
87
97
|
})
|
|
88
|
-
}
|
|
98
|
+
}
|
package/lib/patch.js
CHANGED
|
@@ -22,14 +22,16 @@
|
|
|
22
22
|
export const PATCHED = Symbol.for('dsh-usage-guard.patched')
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Обернуть складывание одной записи
|
|
25
|
+
* Обернуть складывание одной записи реестра с сохранением контекста this.
|
|
26
26
|
*
|
|
27
27
|
* @returns функция возврата к исходному состоянию.
|
|
28
28
|
*/
|
|
29
29
|
export function wrapApply(def, guard) {
|
|
30
30
|
if (!def || typeof def.apply !== 'function' || def.apply[PATCHED]) return () => {}
|
|
31
31
|
const original = def.apply
|
|
32
|
-
const patched = (state, event)
|
|
32
|
+
const patched = function (state, event) {
|
|
33
|
+
return original.call(this, state, guard(event))
|
|
34
|
+
}
|
|
33
35
|
patched[PATCHED] = true
|
|
34
36
|
def.apply = patched
|
|
35
37
|
return () => {
|
|
@@ -73,4 +75,4 @@ export function patchRegistry(registry, guard) {
|
|
|
73
75
|
for (const step of undo.reverse()) step()
|
|
74
76
|
undo.length = 0
|
|
75
77
|
}
|
|
76
|
-
}
|
|
78
|
+
}
|
package/lib/usage.js
CHANGED
|
@@ -20,15 +20,58 @@ export const FIELDS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheW
|
|
|
20
20
|
* не первый ход.
|
|
21
21
|
*/
|
|
22
22
|
const ALIASES = {
|
|
23
|
-
inputTokens: [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
inputTokens: [
|
|
24
|
+
'input_tokens',
|
|
25
|
+
'input',
|
|
26
|
+
'promptTokens',
|
|
27
|
+
'prompt_tokens',
|
|
28
|
+
'promptTokenCount',
|
|
29
|
+
'prompt_eval_count',
|
|
30
|
+
],
|
|
31
|
+
outputTokens: [
|
|
32
|
+
'output_tokens',
|
|
33
|
+
'output',
|
|
34
|
+
'completionTokens',
|
|
35
|
+
'completion_tokens',
|
|
36
|
+
'candidatesTokenCount',
|
|
37
|
+
'eval_count',
|
|
38
|
+
],
|
|
39
|
+
cacheReadTokens: [
|
|
40
|
+
'cache_read_tokens',
|
|
41
|
+
'cachedTokens',
|
|
42
|
+
'cached_tokens',
|
|
43
|
+
'cache_read_input_tokens',
|
|
44
|
+
'cachedContentTokenCount',
|
|
45
|
+
],
|
|
46
|
+
cacheWriteTokens: [
|
|
47
|
+
'cache_write_tokens',
|
|
48
|
+
'cacheCreationTokens',
|
|
49
|
+
'cache_creation_input_tokens',
|
|
50
|
+
],
|
|
27
51
|
}
|
|
28
52
|
|
|
29
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Годное значение счётчика: конечное неотрицательное число.
|
|
55
|
+
* Значения меньше нуля (-1 и т.д.) или нечисловые не признаются годными,
|
|
56
|
+
* так как схема DSH требует nonnegative integer.
|
|
57
|
+
*/
|
|
30
58
|
function sound(value) {
|
|
31
|
-
return typeof value === 'number' && Number.isFinite(value)
|
|
59
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Приведение значения к корректному неотрицательному числу.
|
|
64
|
+
* Поддерживает числа и строковые представления целых неотрицательных чисел (напр. "1540").
|
|
65
|
+
*/
|
|
66
|
+
function coerceNumber(value) {
|
|
67
|
+
if (typeof value === 'number') {
|
|
68
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim())) {
|
|
71
|
+
const num = Number(value.trim())
|
|
72
|
+
return Number.isFinite(num) && num >= 0 ? num : undefined
|
|
73
|
+
}
|
|
74
|
+
return undefined
|
|
32
75
|
}
|
|
33
76
|
|
|
34
77
|
/**
|
|
@@ -57,8 +100,8 @@ export function usageOf(event) {
|
|
|
57
100
|
*
|
|
58
101
|
* Отсутствие кэш-полей нарушением не считается: их не присылает добрая половина
|
|
59
102
|
* провайдеров, и харнесс сам подставляет там ноль. Ругаемся только на то, что
|
|
60
|
-
* действительно ломает сложение, — на
|
|
61
|
-
* полей, которые харнесс берёт без
|
|
103
|
+
* действительно ломает сложение, — на пропущенные, нечисловые или отрицательные
|
|
104
|
+
* значения тех полей, которые харнесс берёт без оглядки, либо на невалидные кэш-поля.
|
|
62
105
|
*/
|
|
63
106
|
export function damage(usage) {
|
|
64
107
|
if (!usage || typeof usage !== 'object') return FIELDS.slice(0, 2)
|
|
@@ -72,11 +115,30 @@ export function damage(usage) {
|
|
|
72
115
|
return bad
|
|
73
116
|
}
|
|
74
117
|
|
|
75
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Число под одним из привычных чужих имён, вложенных структур или строковых чисел.
|
|
120
|
+
* Возвращает валидное неотрицательное число или `undefined`, если его там нет.
|
|
121
|
+
*/
|
|
76
122
|
export function borrowed(usage, field) {
|
|
123
|
+
if (!usage || typeof usage !== 'object') return undefined
|
|
124
|
+
|
|
125
|
+
// 1. Поиск по известным синонимам
|
|
77
126
|
for (const alias of ALIASES[field] ?? []) {
|
|
78
|
-
|
|
127
|
+
const num = coerceNumber(usage[alias])
|
|
128
|
+
if (num !== undefined) return num
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 2. Поиск во вложенных деталях кэша (OpenAI-style: prompt_tokens_details.cached_tokens)
|
|
132
|
+
if (field === 'cacheReadTokens' && usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
|
|
133
|
+
const details = usage.prompt_tokens_details
|
|
134
|
+
const num = coerceNumber(details.cached_tokens ?? details.cachedTokens)
|
|
135
|
+
if (num !== undefined) return num
|
|
79
136
|
}
|
|
137
|
+
|
|
138
|
+
// 3. Само поле, если оно было передано валидной строкой (напр. "123")
|
|
139
|
+
const selfNum = coerceNumber(usage[field])
|
|
140
|
+
if (selfNum !== undefined) return selfNum
|
|
141
|
+
|
|
80
142
|
return undefined
|
|
81
143
|
}
|
|
82
144
|
|
|
@@ -120,11 +182,24 @@ export function healed(event, bad) {
|
|
|
120
182
|
return { ...event, data: { ...event.data, usage } }
|
|
121
183
|
}
|
|
122
184
|
|
|
185
|
+
/** Безопасная сериализация для журнала: предотвращает сбои на циклических объектах и BigInt. */
|
|
186
|
+
function safeStringify(value) {
|
|
187
|
+
try {
|
|
188
|
+
return JSON.stringify(value)
|
|
189
|
+
} catch {
|
|
190
|
+
try {
|
|
191
|
+
return String(value)
|
|
192
|
+
} catch {
|
|
193
|
+
return '[не сериализуется]'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
123
198
|
/** Короткая строка для журнала: по ней видно, кого звать к ответу. */
|
|
124
199
|
export function complaint(found, bad) {
|
|
125
200
|
const where = `ход ${String(found.turn)}, шаг ${String(found.step)}`
|
|
126
201
|
const taken = bad
|
|
127
202
|
.map((field) => (found.usage && borrowed(found.usage, field) !== undefined ? `${field} взят по синониму` : `${field} обнулён`))
|
|
128
203
|
.join('; ')
|
|
129
|
-
return `порция расхода не по форме — ${where}; пришло: ${
|
|
130
|
-
}
|
|
204
|
+
return `порция расхода не по форме — ${where}; пришло: ${safeStringify(found.usage)}; ${taken}`
|
|
205
|
+
}
|
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.1",
|
|
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",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"lib/",
|
|
30
30
|
"cordis.patch.yml",
|
|
31
31
|
"README.md",
|
|
32
|
+
"README.ru.md",
|
|
33
|
+
"README.zh.md",
|
|
32
34
|
"LICENSE"
|
|
33
35
|
],
|
|
34
36
|
"license": "MIT",
|
|
@@ -47,4 +49,4 @@
|
|
|
47
49
|
"scripts": {
|
|
48
50
|
"test": "node --test test/*.test.mjs"
|
|
49
51
|
}
|
|
50
|
-
}
|
|
52
|
+
}
|