langchain-core 0.4.0.dev0__py3-none-any.whl → 1.0.0__py3-none-any.whl

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.

Potentially problematic release.


This version of langchain-core might be problematic. Click here for more details.

Files changed (172) hide show
  1. langchain_core/__init__.py +1 -1
  2. langchain_core/_api/__init__.py +3 -4
  3. langchain_core/_api/beta_decorator.py +45 -70
  4. langchain_core/_api/deprecation.py +80 -80
  5. langchain_core/_api/path.py +22 -8
  6. langchain_core/_import_utils.py +10 -4
  7. langchain_core/agents.py +25 -21
  8. langchain_core/caches.py +53 -63
  9. langchain_core/callbacks/__init__.py +1 -8
  10. langchain_core/callbacks/base.py +341 -348
  11. langchain_core/callbacks/file.py +55 -44
  12. langchain_core/callbacks/manager.py +546 -683
  13. langchain_core/callbacks/stdout.py +29 -30
  14. langchain_core/callbacks/streaming_stdout.py +35 -36
  15. langchain_core/callbacks/usage.py +65 -70
  16. langchain_core/chat_history.py +48 -55
  17. langchain_core/document_loaders/base.py +46 -21
  18. langchain_core/document_loaders/langsmith.py +39 -36
  19. langchain_core/documents/__init__.py +0 -1
  20. langchain_core/documents/base.py +96 -74
  21. langchain_core/documents/compressor.py +12 -9
  22. langchain_core/documents/transformers.py +29 -28
  23. langchain_core/embeddings/fake.py +56 -57
  24. langchain_core/env.py +2 -3
  25. langchain_core/example_selectors/base.py +12 -0
  26. langchain_core/example_selectors/length_based.py +1 -1
  27. langchain_core/example_selectors/semantic_similarity.py +21 -25
  28. langchain_core/exceptions.py +15 -9
  29. langchain_core/globals.py +4 -163
  30. langchain_core/indexing/api.py +132 -125
  31. langchain_core/indexing/base.py +64 -67
  32. langchain_core/indexing/in_memory.py +26 -6
  33. langchain_core/language_models/__init__.py +15 -27
  34. langchain_core/language_models/_utils.py +267 -117
  35. langchain_core/language_models/base.py +92 -177
  36. langchain_core/language_models/chat_models.py +547 -407
  37. langchain_core/language_models/fake.py +11 -11
  38. langchain_core/language_models/fake_chat_models.py +72 -118
  39. langchain_core/language_models/llms.py +168 -242
  40. langchain_core/load/dump.py +8 -11
  41. langchain_core/load/load.py +32 -28
  42. langchain_core/load/mapping.py +2 -4
  43. langchain_core/load/serializable.py +50 -56
  44. langchain_core/messages/__init__.py +36 -51
  45. langchain_core/messages/ai.py +377 -150
  46. langchain_core/messages/base.py +239 -47
  47. langchain_core/messages/block_translators/__init__.py +111 -0
  48. langchain_core/messages/block_translators/anthropic.py +470 -0
  49. langchain_core/messages/block_translators/bedrock.py +94 -0
  50. langchain_core/messages/block_translators/bedrock_converse.py +297 -0
  51. langchain_core/messages/block_translators/google_genai.py +530 -0
  52. langchain_core/messages/block_translators/google_vertexai.py +21 -0
  53. langchain_core/messages/block_translators/groq.py +143 -0
  54. langchain_core/messages/block_translators/langchain_v0.py +301 -0
  55. langchain_core/messages/block_translators/openai.py +1010 -0
  56. langchain_core/messages/chat.py +2 -3
  57. langchain_core/messages/content.py +1423 -0
  58. langchain_core/messages/function.py +7 -7
  59. langchain_core/messages/human.py +44 -38
  60. langchain_core/messages/modifier.py +3 -2
  61. langchain_core/messages/system.py +40 -27
  62. langchain_core/messages/tool.py +160 -58
  63. langchain_core/messages/utils.py +527 -638
  64. langchain_core/output_parsers/__init__.py +1 -14
  65. langchain_core/output_parsers/base.py +68 -104
  66. langchain_core/output_parsers/json.py +13 -17
  67. langchain_core/output_parsers/list.py +11 -33
  68. langchain_core/output_parsers/openai_functions.py +56 -74
  69. langchain_core/output_parsers/openai_tools.py +68 -109
  70. langchain_core/output_parsers/pydantic.py +15 -13
  71. langchain_core/output_parsers/string.py +6 -2
  72. langchain_core/output_parsers/transform.py +17 -60
  73. langchain_core/output_parsers/xml.py +34 -44
  74. langchain_core/outputs/__init__.py +1 -1
  75. langchain_core/outputs/chat_generation.py +26 -11
  76. langchain_core/outputs/chat_result.py +1 -3
  77. langchain_core/outputs/generation.py +17 -6
  78. langchain_core/outputs/llm_result.py +15 -8
  79. langchain_core/prompt_values.py +29 -123
  80. langchain_core/prompts/__init__.py +3 -27
  81. langchain_core/prompts/base.py +48 -63
  82. langchain_core/prompts/chat.py +259 -288
  83. langchain_core/prompts/dict.py +19 -11
  84. langchain_core/prompts/few_shot.py +84 -90
  85. langchain_core/prompts/few_shot_with_templates.py +14 -12
  86. langchain_core/prompts/image.py +19 -14
  87. langchain_core/prompts/loading.py +6 -8
  88. langchain_core/prompts/message.py +7 -8
  89. langchain_core/prompts/prompt.py +42 -43
  90. langchain_core/prompts/string.py +37 -16
  91. langchain_core/prompts/structured.py +43 -46
  92. langchain_core/rate_limiters.py +51 -60
  93. langchain_core/retrievers.py +52 -192
  94. langchain_core/runnables/base.py +1727 -1683
  95. langchain_core/runnables/branch.py +52 -73
  96. langchain_core/runnables/config.py +89 -103
  97. langchain_core/runnables/configurable.py +128 -130
  98. langchain_core/runnables/fallbacks.py +93 -82
  99. langchain_core/runnables/graph.py +127 -127
  100. langchain_core/runnables/graph_ascii.py +63 -41
  101. langchain_core/runnables/graph_mermaid.py +87 -70
  102. langchain_core/runnables/graph_png.py +31 -36
  103. langchain_core/runnables/history.py +145 -161
  104. langchain_core/runnables/passthrough.py +141 -144
  105. langchain_core/runnables/retry.py +84 -68
  106. langchain_core/runnables/router.py +33 -37
  107. langchain_core/runnables/schema.py +79 -72
  108. langchain_core/runnables/utils.py +95 -139
  109. langchain_core/stores.py +85 -131
  110. langchain_core/structured_query.py +11 -15
  111. langchain_core/sys_info.py +31 -32
  112. langchain_core/tools/__init__.py +1 -14
  113. langchain_core/tools/base.py +221 -247
  114. langchain_core/tools/convert.py +144 -161
  115. langchain_core/tools/render.py +10 -10
  116. langchain_core/tools/retriever.py +12 -19
  117. langchain_core/tools/simple.py +52 -29
  118. langchain_core/tools/structured.py +56 -60
  119. langchain_core/tracers/__init__.py +1 -9
  120. langchain_core/tracers/_streaming.py +6 -7
  121. langchain_core/tracers/base.py +103 -112
  122. langchain_core/tracers/context.py +29 -48
  123. langchain_core/tracers/core.py +142 -105
  124. langchain_core/tracers/evaluation.py +30 -34
  125. langchain_core/tracers/event_stream.py +162 -117
  126. langchain_core/tracers/langchain.py +34 -36
  127. langchain_core/tracers/log_stream.py +87 -49
  128. langchain_core/tracers/memory_stream.py +3 -3
  129. langchain_core/tracers/root_listeners.py +18 -34
  130. langchain_core/tracers/run_collector.py +8 -20
  131. langchain_core/tracers/schemas.py +0 -125
  132. langchain_core/tracers/stdout.py +3 -3
  133. langchain_core/utils/__init__.py +1 -4
  134. langchain_core/utils/_merge.py +47 -9
  135. langchain_core/utils/aiter.py +70 -66
  136. langchain_core/utils/env.py +12 -9
  137. langchain_core/utils/function_calling.py +139 -206
  138. langchain_core/utils/html.py +7 -8
  139. langchain_core/utils/input.py +6 -6
  140. langchain_core/utils/interactive_env.py +6 -2
  141. langchain_core/utils/iter.py +48 -45
  142. langchain_core/utils/json.py +14 -4
  143. langchain_core/utils/json_schema.py +159 -43
  144. langchain_core/utils/mustache.py +32 -25
  145. langchain_core/utils/pydantic.py +67 -40
  146. langchain_core/utils/strings.py +5 -5
  147. langchain_core/utils/usage.py +1 -1
  148. langchain_core/utils/utils.py +104 -62
  149. langchain_core/vectorstores/base.py +131 -179
  150. langchain_core/vectorstores/in_memory.py +113 -182
  151. langchain_core/vectorstores/utils.py +23 -17
  152. langchain_core/version.py +1 -1
  153. langchain_core-1.0.0.dist-info/METADATA +68 -0
  154. langchain_core-1.0.0.dist-info/RECORD +172 -0
  155. {langchain_core-0.4.0.dev0.dist-info → langchain_core-1.0.0.dist-info}/WHEEL +1 -1
  156. langchain_core/beta/__init__.py +0 -1
  157. langchain_core/beta/runnables/__init__.py +0 -1
  158. langchain_core/beta/runnables/context.py +0 -448
  159. langchain_core/memory.py +0 -116
  160. langchain_core/messages/content_blocks.py +0 -1435
  161. langchain_core/prompts/pipeline.py +0 -133
  162. langchain_core/pydantic_v1/__init__.py +0 -30
  163. langchain_core/pydantic_v1/dataclasses.py +0 -23
  164. langchain_core/pydantic_v1/main.py +0 -23
  165. langchain_core/tracers/langchain_v1.py +0 -23
  166. langchain_core/utils/loading.py +0 -31
  167. langchain_core/v1/__init__.py +0 -1
  168. langchain_core/v1/chat_models.py +0 -1047
  169. langchain_core/v1/messages.py +0 -755
  170. langchain_core-0.4.0.dev0.dist-info/METADATA +0 -108
  171. langchain_core-0.4.0.dev0.dist-info/RECORD +0 -177
  172. langchain_core-0.4.0.dev0.dist-info/entry_points.txt +0 -4
@@ -1,15 +1,47 @@
1
- import copy
2
1
  import re
3
2
  from collections.abc import Sequence
4
- from typing import Optional
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Literal,
6
+ TypedDict,
7
+ TypeVar,
8
+ )
5
9
 
6
- from langchain_core.messages import BaseMessage
7
- from langchain_core.v1.messages import MessageV1
10
+ if TYPE_CHECKING:
11
+ from langchain_core.messages import BaseMessage
12
+ from langchain_core.messages.content import (
13
+ ContentBlock,
14
+ )
8
15
 
9
16
 
10
- def _is_openai_data_block(block: dict) -> bool:
11
- """Check if the block contains multimodal data in OpenAI Chat Completions format."""
17
+ def is_openai_data_block(
18
+ block: dict, filter_: Literal["image", "audio", "file"] | None = None
19
+ ) -> bool:
20
+ """Check whether a block contains multimodal data in OpenAI Chat Completions format.
21
+
22
+ Supports both data and ID-style blocks (e.g. `'file_data'` and `'file_id'`)
23
+
24
+ If additional keys are present, they are ignored / will not affect outcome as long
25
+ as the required keys are present and valid.
26
+
27
+ Args:
28
+ block: The content block to check.
29
+ filter_: If provided, only return True for blocks matching this specific type.
30
+ - "image": Only match image_url blocks
31
+ - "audio": Only match input_audio blocks
32
+ - "file": Only match file blocks
33
+ If `None`, match any valid OpenAI data block type. Note that this means that
34
+ if the block has a valid OpenAI data type but the filter_ is set to a
35
+ different type, this function will return False.
36
+
37
+ Returns:
38
+ `True` if the block is a valid OpenAI data block and matches the filter_
39
+ (if provided).
40
+
41
+ """
12
42
  if block.get("type") == "image_url":
43
+ if filter_ is not None and filter_ != "image":
44
+ return False
13
45
  if (
14
46
  (set(block.keys()) <= {"type", "image_url", "detail"})
15
47
  and (image_url := block.get("image_url"))
@@ -17,160 +49,278 @@ def _is_openai_data_block(block: dict) -> bool:
17
49
  ):
18
50
  url = image_url.get("url")
19
51
  if isinstance(url, str):
52
+ # Required per OpenAI spec
53
+ return True
54
+ # Ignore `'detail'` since it's optional and specific to OpenAI
55
+
56
+ elif block.get("type") == "input_audio":
57
+ if filter_ is not None and filter_ != "audio":
58
+ return False
59
+ if (audio := block.get("input_audio")) and isinstance(audio, dict):
60
+ audio_data = audio.get("data")
61
+ audio_format = audio.get("format")
62
+ # Both required per OpenAI spec
63
+ if isinstance(audio_data, str) and isinstance(audio_format, str):
20
64
  return True
21
65
 
22
66
  elif block.get("type") == "file":
67
+ if filter_ is not None and filter_ != "file":
68
+ return False
23
69
  if (file := block.get("file")) and isinstance(file, dict):
24
70
  file_data = file.get("file_data")
25
- if isinstance(file_data, str):
26
- return True
27
-
28
- elif block.get("type") == "input_audio":
29
- if (input_audio := block.get("input_audio")) and isinstance(input_audio, dict):
30
- audio_data = input_audio.get("data")
31
- audio_format = input_audio.get("format")
32
- if isinstance(audio_data, str) and isinstance(audio_format, str):
71
+ file_id = file.get("file_id")
72
+ # Files can be either base64-encoded or pre-uploaded with an ID
73
+ if isinstance(file_data, str) or isinstance(file_id, str):
33
74
  return True
34
75
 
35
76
  else:
36
77
  return False
37
78
 
79
+ # Has no `'type'` key
38
80
  return False
39
81
 
40
82
 
41
- def _parse_data_uri(uri: str) -> Optional[dict]:
42
- """Parse a data URI into its components. If parsing fails, return None.
43
-
44
- Example:
83
+ class ParsedDataUri(TypedDict):
84
+ source_type: Literal["base64"]
85
+ data: str
86
+ mime_type: str
45
87
 
46
- .. code-block:: python
47
88
 
48
- data_uri = "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
49
- parsed = _parse_data_uri(data_uri)
89
+ def _parse_data_uri(uri: str) -> ParsedDataUri | None:
90
+ """Parse a data URI into its components.
50
91
 
51
- assert parsed == {
52
- "source_type": "base64",
53
- "mime_type": "image/jpeg",
54
- "data": "/9j/4AAQSkZJRg...",
55
- }
92
+ If parsing fails, return `None`. If either MIME type or data is missing, return
93
+ `None`.
56
94
 
95
+ Example:
96
+ ```python
97
+ data_uri = "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
98
+ parsed = _parse_data_uri(data_uri)
99
+
100
+ assert parsed == {
101
+ "source_type": "base64",
102
+ "mime_type": "image/jpeg",
103
+ "data": "/9j/4AAQSkZJRg...",
104
+ }
105
+ ```
57
106
  """
58
107
  regex = r"^data:(?P<mime_type>[^;]+);base64,(?P<data>.+)$"
59
108
  match = re.match(regex, uri)
60
109
  if match is None:
61
110
  return None
111
+
112
+ mime_type = match.group("mime_type")
113
+ data = match.group("data")
114
+ if not mime_type or not data:
115
+ return None
116
+
62
117
  return {
63
118
  "source_type": "base64",
64
- "data": match.group("data"),
65
- "mime_type": match.group("mime_type"),
119
+ "data": data,
120
+ "mime_type": mime_type,
66
121
  }
67
122
 
68
123
 
69
- def _convert_openai_format_to_data_block(block: dict) -> dict:
70
- """Convert OpenAI image content block to standard data content block.
124
+ def _normalize_messages(
125
+ messages: Sequence["BaseMessage"],
126
+ ) -> list["BaseMessage"]:
127
+ """Normalize message formats to LangChain v1 standard content blocks.
128
+
129
+ Chat models already implement support for:
130
+ - Images in OpenAI Chat Completions format
131
+ These will be passed through unchanged
132
+ - LangChain v1 standard content blocks
133
+
134
+ This function extends support to:
135
+ - `[Audio](https://platform.openai.com/docs/api-reference/chat/create) and
136
+ `[file](https://platform.openai.com/docs/api-reference/files) data in OpenAI
137
+ Chat Completions format
138
+ - Images are technically supported but we expect chat models to handle them
139
+ directly; this may change in the future
140
+ - LangChain v0 standard content blocks for backward compatibility
141
+
142
+ !!! warning "Behavior changed in 1.0.0"
143
+ In previous versions, this function returned messages in LangChain v0 format.
144
+ Now, it returns messages in LangChain v1 format, which upgraded chat models now
145
+ expect to receive when passing back in message history. For backward
146
+ compatibility, this function will convert v0 message content to v1 format.
147
+
148
+ ??? note "v0 Content Block Schemas"
149
+
150
+ `URLContentBlock`:
151
+
152
+ ```python
153
+ {
154
+ mime_type: NotRequired[str]
155
+ type: Literal['image', 'audio', 'file'],
156
+ source_type: Literal['url'],
157
+ url: str,
158
+ }
159
+ ```
160
+
161
+ `Base64ContentBlock`:
162
+
163
+ ```python
164
+ {
165
+ mime_type: NotRequired[str]
166
+ type: Literal['image', 'audio', 'file'],
167
+ source_type: Literal['base64'],
168
+ data: str,
169
+ }
170
+ ```
171
+
172
+ `IDContentBlock`:
173
+
174
+ (In practice, this was never used)
175
+
176
+ ```python
177
+ {
178
+ type: Literal["image", "audio", "file"],
179
+ source_type: Literal["id"],
180
+ id: str,
181
+ }
182
+ ```
183
+
184
+ `PlainTextContentBlock`:
185
+
186
+ ```python
187
+ {
188
+ mime_type: NotRequired[str]
189
+ type: Literal['file'],
190
+ source_type: Literal['text'],
191
+ url: str,
192
+ }
193
+ ```
194
+
195
+ If a v1 message is passed in, it will be returned as-is, meaning it is safe to
196
+ always pass in v1 messages to this function for assurance.
197
+
198
+ For posterity, here are the OpenAI Chat Completions schemas we expect:
199
+
200
+ Chat Completions image. Can be URL-based or base64-encoded. Supports MIME types
201
+ png, jpeg/jpg, webp, static gif:
202
+ {
203
+ "type": Literal['image_url'],
204
+ "image_url": {
205
+ "url": Union["data:$MIME_TYPE;base64,$BASE64_ENCODED_IMAGE", "$IMAGE_URL"],
206
+ "detail": Literal['low', 'high', 'auto'] = 'auto', # Supported by OpenAI
207
+ }
208
+ }
71
209
 
72
- If parsing fails, pass-through.
210
+ Chat Completions audio:
211
+ {
212
+ "type": Literal['input_audio'],
213
+ "input_audio": {
214
+ "format": Literal['wav', 'mp3'],
215
+ "data": str = "$BASE64_ENCODED_AUDIO",
216
+ },
217
+ }
73
218
 
74
- Args:
75
- block: The OpenAI image content block to convert.
219
+ Chat Completions files: either base64 or pre-uploaded file ID
220
+ {
221
+ "type": Literal['file'],
222
+ "file": Union[
223
+ {
224
+ "filename": str | None = "$FILENAME",
225
+ "file_data": str = "$BASE64_ENCODED_FILE",
226
+ },
227
+ {
228
+ "file_id": str = "$FILE_ID", # For pre-uploaded files to OpenAI
229
+ },
230
+ ],
231
+ }
76
232
 
77
- Returns:
78
- The converted standard data content block.
79
- """
80
- if block["type"] == "image_url":
81
- parsed = _parse_data_uri(block["image_url"]["url"])
82
- if parsed is not None:
83
- parsed["type"] = "image"
84
- return parsed
85
- return block
86
-
87
- if block["type"] == "file":
88
- parsed = _parse_data_uri(block["file"]["file_data"])
89
- if parsed is not None:
90
- parsed["type"] = "file"
91
- if filename := block["file"].get("filename"):
92
- parsed["filename"] = filename
93
- return parsed
94
- return block
95
-
96
- if block["type"] == "input_audio":
97
- data = block["input_audio"].get("data")
98
- audio_format = block["input_audio"].get("format")
99
- if data and audio_format:
100
- return {
101
- "type": "audio",
102
- "source_type": "base64",
103
- "data": data,
104
- "mime_type": f"audio/{audio_format}",
105
- }
106
- return block
107
-
108
- return block
109
-
110
-
111
- def _normalize_messages(messages: Sequence[BaseMessage]) -> list[BaseMessage]:
112
- """Extend support for message formats.
113
-
114
- Chat models implement support for images in OpenAI Chat Completions format, as well
115
- as other multimodal data as standard data blocks. This function extends support to
116
- audio and file data in OpenAI Chat Completions format by converting them to standard
117
- data blocks.
118
233
  """
234
+ from langchain_core.messages.block_translators.langchain_v0 import ( # noqa: PLC0415
235
+ _convert_legacy_v0_content_block_to_v1,
236
+ )
237
+ from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
238
+ _convert_openai_format_to_data_block,
239
+ )
240
+
119
241
  formatted_messages = []
120
242
  for message in messages:
243
+ # We preserve input messages - the caller may reuse them elsewhere and expects
244
+ # them to remain unchanged. We only create a copy if we need to translate.
121
245
  formatted_message = message
246
+
122
247
  if isinstance(message.content, list):
123
248
  for idx, block in enumerate(message.content):
249
+ # OpenAI Chat Completions multimodal data blocks to v1 standard
124
250
  if (
125
251
  isinstance(block, dict)
126
- # Subset to (PDF) files and audio, as most relevant chat models
127
- # support images in OAI format (and some may not yet support the
128
- # standard data block format)
129
- and block.get("type") in {"file", "input_audio"}
130
- and _is_openai_data_block(block)
252
+ and block.get("type") in {"input_audio", "file"}
253
+ # Discriminate between OpenAI/LC format since they share `'type'`
254
+ and is_openai_data_block(block)
131
255
  ):
132
- if formatted_message is message:
133
- formatted_message = message.model_copy()
134
- # Also shallow-copy content
135
- formatted_message.content = list(formatted_message.content)
136
-
137
- formatted_message.content[idx] = ( # type: ignore[index] # mypy confused by .model_copy
138
- _convert_openai_format_to_data_block(block)
139
- )
140
- formatted_messages.append(formatted_message)
141
-
142
- return formatted_messages
143
-
256
+ formatted_message = _ensure_message_copy(message, formatted_message)
144
257
 
145
- def _normalize_messages_v1(messages: Sequence[MessageV1]) -> list[MessageV1]:
146
- """Extend support for message formats.
258
+ converted_block = _convert_openai_format_to_data_block(block)
259
+ _update_content_block(formatted_message, idx, converted_block)
147
260
 
148
- Chat models implement support for images in OpenAI Chat Completions format, as well
149
- as other multimodal data as standard data blocks. This function extends support to
150
- audio and file data in OpenAI Chat Completions format by converting them to standard
151
- data blocks.
152
- """
153
- formatted_messages = []
154
- for message in messages:
155
- formatted_message = message
156
- if isinstance(message.content, list):
157
- for idx, block in enumerate(message.content):
158
- if (
261
+ # Convert multimodal LangChain v0 to v1 standard content blocks
262
+ elif (
159
263
  isinstance(block, dict)
160
- # Subset to (PDF) files and audio, as most relevant chat models
161
- # support images in OAI format (and some may not yet support the
162
- # standard data block format)
163
- and block.get("type") in {"file", "input_audio"}
164
- and _is_openai_data_block(block) # type: ignore[arg-type]
264
+ and block.get("type")
265
+ in {
266
+ "image",
267
+ "audio",
268
+ "file",
269
+ }
270
+ and block.get("source_type") # v1 doesn't have `source_type`
271
+ in {
272
+ "url",
273
+ "base64",
274
+ "id",
275
+ "text",
276
+ }
165
277
  ):
166
- if formatted_message is message:
167
- formatted_message = copy.copy(message)
168
- # Also shallow-copy content
169
- formatted_message.content = list(formatted_message.content)
170
-
171
- formatted_message.content[idx] = ( # type: ignore[call-overload]
172
- _convert_openai_format_to_data_block(block) # type: ignore[arg-type]
173
- )
278
+ formatted_message = _ensure_message_copy(message, formatted_message)
279
+
280
+ converted_block = _convert_legacy_v0_content_block_to_v1(block)
281
+ _update_content_block(formatted_message, idx, converted_block)
282
+ continue
283
+
284
+ # else, pass through blocks that look like they have v1 format unchanged
285
+
174
286
  formatted_messages.append(formatted_message)
175
287
 
176
288
  return formatted_messages
289
+
290
+
291
+ T = TypeVar("T", bound="BaseMessage")
292
+
293
+
294
+ def _ensure_message_copy(message: T, formatted_message: T) -> T:
295
+ """Create a copy of the message if it hasn't been copied yet."""
296
+ if formatted_message is message:
297
+ formatted_message = message.model_copy()
298
+ # Shallow-copy content list to allow modifications
299
+ formatted_message.content = list(formatted_message.content)
300
+ return formatted_message
301
+
302
+
303
+ def _update_content_block(
304
+ formatted_message: "BaseMessage", idx: int, new_block: ContentBlock | dict
305
+ ) -> None:
306
+ """Update a content block at the given index, handling type issues."""
307
+ # Type ignore needed because:
308
+ # - `BaseMessage.content` is typed as `Union[str, list[Union[str, dict]]]`
309
+ # - When content is str, indexing fails (index error)
310
+ # - When content is list, the items are `Union[str, dict]` but we're assigning
311
+ # `Union[ContentBlock, dict]` where ContentBlock is richer than dict
312
+ # - This is safe because we only call this when we've verified content is a list and
313
+ # we're doing content block conversions
314
+ formatted_message.content[idx] = new_block # type: ignore[index, assignment]
315
+
316
+
317
+ def _update_message_content_to_blocks(message: T, output_version: str) -> T:
318
+ return message.model_copy(
319
+ update={
320
+ "content": message.content_blocks,
321
+ "response_metadata": {
322
+ **message.response_metadata,
323
+ "output_version": output_version,
324
+ },
325
+ }
326
+ )