ai-stream-auditor 1.0.0 → 1.0.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/CHANGELOG.md +9 -0
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/index.cjs +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project uses [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [1.0.1] - 2026-10-05
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Preserve structural error state independently of bounded diagnostic storage, so warning saturation cannot turn malformed streams into successful reports.
|
|
12
|
+
- Reject OpenAI content-part starts, deltas, and stops without an open parent output item.
|
|
13
|
+
|
|
5
14
|
## [1.0.0] - 2026-10-03
|
|
6
15
|
|
|
7
16
|
### Added
|
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ This function deliberately accepts only synchronous iterables. Use `createStream
|
|
|
142
142
|
|
|
143
143
|
- `response.created` lifecycle start
|
|
144
144
|
- Output-item and content-part added/done pairing
|
|
145
|
+
- Content-part starts, deltas, and stops require their output item to remain open
|
|
145
146
|
- Delta events targeting an open item or content part
|
|
146
147
|
- Non-increasing `sequence_number` values when present
|
|
147
148
|
- `response.completed`, `response.failed`, `response.incomplete`, `response.cancelled`, and `error` terminals
|
|
@@ -215,7 +216,9 @@ Provider protocols can add event types. The report remains successful if no stru
|
|
|
215
216
|
|
|
216
217
|
### Too many diagnostics
|
|
217
218
|
|
|
218
|
-
`droppedDiagnostics` counts records beyond `maxDiagnostics`. The first bounded set is retained. Increase the limit only when your logging and memory budgets permit.
|
|
219
|
+
`droppedDiagnostics` counts records beyond `maxDiagnostics`. The first bounded set is retained. Errors still affect the final outcome even when their diagnostic records are dropped. Increase the limit only when your logging and memory budgets permit.
|
|
220
|
+
|
|
221
|
+
Content parts are checked against their parent output item's lifecycle, as defined in the [OpenAI streaming events reference](https://developers.openai.com/api/reference/resources/responses/streaming-events). Orphan content parts and content activity after the output item completes make a nominal completion invalid.
|
|
219
222
|
|
|
220
223
|
## Security and privacy
|
|
221
224
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-stream-auditor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Audit decoded OpenAI Responses and Anthropic Messages event lifecycles for silent or malformed stream endings.",
|
|
5
5
|
"keywords": ["ai", "streaming", "openai", "anthropic", "llm", "diagnostics", "testing"],
|
|
6
6
|
"license": "MIT",
|
package/src/index.cjs
CHANGED
|
@@ -57,6 +57,7 @@ class StreamAuditor {
|
|
|
57
57
|
this.eventCount = 0;
|
|
58
58
|
this.counts = new Map();
|
|
59
59
|
this.diagnostics = [];
|
|
60
|
+
this.hasErrors = false;
|
|
60
61
|
this.droppedDiagnostics = 0;
|
|
61
62
|
this.terminal = undefined;
|
|
62
63
|
this.started = false;
|
|
@@ -69,6 +70,7 @@ class StreamAuditor {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
_diagnostic(severity, code, index, type, details) {
|
|
73
|
+
if (severity === 'error') this.hasErrors = true;
|
|
72
74
|
if (this.diagnostics.length >= this.limits.maxDiagnostics) {
|
|
73
75
|
this.droppedDiagnostics += 1;
|
|
74
76
|
return;
|
|
@@ -95,6 +97,13 @@ class StreamAuditor {
|
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
_requireOpenItem(event, index, type) {
|
|
101
|
+
const parent = `item:${event.output_index}`;
|
|
102
|
+
if (!this.openUnits.has(parent)) {
|
|
103
|
+
this._diagnostic('error', this.doneUnits.has(parent) ? 'content_after_item_done' : 'content_without_item', index, type, { parent });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
98
107
|
_done(key, index, type) {
|
|
99
108
|
if (!this.openUnits.has(key)) {
|
|
100
109
|
this._diagnostic('error', this.doneUnits.has(key) ? 'duplicate_done' : 'done_without_start', index, type, { unit: key });
|
|
@@ -167,14 +176,17 @@ class StreamAuditor {
|
|
|
167
176
|
return;
|
|
168
177
|
}
|
|
169
178
|
if (type === 'response.content_part.added') {
|
|
179
|
+
this._requireOpenItem(event, index, type);
|
|
170
180
|
this._open(`content:${event.output_index}:${event.content_index}`, index, type);
|
|
171
181
|
return;
|
|
172
182
|
}
|
|
173
183
|
if (type === 'response.content_part.done') {
|
|
184
|
+
this._requireOpenItem(event, index, type);
|
|
174
185
|
this._done(`content:${event.output_index}:${event.content_index}`, index, type);
|
|
175
186
|
return;
|
|
176
187
|
}
|
|
177
188
|
if (/\.delta$/.test(type)) {
|
|
189
|
+
if (Number.isSafeInteger(event.content_index)) this._requireOpenItem(event, index, type);
|
|
178
190
|
const key = Number.isSafeInteger(event.content_index)
|
|
179
191
|
? `content:${event.output_index}:${event.content_index}`
|
|
180
192
|
: `item:${event.output_index}`;
|
|
@@ -245,9 +257,8 @@ class StreamAuditor {
|
|
|
245
257
|
if (!this.started && !this.terminal && this.eventCount > 0) this._diagnostic('error', 'missing_start', this.eventCount);
|
|
246
258
|
if (!this.terminal && !this.boundExceeded) this._diagnostic('error', 'missing_terminal', this.eventCount);
|
|
247
259
|
|
|
248
|
-
const hasErrors = this.diagnostics.some(({ severity }) => severity === 'error');
|
|
249
260
|
let outcome;
|
|
250
|
-
if (this.boundExceeded || hasErrors && this.terminal && this.terminal.kind === 'completed') outcome = 'invalid';
|
|
261
|
+
if (this.boundExceeded || this.hasErrors && this.terminal && this.terminal.kind === 'completed') outcome = 'invalid';
|
|
251
262
|
else if (this.terminal) outcome = this.terminal.kind === 'completed' && this.dialect === 'anthropic-messages' && this.stopReason === 'tool_use'
|
|
252
263
|
? 'tool_use'
|
|
253
264
|
: this.terminal.kind === 'completed' && this.dialect === 'anthropic-messages' && this.stopReason === 'pause_turn'
|