From 07c71ae6326952621ebad0bff33123bdaaf8f0f7 Mon Sep 17 00:00:00 2001 From: Nick Hollon Date: Tue, 21 Apr 2026 13:25:52 -0400 Subject: [PATCH] docs(core): document positional-fallback fragility in bridge When a provider emits content_blocks without an `index` field (e.g. anthropic `_stream` with coerce_content_to_string=True for pure text), the bridge's positional-0 fallback merges successive chunks into one block. This works correctly today because the only coerced-string path in the anthropic integration is mutually exclusive with any structured (indexed) emission. Document the scenario that would surface the gap and the two ways to close it (native _stream_chat_model_events hook or 'continue on anonymous key' bridge rule) so a future integration doesn't discover the edge case the hard way. --- .../language_models/_compat_bridge.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libs/core/langchain_core/language_models/_compat_bridge.py b/libs/core/langchain_core/language_models/_compat_bridge.py index 6ec1a988f95..1118bdcd599 100644 --- a/libs/core/langchain_core/language_models/_compat_bridge.py +++ b/libs/core/langchain_core/language_models/_compat_bridge.py @@ -121,6 +121,25 @@ def _iter_protocol_blocks(msg: BaseMessage) -> list[tuple[Any, CompatBlock]]: For finalized :class:`AIMessage`, also surfaces `invalid_tool_calls` — which `AIMessage.content_blocks` currently omits from its return value even though they are a defined protocol block type. + + The positional fallback is a known fragility: when a provider emits + blocks without an `index` field (e.g. Anthropic's `_stream` with + `coerce_content_to_string=True`, where text chunks lose their + source-side index), every such chunk gets positional key 0 and + successive chunks merge into one block. This works correctly for + single-type streams (pure-text responses merge cleanly) because all + chunks share the same key and the open-block logic collapses them. + It would miscategorise a stream that mixed indexed structured + blocks with non-indexed coerced-text blocks, since an indexed + block with `index == 0` would collide with the anonymous text + block's positional-0 key. In the anthropic integration this + cannot currently occur: coerce-to-string mode is only selected + when no tools, thinking, or documents are present, and any of + those flips the stream to structured mode where every block + carries an integer index. A native `_stream_chat_model_events` + hook per provider (or a bridge-level "continue the open block when + the source has no identity" rule) would close the gap if another + integration ever emits mixed content. """ try: raw = msg.content_blocks