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

  1. 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.
  2. Reduce generation length (lower max_tokens, shorter prompts) or split work into smaller turns so streams finish within proxy timeouts.
  3. Raise proxy/LB idle and response timeouts to exceed worst-case generation time.
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/1579e0019773682a. Report an issue: GitHub.