can1357/oh-my-pi · error · AnthropicStreamEnvelopeError
stream ended before message_stop
Error message
stream ended before message_stop
What it means
This AnthropicStreamEnvelopeError is thrown when the stream ends without a terminal envelope: neither a message_delta carrying stop_reason nor a message_stop frame arrived. The library deliberately fails the turn instead of finalizing the partial message, because treating a truncated stream as a clean stop would make the agent loop think the turn completed and silently halt mid-sentence.
Source
Thrown at packages/ai/src/providers/anthropic.ts:2674
throw firstEventTimeoutError;
}
if (activeAbortTracker.wasCallerAbort()) {
throw new AIError.AbortError();
}
if (!sawEvent || !sawMessageStart) {
throw new AIError.AnthropicStreamEnvelopeError("stream ended before message_start");
}
if (!sawTerminalEnvelope) {
// Neither a message_delta stop_reason nor message_stop arrived: the
// connection died mid-generation. Finalizing the partial message as
// a clean "stop" would make the agent loop treat the truncated turn
// as complete (silent mid-sentence halt), so fail the turn. The
// envelope error is transparently retried before replay-unsafe
// content streams; afterwards it surfaces as an error turn whose
// complete tool calls the agent loop salvages
// (`recoverTransientErrorToolTurn` recognizes the envelope-error
// text and `retainCompletedToolCalls` drops half-streamed calls).
throw new AIError.AnthropicStreamEnvelopeError("stream ended before message_stop");
}
if (!sawMessageStop) {
// A stop_reason arrived via message_delta, so generation finished;
// only the trailing message_stop frame is missing (non-conforming
// gateway). Degrade to best-effort instead of discarding the turn.
reportAnthropicEnvelopeAnomaly("stream ended before message_stop");
}
if (openBlocks.size > 0) {
for (const [openIndex, openBlock] of openBlocks) {
reportAnthropicEnvelopeAnomaly(
`stream ended with an unterminated ${openBlock.kind} block at index ${openIndex}`,
);
if (openBlock.kind === "ignored" || openBlock.contentIndex < 0) continue;
const danglingBlock = blocks[openBlock.contentIndex];
if (danglingBlock) finalizeStreamBlock(danglingBlock, openBlock.contentIndex);
}
openBlocks.clear();
}View on GitHub (pinned to 9690622007)
Solutions
- Retry the turn — envelope errors are retried before replay-unsafe streams and later surface as an error turn whose completed tool calls are salvaged via recoverTransientErrorToolCall/retainCompletedToolCalls.
- Reduce generation length (lower max_tokens, shorter prompts) or split work into smaller turns so streams finish within proxy timeouts.
- Raise proxy/LB idle and response timeouts to exceed worst-case generation time.
- Check network stability and whether the failure correlates with a specific gateway that caps stream duration.
Defensive patterns
Strategy: retry
Validate before calling
// Ensure max_tokens and prompt size keep generation well under proxy response-time limits:
if (estimatedGenerationMs > proxyTimeoutMs) {
throw new Error("Generation likely exceeds gateway timeout; reduce max_tokens or raise the proxy timeout.");
} Try / catch
try {
yield* streamAnthropicEvents(request);
} catch (err) {
if (err instanceof AIError.AnthropicStreamEnvelopeError && /ended before message_stop/.test(err.message)) {
// salvage completed tool calls via recoverTransientErrorToolTurn / retainCompletedToolCalls
// then retry the remaining generation
} else {
throw err;
}
} Prevention
- Configure gateway/LB response-time caps above worst-case generation duration.
- Lower max_tokens or split long tasks into multiple turns to shorten streams.
- Keep the agent-loop salvage path (recoverTransientErrorToolTurn) enabled so completed tool calls survive retries.
- Alert on truncated-stream rates per gateway to catch timeout misconfigurations early.
When it happens
Trigger: SSE iteration ends after message_start (and possibly partial content blocks) but before message_delta/message_stop — e.g. connection drop mid-generation, proxy cutting long-running streams, or gateway max-duration limits; notably distinct from the later message_stop-only degradation, which is tolerated as an anomaly.
Common situations: Load balancers or Cloudflare idle timeouts killing long generations; mobile/unstable networks; gateways imposing response-time caps shorter than generation time; server crashes mid-stream; max_tokens not reached but connection severed.
Related errors
- stream ended before message_start
- V2 compaction stream closed before response.completed
- AnthropicConnectionError
- AnthropicConnectionTimeoutError
- Attempted to iterate over an Anthropic response with no body
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/1579e0019773682a.
Report an issue: GitHub.