Problem
PR #40 (slice 1 of #39) added envelope schema validation to the `generateResponse` path for Anthropic. The `streamResponse` path is not covered.
Current streaming code in `src/providers/anthropic.ts`:
```ts
try {
const parsed = JSON.parse(data);
if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
controller.enqueue(parsed.delta.text);
}
} catch {
// Malformed SSE chunk - skip silently
}
```
If Anthropic reshapes the SSE delta envelope — renames `content_block_delta` to `content_delta`, moves `delta.text` to `delta.content`, etc. — streaming consumers get partial/empty output with no error, no hook, no fallback. The silent-skip catch makes the failure quieter than the generateResponse path, not louder.
This is worse than the non-streaming drift case: at least a crashed `generateResponse` triggers fallback. A broken stream just returns short output and the caller has no idea anything went wrong until someone notices responses getting shorter.
Scope
- Define a streaming event schema per provider. For Anthropic: `content_block_delta`, `message_delta`, `message_stop`, `message_start` event shapes. For OpenAI-compatible providers: `choices[0].delta.content` shape.
- Validate each SSE chunk as it parses. On mismatch, throw `SchemaDriftError` into the stream (`controller.error`). The downstream consumer can then react like a failed request — log, alert, fail the operation.
- Fire `onSchemaDrift` from the stream error handler. Currently streaming hooks don't have factory-level drift routing, so this needs its own wiring in `generateResponseStream`.
- Stop swallowing JSON parse errors silently. If `JSON.parse(data)` throws on a non-`[DONE]` chunk, that's a drift signal — surface it. Today we drop it and keep going.
Open questions
- Fallback mid-stream: if we detect drift three chunks into a stream, can we retry on another provider? Probably not without buffering, which defeats the point of streaming. Proposal: mid-stream drift terminates the stream with an error, caller retries end-to-end if they want. Not invisible-fallback.
- Partial response handling: what does the caller see if drift happens after we've already yielded 50 tokens? Proposal: emit an error on the stream (via `controller.error`) after the last good chunk. Most stream consumers handle this as an abort.
Tests
- Anthropic streaming with reshaped `delta` field → SchemaDriftError surfaces via stream controller
- Anthropic streaming with valid shape → still yields tokens correctly
- OpenAI-compatible streaming with missing `delta.content` → SchemaDriftError
- Stream error fires `onSchemaDrift` hook with path/expected/actual
Priority
HIGH. Review finding from PR #40. Streaming is the silent-failure path — users won't notice until they get a support ticket.
Related
Problem
PR #40 (slice 1 of #39) added envelope schema validation to the `generateResponse` path for Anthropic. The `streamResponse` path is not covered.
Current streaming code in `src/providers/anthropic.ts`:
```ts
try {
const parsed = JSON.parse(data);
if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
controller.enqueue(parsed.delta.text);
}
} catch {
// Malformed SSE chunk - skip silently
}
```
If Anthropic reshapes the SSE delta envelope — renames `content_block_delta` to `content_delta`, moves `delta.text` to `delta.content`, etc. — streaming consumers get partial/empty output with no error, no hook, no fallback. The silent-skip catch makes the failure quieter than the generateResponse path, not louder.
This is worse than the non-streaming drift case: at least a crashed `generateResponse` triggers fallback. A broken stream just returns short output and the caller has no idea anything went wrong until someone notices responses getting shorter.
Scope
Open questions
Tests
Priority
HIGH. Review finding from PR #40. Streaming is the silent-failure path — users won't notice until they get a support ticket.
Related