langchain-core 1.0.0a1__py3-none-any.whl → 1.0.0a3__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 (131) hide show
  1. langchain_core/_api/beta_decorator.py +17 -40
  2. langchain_core/_api/deprecation.py +20 -7
  3. langchain_core/_api/path.py +19 -2
  4. langchain_core/_import_utils.py +7 -0
  5. langchain_core/agents.py +10 -6
  6. langchain_core/callbacks/base.py +28 -15
  7. langchain_core/callbacks/manager.py +81 -69
  8. langchain_core/callbacks/usage.py +4 -2
  9. langchain_core/chat_history.py +29 -21
  10. langchain_core/document_loaders/base.py +34 -9
  11. langchain_core/document_loaders/langsmith.py +3 -0
  12. langchain_core/documents/base.py +35 -10
  13. langchain_core/documents/transformers.py +4 -2
  14. langchain_core/embeddings/fake.py +8 -5
  15. langchain_core/env.py +2 -3
  16. langchain_core/example_selectors/base.py +12 -0
  17. langchain_core/exceptions.py +7 -0
  18. langchain_core/globals.py +17 -28
  19. langchain_core/indexing/api.py +57 -45
  20. langchain_core/indexing/base.py +5 -8
  21. langchain_core/indexing/in_memory.py +23 -3
  22. langchain_core/language_models/__init__.py +6 -2
  23. langchain_core/language_models/_utils.py +28 -4
  24. langchain_core/language_models/base.py +33 -21
  25. langchain_core/language_models/chat_models.py +103 -29
  26. langchain_core/language_models/fake_chat_models.py +5 -7
  27. langchain_core/language_models/llms.py +54 -20
  28. langchain_core/load/dump.py +2 -3
  29. langchain_core/load/load.py +15 -1
  30. langchain_core/load/serializable.py +38 -43
  31. langchain_core/memory.py +7 -3
  32. langchain_core/messages/__init__.py +7 -17
  33. langchain_core/messages/ai.py +41 -34
  34. langchain_core/messages/base.py +16 -7
  35. langchain_core/messages/block_translators/__init__.py +10 -8
  36. langchain_core/messages/block_translators/anthropic.py +3 -1
  37. langchain_core/messages/block_translators/bedrock.py +3 -1
  38. langchain_core/messages/block_translators/bedrock_converse.py +3 -1
  39. langchain_core/messages/block_translators/google_genai.py +3 -1
  40. langchain_core/messages/block_translators/google_vertexai.py +3 -1
  41. langchain_core/messages/block_translators/groq.py +3 -1
  42. langchain_core/messages/block_translators/langchain_v0.py +3 -136
  43. langchain_core/messages/block_translators/ollama.py +3 -1
  44. langchain_core/messages/block_translators/openai.py +252 -10
  45. langchain_core/messages/content.py +26 -124
  46. langchain_core/messages/human.py +2 -13
  47. langchain_core/messages/system.py +2 -6
  48. langchain_core/messages/tool.py +34 -14
  49. langchain_core/messages/utils.py +189 -74
  50. langchain_core/output_parsers/base.py +5 -2
  51. langchain_core/output_parsers/json.py +4 -4
  52. langchain_core/output_parsers/list.py +7 -22
  53. langchain_core/output_parsers/openai_functions.py +3 -0
  54. langchain_core/output_parsers/openai_tools.py +6 -1
  55. langchain_core/output_parsers/pydantic.py +4 -0
  56. langchain_core/output_parsers/string.py +5 -1
  57. langchain_core/output_parsers/xml.py +19 -19
  58. langchain_core/outputs/chat_generation.py +18 -7
  59. langchain_core/outputs/generation.py +14 -3
  60. langchain_core/outputs/llm_result.py +8 -1
  61. langchain_core/prompt_values.py +10 -4
  62. langchain_core/prompts/base.py +6 -11
  63. langchain_core/prompts/chat.py +88 -60
  64. langchain_core/prompts/dict.py +16 -8
  65. langchain_core/prompts/few_shot.py +9 -11
  66. langchain_core/prompts/few_shot_with_templates.py +5 -1
  67. langchain_core/prompts/image.py +12 -5
  68. langchain_core/prompts/loading.py +2 -2
  69. langchain_core/prompts/message.py +5 -6
  70. langchain_core/prompts/pipeline.py +13 -8
  71. langchain_core/prompts/prompt.py +22 -8
  72. langchain_core/prompts/string.py +18 -10
  73. langchain_core/prompts/structured.py +7 -2
  74. langchain_core/rate_limiters.py +2 -2
  75. langchain_core/retrievers.py +7 -6
  76. langchain_core/runnables/base.py +387 -246
  77. langchain_core/runnables/branch.py +11 -28
  78. langchain_core/runnables/config.py +20 -17
  79. langchain_core/runnables/configurable.py +34 -19
  80. langchain_core/runnables/fallbacks.py +20 -13
  81. langchain_core/runnables/graph.py +48 -38
  82. langchain_core/runnables/graph_ascii.py +40 -17
  83. langchain_core/runnables/graph_mermaid.py +54 -25
  84. langchain_core/runnables/graph_png.py +27 -31
  85. langchain_core/runnables/history.py +55 -58
  86. langchain_core/runnables/passthrough.py +44 -21
  87. langchain_core/runnables/retry.py +44 -23
  88. langchain_core/runnables/router.py +9 -8
  89. langchain_core/runnables/schema.py +9 -0
  90. langchain_core/runnables/utils.py +53 -90
  91. langchain_core/stores.py +19 -31
  92. langchain_core/sys_info.py +9 -8
  93. langchain_core/tools/base.py +36 -27
  94. langchain_core/tools/convert.py +25 -14
  95. langchain_core/tools/simple.py +36 -8
  96. langchain_core/tools/structured.py +25 -12
  97. langchain_core/tracers/base.py +2 -2
  98. langchain_core/tracers/context.py +5 -1
  99. langchain_core/tracers/core.py +110 -46
  100. langchain_core/tracers/evaluation.py +22 -26
  101. langchain_core/tracers/event_stream.py +97 -42
  102. langchain_core/tracers/langchain.py +12 -3
  103. langchain_core/tracers/langchain_v1.py +10 -2
  104. langchain_core/tracers/log_stream.py +56 -17
  105. langchain_core/tracers/root_listeners.py +4 -20
  106. langchain_core/tracers/run_collector.py +6 -16
  107. langchain_core/tracers/schemas.py +5 -1
  108. langchain_core/utils/aiter.py +14 -6
  109. langchain_core/utils/env.py +3 -0
  110. langchain_core/utils/function_calling.py +46 -20
  111. langchain_core/utils/interactive_env.py +6 -2
  112. langchain_core/utils/iter.py +12 -5
  113. langchain_core/utils/json.py +12 -3
  114. langchain_core/utils/json_schema.py +156 -40
  115. langchain_core/utils/loading.py +5 -1
  116. langchain_core/utils/mustache.py +25 -16
  117. langchain_core/utils/pydantic.py +38 -9
  118. langchain_core/utils/utils.py +25 -9
  119. langchain_core/vectorstores/base.py +7 -20
  120. langchain_core/vectorstores/in_memory.py +20 -14
  121. langchain_core/vectorstores/utils.py +18 -12
  122. langchain_core/version.py +1 -1
  123. langchain_core-1.0.0a3.dist-info/METADATA +77 -0
  124. langchain_core-1.0.0a3.dist-info/RECORD +181 -0
  125. langchain_core/beta/__init__.py +0 -1
  126. langchain_core/beta/runnables/__init__.py +0 -1
  127. langchain_core/beta/runnables/context.py +0 -448
  128. langchain_core-1.0.0a1.dist-info/METADATA +0 -106
  129. langchain_core-1.0.0a1.dist-info/RECORD +0 -184
  130. {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a3.dist-info}/WHEEL +0 -0
  131. {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a3.dist-info}/entry_points.txt +0 -0
@@ -150,7 +150,8 @@ class BaseMessage(Serializable):
150
150
  def get_lc_namespace(cls) -> list[str]:
151
151
  """Get the namespace of the langchain object.
152
152
 
153
- Default is ["langchain", "schema", "messages"].
153
+ Returns:
154
+ ``["langchain", "schema", "messages"]``
154
155
  """
155
156
  return ["langchain", "schema", "messages"]
156
157
 
@@ -179,14 +180,14 @@ class BaseMessage(Serializable):
179
180
  .. versionadded:: 1.0.0
180
181
 
181
182
  """ # noqa: E501
182
- from langchain_core.messages import content as types
183
- from langchain_core.messages.block_translators.anthropic import (
183
+ from langchain_core.messages import content as types # noqa: PLC0415
184
+ from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
184
185
  _convert_to_v1_from_anthropic_input,
185
186
  )
186
- from langchain_core.messages.block_translators.langchain_v0 import (
187
+ from langchain_core.messages.block_translators.langchain_v0 import ( # noqa: PLC0415
187
188
  _convert_v0_multimodal_input_to_v1,
188
189
  )
189
- from langchain_core.messages.block_translators.openai import (
190
+ from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
190
191
  _convert_to_v1_from_chat_completions_input,
191
192
  )
192
193
 
@@ -246,8 +247,16 @@ class BaseMessage(Serializable):
246
247
  return TextAccessor(text_value)
247
248
 
248
249
  def __add__(self, other: Any) -> ChatPromptTemplate:
249
- """Concatenate this message with another message."""
250
- from langchain_core.prompts.chat import ChatPromptTemplate
250
+ """Concatenate this message with another message.
251
+
252
+ Args:
253
+ other: Another message to concatenate with this one.
254
+
255
+ Returns:
256
+ A ChatPromptTemplate containing both messages.
257
+ """
258
+ # Import locally to prevent circular imports.
259
+ from langchain_core.prompts.chat import ChatPromptTemplate # noqa: PLC0415
251
260
 
252
261
  prompt = ChatPromptTemplate(messages=[self])
253
262
  return prompt + other
@@ -53,26 +53,28 @@ def _register_translators() -> None:
53
53
  For translators implemented outside langchain-core, they can be registered by
54
54
  calling ``register_translator`` from within the integration package.
55
55
  """
56
- from langchain_core.messages.block_translators.anthropic import (
56
+ from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
57
57
  _register_anthropic_translator,
58
58
  )
59
- from langchain_core.messages.block_translators.bedrock import (
59
+ from langchain_core.messages.block_translators.bedrock import ( # noqa: PLC0415
60
60
  _register_bedrock_translator,
61
61
  )
62
- from langchain_core.messages.block_translators.bedrock_converse import (
62
+ from langchain_core.messages.block_translators.bedrock_converse import ( # noqa: PLC0415
63
63
  _register_bedrock_converse_translator,
64
64
  )
65
- from langchain_core.messages.block_translators.google_genai import (
65
+ from langchain_core.messages.block_translators.google_genai import ( # noqa: PLC0415
66
66
  _register_google_genai_translator,
67
67
  )
68
- from langchain_core.messages.block_translators.google_vertexai import (
68
+ from langchain_core.messages.block_translators.google_vertexai import ( # noqa: PLC0415
69
69
  _register_google_vertexai_translator,
70
70
  )
71
- from langchain_core.messages.block_translators.groq import _register_groq_translator
72
- from langchain_core.messages.block_translators.ollama import (
71
+ from langchain_core.messages.block_translators.groq import ( # noqa: PLC0415
72
+ _register_groq_translator,
73
+ )
74
+ from langchain_core.messages.block_translators.ollama import ( # noqa: PLC0415
73
75
  _register_ollama_translator,
74
76
  )
75
- from langchain_core.messages.block_translators.openai import (
77
+ from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
76
78
  _register_openai_translator,
77
79
  )
78
80
 
@@ -443,7 +443,9 @@ def _register_anthropic_translator() -> None:
443
443
 
444
444
  Run automatically when the module is imported.
445
445
  """
446
- from langchain_core.messages.block_translators import register_translator
446
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
447
+ register_translator,
448
+ )
447
449
 
448
450
  register_translator("anthropic", translate_content, translate_content_chunk)
449
451
 
@@ -37,7 +37,9 @@ def _register_bedrock_translator() -> None:
37
37
 
38
38
  Run automatically when the module is imported.
39
39
  """
40
- from langchain_core.messages.block_translators import register_translator
40
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
41
+ register_translator,
42
+ )
41
43
 
42
44
  register_translator("bedrock", translate_content, translate_content_chunk)
43
45
 
@@ -39,7 +39,9 @@ def _register_bedrock_converse_translator() -> None:
39
39
 
40
40
  Run automatically when the module is imported.
41
41
  """
42
- from langchain_core.messages.block_translators import register_translator
42
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
43
+ register_translator,
44
+ )
43
45
 
44
46
  register_translator("bedrock_converse", translate_content, translate_content_chunk)
45
47
 
@@ -37,7 +37,9 @@ def _register_google_genai_translator() -> None:
37
37
 
38
38
  Run automatically when the module is imported.
39
39
  """
40
- from langchain_core.messages.block_translators import register_translator
40
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
41
+ register_translator,
42
+ )
41
43
 
42
44
  register_translator("google_genai", translate_content, translate_content_chunk)
43
45
 
@@ -39,7 +39,9 @@ def _register_google_vertexai_translator() -> None:
39
39
 
40
40
  Run automatically when the module is imported.
41
41
  """
42
- from langchain_core.messages.block_translators import register_translator
42
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
43
+ register_translator,
44
+ )
43
45
 
44
46
  register_translator("google_vertexai", translate_content, translate_content_chunk)
45
47
 
@@ -37,7 +37,9 @@ def _register_groq_translator() -> None:
37
37
 
38
38
  Run automatically when the module is imported.
39
39
  """
40
- from langchain_core.messages.block_translators import register_translator
40
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
41
+ register_translator,
42
+ )
41
43
 
42
44
  register_translator("groq", translate_content, translate_content_chunk)
43
45
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  from typing import Any, Union, cast
4
4
 
5
- from langchain_core.language_models._utils import _parse_data_uri
6
5
  from langchain_core.messages import content as types
7
6
 
8
7
 
@@ -11,14 +10,15 @@ def _convert_v0_multimodal_input_to_v1(
11
10
  ) -> list[types.ContentBlock]:
12
11
  """Convert v0 multimodal blocks to v1 format.
13
12
 
14
- Processes non_standard blocks that might be v0 format and converts them
15
- to proper v1 ContentBlocks.
13
+ Processes ``'non_standard'`` blocks that might be v0 format and converts them
14
+ to proper v1 ``ContentBlock``.
16
15
 
17
16
  Args:
18
17
  blocks: List of content blocks to process.
19
18
 
20
19
  Returns:
21
20
  Updated list with v0 blocks converted to v1 format.
21
+
22
22
  """
23
23
  converted_blocks = []
24
24
  unpacked_blocks: list[dict[str, Any]] = [
@@ -162,136 +162,3 @@ def _convert_legacy_v0_content_block_to_v1(
162
162
 
163
163
  # If we can't convert, return the block unchanged
164
164
  return block
165
-
166
-
167
- def _convert_openai_format_to_data_block(
168
- block: dict,
169
- ) -> Union[types.ContentBlock, dict[Any, Any]]:
170
- """Convert OpenAI image/audio/file content block to respective v1 multimodal block.
171
-
172
- We expect that the incoming block is verified to be in OpenAI Chat Completions
173
- format.
174
-
175
- If parsing fails, passes block through unchanged.
176
-
177
- Mappings (Chat Completions to LangChain v1):
178
- - Image -> `ImageContentBlock`
179
- - Audio -> `AudioContentBlock`
180
- - File -> `FileContentBlock`
181
-
182
- """
183
-
184
- # Extract extra keys to put them in `extras`
185
- def _extract_extras(block_dict: dict, known_keys: set[str]) -> dict[str, Any]:
186
- """Extract unknown keys from block to preserve as extras."""
187
- return {k: v for k, v in block_dict.items() if k not in known_keys}
188
-
189
- # base64-style image block
190
- if (block["type"] == "image_url") and (
191
- parsed := _parse_data_uri(block["image_url"]["url"])
192
- ):
193
- known_keys = {"type", "image_url"}
194
- extras = _extract_extras(block, known_keys)
195
-
196
- # Also extract extras from nested image_url dict
197
- image_url_known_keys = {"url"}
198
- image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
199
-
200
- # Merge extras
201
- all_extras = {**extras}
202
- for key, value in image_url_extras.items():
203
- if key == "detail": # Don't rename
204
- all_extras["detail"] = value
205
- else:
206
- all_extras[f"image_url_{key}"] = value
207
-
208
- return types.create_image_block(
209
- # Even though this is labeled as `url`, it can be base64-encoded
210
- base64=parsed["data"],
211
- mime_type=parsed["mime_type"],
212
- **all_extras,
213
- )
214
-
215
- # url-style image block
216
- if (block["type"] == "image_url") and isinstance(
217
- block["image_url"].get("url"), str
218
- ):
219
- known_keys = {"type", "image_url"}
220
- extras = _extract_extras(block, known_keys)
221
-
222
- image_url_known_keys = {"url"}
223
- image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
224
-
225
- all_extras = {**extras}
226
- for key, value in image_url_extras.items():
227
- if key == "detail": # Don't rename
228
- all_extras["detail"] = value
229
- else:
230
- all_extras[f"image_url_{key}"] = value
231
-
232
- return types.create_image_block(
233
- url=block["image_url"]["url"],
234
- **all_extras,
235
- )
236
-
237
- # base64-style audio block
238
- # audio is only represented via raw data, no url or ID option
239
- if block["type"] == "input_audio":
240
- known_keys = {"type", "input_audio"}
241
- extras = _extract_extras(block, known_keys)
242
-
243
- # Also extract extras from nested audio dict
244
- audio_known_keys = {"data", "format"}
245
- audio_extras = _extract_extras(block["input_audio"], audio_known_keys)
246
-
247
- all_extras = {**extras}
248
- for key, value in audio_extras.items():
249
- all_extras[f"audio_{key}"] = value
250
-
251
- return types.create_audio_block(
252
- base64=block["input_audio"]["data"],
253
- mime_type=f"audio/{block['input_audio']['format']}",
254
- **all_extras,
255
- )
256
-
257
- # id-style file block
258
- if block.get("type") == "file" and "file_id" in block.get("file", {}):
259
- known_keys = {"type", "file"}
260
- extras = _extract_extras(block, known_keys)
261
-
262
- file_known_keys = {"file_id"}
263
- file_extras = _extract_extras(block["file"], file_known_keys)
264
-
265
- all_extras = {**extras}
266
- for key, value in file_extras.items():
267
- all_extras[f"file_{key}"] = value
268
-
269
- return types.create_file_block(
270
- file_id=block["file"]["file_id"],
271
- **all_extras,
272
- )
273
-
274
- # base64-style file block
275
- if (block["type"] == "file") and (
276
- parsed := _parse_data_uri(block["file"]["file_data"])
277
- ):
278
- known_keys = {"type", "file"}
279
- extras = _extract_extras(block, known_keys)
280
-
281
- file_known_keys = {"file_data", "filename"}
282
- file_extras = _extract_extras(block["file"], file_known_keys)
283
-
284
- all_extras = {**extras}
285
- for key, value in file_extras.items():
286
- all_extras[f"file_{key}"] = value
287
-
288
- filename = block["file"].get("filename")
289
- return types.create_file_block(
290
- base64=parsed["data"],
291
- mime_type="application/pdf",
292
- filename=filename,
293
- **all_extras,
294
- )
295
-
296
- # Escape hatch
297
- return block
@@ -37,7 +37,9 @@ def _register_ollama_translator() -> None:
37
37
 
38
38
  Run automatically when the module is imported.
39
39
  """
40
- from langchain_core.messages.block_translators import register_translator
40
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
41
+ register_translator,
42
+ )
41
43
 
42
44
  register_translator("ollama", translate_content, translate_content_chunk)
43
45
 
@@ -3,21 +3,128 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import warnings
6
7
  from collections.abc import Iterable
7
- from typing import TYPE_CHECKING, Any, Optional, Union, cast
8
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
8
9
 
9
10
  from langchain_core.language_models._utils import (
10
- _is_openai_data_block,
11
+ _parse_data_uri,
12
+ is_openai_data_block,
11
13
  )
12
14
  from langchain_core.messages import content as types
13
- from langchain_core.messages.block_translators.langchain_v0 import (
14
- _convert_openai_format_to_data_block,
15
- )
16
15
 
17
16
  if TYPE_CHECKING:
18
17
  from langchain_core.messages import AIMessage, AIMessageChunk
19
18
 
20
19
 
20
+ def convert_to_openai_image_block(block: dict[str, Any]) -> dict:
21
+ """Convert ``ImageContentBlock`` to format expected by OpenAI Chat Completions."""
22
+ if "url" in block:
23
+ return {
24
+ "type": "image_url",
25
+ "image_url": {
26
+ "url": block["url"],
27
+ },
28
+ }
29
+ if "base64" in block or block.get("source_type") == "base64":
30
+ if "mime_type" not in block:
31
+ error_message = "mime_type key is required for base64 data."
32
+ raise ValueError(error_message)
33
+ mime_type = block["mime_type"]
34
+ base64_data = block["data"] if "data" in block else block["base64"]
35
+ return {
36
+ "type": "image_url",
37
+ "image_url": {
38
+ "url": f"data:{mime_type};base64,{base64_data}",
39
+ },
40
+ }
41
+ error_message = "Unsupported source type. Only 'url' and 'base64' are supported."
42
+ raise ValueError(error_message)
43
+
44
+
45
+ def convert_to_openai_data_block(
46
+ block: dict, api: Literal["chat/completions", "responses"] = "chat/completions"
47
+ ) -> dict:
48
+ """Format standard data content block to format expected by OpenAI.
49
+
50
+ "Standard data content block" can include old-style LangChain v0 blocks
51
+ (URLContentBlock, Base64ContentBlock, IDContentBlock) or new ones.
52
+ """
53
+ if block["type"] == "image":
54
+ chat_completions_block = convert_to_openai_image_block(block)
55
+ if api == "responses":
56
+ formatted_block = {
57
+ "type": "input_image",
58
+ "image_url": chat_completions_block["image_url"]["url"],
59
+ }
60
+ if chat_completions_block["image_url"].get("detail"):
61
+ formatted_block["detail"] = chat_completions_block["image_url"][
62
+ "detail"
63
+ ]
64
+ else:
65
+ formatted_block = chat_completions_block
66
+
67
+ elif block["type"] == "file":
68
+ if block.get("source_type") == "base64" or "base64" in block:
69
+ # Handle v0 format (Base64CB): {"source_type": "base64", "data": "...", ...}
70
+ # Handle v1 format (IDCB): {"base64": "...", ...}
71
+ base64_data = block["data"] if "source_type" in block else block["base64"]
72
+ file = {"file_data": f"data:{block['mime_type']};base64,{base64_data}"}
73
+ if filename := block.get("filename"):
74
+ file["filename"] = filename
75
+ elif (extras := block.get("extras")) and ("filename" in extras):
76
+ file["filename"] = extras["filename"]
77
+ elif (extras := block.get("metadata")) and ("filename" in extras):
78
+ # Backward compat
79
+ file["filename"] = extras["filename"]
80
+ else:
81
+ # Can't infer filename
82
+ warnings.warn(
83
+ "OpenAI may require a filename for file uploads. Specify a filename"
84
+ " in the content block, e.g.: {'type': 'file', 'mime_type': "
85
+ "'...', 'base64': '...', 'filename': 'my-file.pdf'}",
86
+ stacklevel=1,
87
+ )
88
+ formatted_block = {"type": "file", "file": file}
89
+ if api == "responses":
90
+ formatted_block = {"type": "input_file", **formatted_block["file"]}
91
+ elif block.get("source_type") == "id" or "file_id" in block:
92
+ # Handle v0 format (IDContentBlock): {"source_type": "id", "id": "...", ...}
93
+ # Handle v1 format (IDCB): {"file_id": "...", ...}
94
+ file_id = block["id"] if "source_type" in block else block["file_id"]
95
+ formatted_block = {"type": "file", "file": {"file_id": file_id}}
96
+ if api == "responses":
97
+ formatted_block = {"type": "input_file", **formatted_block["file"]}
98
+ elif "url" in block: # Intentionally do not check for source_type="url"
99
+ if api == "chat/completions":
100
+ error_msg = "OpenAI Chat Completions does not support file URLs."
101
+ raise ValueError(error_msg)
102
+ # Only supported by Responses API; return in that format
103
+ formatted_block = {"type": "input_file", "file_url": block["url"]}
104
+ else:
105
+ error_msg = "Keys base64, url, or file_id required for file blocks."
106
+ raise ValueError(error_msg)
107
+
108
+ elif block["type"] == "audio":
109
+ if "base64" in block or block.get("source_type") == "base64":
110
+ # Handle v0 format: {"source_type": "base64", "data": "...", ...}
111
+ # Handle v1 format: {"base64": "...", ...}
112
+ base64_data = block["data"] if "source_type" in block else block["base64"]
113
+ audio_format = block["mime_type"].split("/")[-1]
114
+ formatted_block = {
115
+ "type": "input_audio",
116
+ "input_audio": {"data": base64_data, "format": audio_format},
117
+ }
118
+ else:
119
+ error_msg = "Key base64 is required for audio blocks."
120
+ raise ValueError(error_msg)
121
+ else:
122
+ error_msg = f"Block of type {block['type']} is not supported."
123
+ raise ValueError(error_msg)
124
+
125
+ return formatted_block
126
+
127
+
21
128
  # v1 / Chat Completions
22
129
  def _convert_to_v1_from_chat_completions(
23
130
  message: AIMessage,
@@ -57,7 +164,7 @@ def _convert_to_v1_from_chat_completions_input(
57
164
  Returns:
58
165
  Updated list with OpenAI blocks converted to v1 format.
59
166
  """
60
- from langchain_core.messages import content as types
167
+ from langchain_core.messages import content as types # noqa: PLC0415
61
168
 
62
169
  converted_blocks = []
63
170
  unpacked_blocks: list[dict[str, Any]] = [
@@ -71,7 +178,7 @@ def _convert_to_v1_from_chat_completions_input(
71
178
  "image_url",
72
179
  "input_audio",
73
180
  "file",
74
- } and _is_openai_data_block(block):
181
+ } and is_openai_data_block(block):
75
182
  converted_block = _convert_openai_format_to_data_block(block)
76
183
  # If conversion succeeded, use it; otherwise keep as non_standard
77
184
  if (
@@ -153,7 +260,7 @@ _FUNCTION_CALL_IDS_MAP_KEY = "__openai_function_call_ids__"
153
260
 
154
261
  def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
155
262
  """Convert v0 AIMessage into ``output_version="responses/v1"`` format."""
156
- from langchain_core.messages import AIMessageChunk
263
+ from langchain_core.messages import AIMessageChunk # noqa: PLC0415
157
264
 
158
265
  # Only update ChatOpenAI v0.3 AIMessages
159
266
  is_chatopenai_v03 = (
@@ -288,6 +395,139 @@ def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
288
395
  )
289
396
 
290
397
 
398
+ def _convert_openai_format_to_data_block(
399
+ block: dict,
400
+ ) -> Union[types.ContentBlock, dict[Any, Any]]:
401
+ """Convert OpenAI image/audio/file content block to respective v1 multimodal block.
402
+
403
+ We expect that the incoming block is verified to be in OpenAI Chat Completions
404
+ format.
405
+
406
+ If parsing fails, passes block through unchanged.
407
+
408
+ Mappings (Chat Completions to LangChain v1):
409
+ - Image -> `ImageContentBlock`
410
+ - Audio -> `AudioContentBlock`
411
+ - File -> `FileContentBlock`
412
+
413
+ """
414
+
415
+ # Extract extra keys to put them in `extras`
416
+ def _extract_extras(block_dict: dict, known_keys: set[str]) -> dict[str, Any]:
417
+ """Extract unknown keys from block to preserve as extras."""
418
+ return {k: v for k, v in block_dict.items() if k not in known_keys}
419
+
420
+ # base64-style image block
421
+ if (block["type"] == "image_url") and (
422
+ parsed := _parse_data_uri(block["image_url"]["url"])
423
+ ):
424
+ known_keys = {"type", "image_url"}
425
+ extras = _extract_extras(block, known_keys)
426
+
427
+ # Also extract extras from nested image_url dict
428
+ image_url_known_keys = {"url"}
429
+ image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
430
+
431
+ # Merge extras
432
+ all_extras = {**extras}
433
+ for key, value in image_url_extras.items():
434
+ if key == "detail": # Don't rename
435
+ all_extras["detail"] = value
436
+ else:
437
+ all_extras[f"image_url_{key}"] = value
438
+
439
+ return types.create_image_block(
440
+ # Even though this is labeled as `url`, it can be base64-encoded
441
+ base64=parsed["data"],
442
+ mime_type=parsed["mime_type"],
443
+ **all_extras,
444
+ )
445
+
446
+ # url-style image block
447
+ if (block["type"] == "image_url") and isinstance(
448
+ block["image_url"].get("url"), str
449
+ ):
450
+ known_keys = {"type", "image_url"}
451
+ extras = _extract_extras(block, known_keys)
452
+
453
+ image_url_known_keys = {"url"}
454
+ image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
455
+
456
+ all_extras = {**extras}
457
+ for key, value in image_url_extras.items():
458
+ if key == "detail": # Don't rename
459
+ all_extras["detail"] = value
460
+ else:
461
+ all_extras[f"image_url_{key}"] = value
462
+
463
+ return types.create_image_block(
464
+ url=block["image_url"]["url"],
465
+ **all_extras,
466
+ )
467
+
468
+ # base64-style audio block
469
+ # audio is only represented via raw data, no url or ID option
470
+ if block["type"] == "input_audio":
471
+ known_keys = {"type", "input_audio"}
472
+ extras = _extract_extras(block, known_keys)
473
+
474
+ # Also extract extras from nested audio dict
475
+ audio_known_keys = {"data", "format"}
476
+ audio_extras = _extract_extras(block["input_audio"], audio_known_keys)
477
+
478
+ all_extras = {**extras}
479
+ for key, value in audio_extras.items():
480
+ all_extras[f"audio_{key}"] = value
481
+
482
+ return types.create_audio_block(
483
+ base64=block["input_audio"]["data"],
484
+ mime_type=f"audio/{block['input_audio']['format']}",
485
+ **all_extras,
486
+ )
487
+
488
+ # id-style file block
489
+ if block.get("type") == "file" and "file_id" in block.get("file", {}):
490
+ known_keys = {"type", "file"}
491
+ extras = _extract_extras(block, known_keys)
492
+
493
+ file_known_keys = {"file_id"}
494
+ file_extras = _extract_extras(block["file"], file_known_keys)
495
+
496
+ all_extras = {**extras}
497
+ for key, value in file_extras.items():
498
+ all_extras[f"file_{key}"] = value
499
+
500
+ return types.create_file_block(
501
+ file_id=block["file"]["file_id"],
502
+ **all_extras,
503
+ )
504
+
505
+ # base64-style file block
506
+ if (block["type"] == "file") and (
507
+ parsed := _parse_data_uri(block["file"]["file_data"])
508
+ ):
509
+ known_keys = {"type", "file"}
510
+ extras = _extract_extras(block, known_keys)
511
+
512
+ file_known_keys = {"file_data", "filename"}
513
+ file_extras = _extract_extras(block["file"], file_known_keys)
514
+
515
+ all_extras = {**extras}
516
+ for key, value in file_extras.items():
517
+ all_extras[f"file_{key}"] = value
518
+
519
+ filename = block["file"].get("filename")
520
+ return types.create_file_block(
521
+ base64=parsed["data"],
522
+ mime_type="application/pdf",
523
+ filename=filename,
524
+ **all_extras,
525
+ )
526
+
527
+ # Escape hatch
528
+ return block
529
+
530
+
291
531
  # v1 / Responses
292
532
  def _convert_annotation_to_v1(annotation: dict[str, Any]) -> types.Annotation:
293
533
  annotation_type = annotation.get("type")
@@ -438,7 +678,7 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
438
678
  ] = None
439
679
  call_id = block.get("call_id", "")
440
680
 
441
- from langchain_core.messages import AIMessageChunk
681
+ from langchain_core.messages import AIMessageChunk # noqa: PLC0415
442
682
 
443
683
  if (
444
684
  isinstance(message, AIMessageChunk)
@@ -578,7 +818,9 @@ def _register_openai_translator() -> None:
578
818
 
579
819
  Run automatically when the module is imported.
580
820
  """
581
- from langchain_core.messages.block_translators import register_translator
821
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
822
+ register_translator,
823
+ )
582
824
 
583
825
  register_translator("openai", translate_content, translate_content_chunk)
584
826