coco-framework 0.1.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.
Files changed (400) hide show
  1. coco/__init__.py +3 -0
  2. coco/agents/__init__.py +9 -0
  3. coco/agents/_subagent_transformer.py +307 -0
  4. coco/agents/factory.py +2164 -0
  5. coco/agents/middleware/__init__.py +100 -0
  6. coco/agents/middleware/_execution.py +385 -0
  7. coco/agents/middleware/_redaction.py +454 -0
  8. coco/agents/middleware/_retry.py +134 -0
  9. coco/agents/middleware/_trace_policy.py +61 -0
  10. coco/agents/middleware/context_editing.py +306 -0
  11. coco/agents/middleware/file_search.py +433 -0
  12. coco/agents/middleware/human_in_the_loop.py +680 -0
  13. coco/agents/middleware/internal_call_transformer.py +108 -0
  14. coco/agents/middleware/model_call_limit.py +267 -0
  15. coco/agents/middleware/model_fallback.py +417 -0
  16. coco/agents/middleware/model_retry.py +327 -0
  17. coco/agents/middleware/pii.py +878 -0
  18. coco/agents/middleware/provider_tool_search.py +307 -0
  19. coco/agents/middleware/shell_tool.py +949 -0
  20. coco/agents/middleware/summarization.py +910 -0
  21. coco/agents/middleware/todo.py +357 -0
  22. coco/agents/middleware/tool_call_limit.py +495 -0
  23. coco/agents/middleware/tool_emulator.py +251 -0
  24. coco/agents/middleware/tool_error.py +209 -0
  25. coco/agents/middleware/tool_retry.py +421 -0
  26. coco/agents/middleware/tool_selection.py +515 -0
  27. coco/agents/middleware/types.py +2201 -0
  28. coco/agents/structured_output.py +472 -0
  29. coco/chat_models/__init__.py +7 -0
  30. coco/chat_models/base.py +1082 -0
  31. coco/embeddings/__init__.py +18 -0
  32. coco/embeddings/base.py +279 -0
  33. coco/mcp/__init__.py +36 -0
  34. coco/mcp/adapter.py +245 -0
  35. coco/mcp/elicitation.py +394 -0
  36. coco/mcp/tools.py +307 -0
  37. coco/messages/__init__.py +73 -0
  38. coco/py.typed +0 -0
  39. coco/rate_limiters/__init__.py +13 -0
  40. coco/tools/__init__.py +22 -0
  41. coco/tools/tool_node.py +20 -0
  42. coco_anthropic/__init__.py +15 -0
  43. coco_anthropic/_client_utils.py +81 -0
  44. coco_anthropic/_compat.py +228 -0
  45. coco_anthropic/_sdk_compat.py +124 -0
  46. coco_anthropic/_version.py +3 -0
  47. coco_anthropic/chat_models.py +3349 -0
  48. coco_anthropic/data/__init__.py +1 -0
  49. coco_anthropic/data/_profiles.py +491 -0
  50. coco_anthropic/data/profile_augmentations.toml +69 -0
  51. coco_anthropic/experimental.py +140 -0
  52. coco_anthropic/llms.py +428 -0
  53. coco_anthropic/middleware/__init__.py +25 -0
  54. coco_anthropic/middleware/anthropic_tools.py +1197 -0
  55. coco_anthropic/middleware/bash.py +84 -0
  56. coco_anthropic/middleware/file_search.py +360 -0
  57. coco_anthropic/middleware/prompt_caching.py +276 -0
  58. coco_anthropic/output_parsers.py +93 -0
  59. coco_anthropic/py.typed +0 -0
  60. coco_core/__init__.py +20 -0
  61. coco_core/_api/__init__.py +87 -0
  62. coco_core/_api/beta_decorator.py +266 -0
  63. coco_core/_api/deprecation.py +637 -0
  64. coco_core/_api/internal.py +23 -0
  65. coco_core/_api/path.py +50 -0
  66. coco_core/_import_utils.py +41 -0
  67. coco_core/_protocol.py +625 -0
  68. coco_core/_security/__init__.py +36 -0
  69. coco_core/_security/_exceptions.py +9 -0
  70. coco_core/_security/_policy.py +313 -0
  71. coco_core/_security/_ssrf_protection.py +155 -0
  72. coco_core/_security/_transport.py +254 -0
  73. coco_core/agents.py +258 -0
  74. coco_core/caches.py +272 -0
  75. coco_core/callbacks/__init__.py +132 -0
  76. coco_core/callbacks/base.py +1229 -0
  77. coco_core/callbacks/file.py +266 -0
  78. coco_core/callbacks/manager.py +2826 -0
  79. coco_core/callbacks/stdout.py +123 -0
  80. coco_core/callbacks/streaming_stdout.py +154 -0
  81. coco_core/callbacks/usage.py +124 -0
  82. coco_core/chat_history.py +267 -0
  83. coco_core/chat_loaders.py +26 -0
  84. coco_core/chat_sessions.py +19 -0
  85. coco_core/cross_encoders.py +18 -0
  86. coco_core/document_loaders/__init__.py +36 -0
  87. coco_core/document_loaders/base.py +155 -0
  88. coco_core/document_loaders/blob_loaders.py +38 -0
  89. coco_core/documents/__init__.py +55 -0
  90. coco_core/documents/base.py +347 -0
  91. coco_core/documents/compressor.py +74 -0
  92. coco_core/documents/transformers.py +79 -0
  93. coco_core/embeddings/__init__.py +31 -0
  94. coco_core/embeddings/embeddings.py +78 -0
  95. coco_core/embeddings/fake.py +129 -0
  96. coco_core/env.py +22 -0
  97. coco_core/example_selectors/__init__.py +47 -0
  98. coco_core/example_selectors/base.py +60 -0
  99. coco_core/example_selectors/length_based.py +131 -0
  100. coco_core/example_selectors/semantic_similarity.py +366 -0
  101. coco_core/exceptions.py +166 -0
  102. coco_core/globals.py +72 -0
  103. coco_core/indexing/__init__.py +53 -0
  104. coco_core/indexing/api.py +954 -0
  105. coco_core/indexing/base.py +661 -0
  106. coco_core/indexing/in_memory.py +104 -0
  107. coco_core/language_models/__init__.py +116 -0
  108. coco_core/language_models/_compat_bridge.py +844 -0
  109. coco_core/language_models/_utils.py +343 -0
  110. coco_core/language_models/base.py +500 -0
  111. coco_core/language_models/chat_model_stream.py +1525 -0
  112. coco_core/language_models/chat_models.py +2748 -0
  113. coco_core/language_models/fake.py +137 -0
  114. coco_core/language_models/fake_chat_models.py +396 -0
  115. coco_core/language_models/llms.py +1569 -0
  116. coco_core/language_models/model_profile.py +183 -0
  117. coco_core/load/__init__.py +44 -0
  118. coco_core/load/_validation.py +191 -0
  119. coco_core/load/dump.py +120 -0
  120. coco_core/load/load.py +828 -0
  121. coco_core/load/mapping.py +1097 -0
  122. coco_core/load/serializable.py +406 -0
  123. coco_core/messages/__init__.py +198 -0
  124. coco_core/messages/ai.py +854 -0
  125. coco_core/messages/base.py +518 -0
  126. coco_core/messages/block_translators/__init__.py +112 -0
  127. coco_core/messages/block_translators/anthropic.py +534 -0
  128. coco_core/messages/block_translators/bedrock.py +108 -0
  129. coco_core/messages/block_translators/bedrock_converse.py +329 -0
  130. coco_core/messages/block_translators/coco_v0.py +303 -0
  131. coco_core/messages/block_translators/google_genai.py +610 -0
  132. coco_core/messages/block_translators/google_vertexai.py +21 -0
  133. coco_core/messages/block_translators/groq.py +157 -0
  134. coco_core/messages/block_translators/openai.py +1089 -0
  135. coco_core/messages/chat.py +64 -0
  136. coco_core/messages/content.py +1488 -0
  137. coco_core/messages/function.py +62 -0
  138. coco_core/messages/human.py +70 -0
  139. coco_core/messages/modifier.py +33 -0
  140. coco_core/messages/system.py +70 -0
  141. coco_core/messages/tool.py +418 -0
  142. coco_core/messages/utils.py +2442 -0
  143. coco_core/output_parsers/__init__.py +101 -0
  144. coco_core/output_parsers/base.py +361 -0
  145. coco_core/output_parsers/format_instructions.py +16 -0
  146. coco_core/output_parsers/json.py +139 -0
  147. coco_core/output_parsers/list.py +249 -0
  148. coco_core/output_parsers/openai_functions.py +324 -0
  149. coco_core/output_parsers/openai_tools.py +383 -0
  150. coco_core/output_parsers/pydantic.py +141 -0
  151. coco_core/output_parsers/string.py +63 -0
  152. coco_core/output_parsers/transform.py +175 -0
  153. coco_core/output_parsers/xml.py +301 -0
  154. coco_core/outputs/__init__.py +66 -0
  155. coco_core/outputs/chat_generation.py +147 -0
  156. coco_core/outputs/chat_result.py +39 -0
  157. coco_core/outputs/generation.py +80 -0
  158. coco_core/outputs/llm_result.py +112 -0
  159. coco_core/outputs/run_info.py +22 -0
  160. coco_core/prompt_values.py +161 -0
  161. coco_core/prompts/__init__.py +101 -0
  162. coco_core/prompts/base.py +508 -0
  163. coco_core/prompts/chat.py +1495 -0
  164. coco_core/prompts/dict.py +177 -0
  165. coco_core/prompts/few_shot.py +483 -0
  166. coco_core/prompts/few_shot_with_templates.py +239 -0
  167. coco_core/prompts/image.py +177 -0
  168. coco_core/prompts/loading.py +296 -0
  169. coco_core/prompts/message.py +97 -0
  170. coco_core/prompts/prompt.py +312 -0
  171. coco_core/prompts/string.py +399 -0
  172. coco_core/prompts/structured.py +214 -0
  173. coco_core/py.typed +0 -0
  174. coco_core/rate_limiters.py +254 -0
  175. coco_core/retrievers.py +328 -0
  176. coco_core/runnables/__init__.py +136 -0
  177. coco_core/runnables/base.py +6725 -0
  178. coco_core/runnables/branch.py +457 -0
  179. coco_core/runnables/config.py +712 -0
  180. coco_core/runnables/configurable.py +710 -0
  181. coco_core/runnables/fallbacks.py +663 -0
  182. coco_core/runnables/graph.py +741 -0
  183. coco_core/runnables/graph_ascii.py +366 -0
  184. coco_core/runnables/graph_mermaid.py +503 -0
  185. coco_core/runnables/graph_png.py +215 -0
  186. coco_core/runnables/history.py +631 -0
  187. coco_core/runnables/passthrough.py +849 -0
  188. coco_core/runnables/retry.py +379 -0
  189. coco_core/runnables/router.py +239 -0
  190. coco_core/runnables/schema.py +188 -0
  191. coco_core/runnables/utils.py +798 -0
  192. coco_core/stores.py +291 -0
  193. coco_core/structured_query.py +203 -0
  194. coco_core/sys_info.py +154 -0
  195. coco_core/tools/__init__.py +95 -0
  196. coco_core/tools/base.py +1948 -0
  197. coco_core/tools/convert.py +497 -0
  198. coco_core/tools/render.py +67 -0
  199. coco_core/tools/retriever.py +94 -0
  200. coco_core/tools/simple.py +208 -0
  201. coco_core/tools/structured.py +322 -0
  202. coco_core/tracers/__init__.py +47 -0
  203. coco_core/tracers/_compat.py +95 -0
  204. coco_core/tracers/_context.py +337 -0
  205. coco_core/tracers/_streaming.py +53 -0
  206. coco_core/tracers/base.py +955 -0
  207. coco_core/tracers/coco.py +279 -0
  208. coco_core/tracers/context.py +205 -0
  209. coco_core/tracers/core.py +766 -0
  210. coco_core/tracers/event_stream.py +1101 -0
  211. coco_core/tracers/log_stream.py +765 -0
  212. coco_core/tracers/memory_stream.py +200 -0
  213. coco_core/tracers/root_listeners.py +130 -0
  214. coco_core/tracers/run_collector.py +40 -0
  215. coco_core/tracers/schemas.py +14 -0
  216. coco_core/tracers/stdout.py +205 -0
  217. coco_core/utils/__init__.py +111 -0
  218. coco_core/utils/_gateway.py +274 -0
  219. coco_core/utils/_merge.py +218 -0
  220. coco_core/utils/aiter.py +357 -0
  221. coco_core/utils/env.py +86 -0
  222. coco_core/utils/formatting.py +80 -0
  223. coco_core/utils/function_calling.py +869 -0
  224. coco_core/utils/html.py +132 -0
  225. coco_core/utils/image.py +15 -0
  226. coco_core/utils/input.py +82 -0
  227. coco_core/utils/interactive_env.py +12 -0
  228. coco_core/utils/iter.py +229 -0
  229. coco_core/utils/json.py +230 -0
  230. coco_core/utils/json_schema.py +273 -0
  231. coco_core/utils/mustache.py +706 -0
  232. coco_core/utils/pydantic.py +631 -0
  233. coco_core/utils/strings.py +69 -0
  234. coco_core/utils/usage.py +61 -0
  235. coco_core/utils/utils.py +528 -0
  236. coco_core/utils/uuid.py +57 -0
  237. coco_core/vectorstores/__init__.py +53 -0
  238. coco_core/vectorstores/base.py +1111 -0
  239. coco_core/vectorstores/in_memory.py +546 -0
  240. coco_core/vectorstores/utils.py +163 -0
  241. coco_core/version.py +3 -0
  242. coco_framework-0.1.0.dist-info/METADATA +184 -0
  243. coco_framework-0.1.0.dist-info/RECORD +400 -0
  244. coco_framework-0.1.0.dist-info/WHEEL +4 -0
  245. coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
  246. coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
  247. coco_graph/_internal/__init__.py +4 -0
  248. coco_graph/_internal/_cache.py +31 -0
  249. coco_graph/_internal/_config.py +474 -0
  250. coco_graph/_internal/_constants.py +140 -0
  251. coco_graph/_internal/_fields.py +213 -0
  252. coco_graph/_internal/_future.py +220 -0
  253. coco_graph/_internal/_pydantic.py +275 -0
  254. coco_graph/_internal/_queue.py +124 -0
  255. coco_graph/_internal/_replay.py +90 -0
  256. coco_graph/_internal/_retry.py +29 -0
  257. coco_graph/_internal/_runnable.py +969 -0
  258. coco_graph/_internal/_scratchpad.py +19 -0
  259. coco_graph/_internal/_serde.py +253 -0
  260. coco_graph/_internal/_timeout.py +25 -0
  261. coco_graph/_internal/_typing.py +54 -0
  262. coco_graph/cache/base/__init__.py +48 -0
  263. coco_graph/cache/base/py.typed +0 -0
  264. coco_graph/cache/memory/__init__.py +73 -0
  265. coco_graph/cache/redis/__init__.py +144 -0
  266. coco_graph/cache/sqlite/__init__.py +120 -0
  267. coco_graph/callbacks.py +394 -0
  268. coco_graph/channels/__init__.py +29 -0
  269. coco_graph/channels/any_value.py +72 -0
  270. coco_graph/channels/base.py +121 -0
  271. coco_graph/channels/binop.py +155 -0
  272. coco_graph/channels/delta.py +202 -0
  273. coco_graph/channels/ephemeral_value.py +79 -0
  274. coco_graph/channels/last_value.py +151 -0
  275. coco_graph/channels/named_barrier_value.py +167 -0
  276. coco_graph/channels/topic.py +94 -0
  277. coco_graph/channels/untracked_value.py +73 -0
  278. coco_graph/checkpoint/base/__init__.py +861 -0
  279. coco_graph/checkpoint/base/id.py +109 -0
  280. coco_graph/checkpoint/base/py.typed +0 -0
  281. coco_graph/checkpoint/memory/__init__.py +698 -0
  282. coco_graph/checkpoint/memory/py.typed +0 -0
  283. coco_graph/checkpoint/postgres/__init__.py +602 -0
  284. coco_graph/checkpoint/postgres/_ainternal.py +23 -0
  285. coco_graph/checkpoint/postgres/_internal.py +21 -0
  286. coco_graph/checkpoint/postgres/aio.py +691 -0
  287. coco_graph/checkpoint/postgres/base.py +666 -0
  288. coco_graph/checkpoint/postgres/py.typed +0 -0
  289. coco_graph/checkpoint/postgres/shallow.py +967 -0
  290. coco_graph/checkpoint/serde/__init__.py +0 -0
  291. coco_graph/checkpoint/serde/_msgpack.py +97 -0
  292. coco_graph/checkpoint/serde/base.py +64 -0
  293. coco_graph/checkpoint/serde/encrypted.py +80 -0
  294. coco_graph/checkpoint/serde/event_hooks.py +52 -0
  295. coco_graph/checkpoint/serde/jsonplus.py +883 -0
  296. coco_graph/checkpoint/serde/py.typed +0 -0
  297. coco_graph/checkpoint/serde/types.py +68 -0
  298. coco_graph/checkpoint/sqlite/__init__.py +645 -0
  299. coco_graph/checkpoint/sqlite/_delta.py +172 -0
  300. coco_graph/checkpoint/sqlite/aio.py +738 -0
  301. coco_graph/checkpoint/sqlite/py.typed +0 -0
  302. coco_graph/checkpoint/sqlite/utils.py +116 -0
  303. coco_graph/config.py +196 -0
  304. coco_graph/constants.py +64 -0
  305. coco_graph/errors.py +241 -0
  306. coco_graph/func/__init__.py +620 -0
  307. coco_graph/graph/__init__.py +12 -0
  308. coco_graph/graph/_branch.py +225 -0
  309. coco_graph/graph/_node.py +103 -0
  310. coco_graph/graph/message.py +437 -0
  311. coco_graph/graph/state.py +1978 -0
  312. coco_graph/graph/ui.py +227 -0
  313. coco_graph/managed/__init__.py +3 -0
  314. coco_graph/managed/base.py +31 -0
  315. coco_graph/managed/is_last_step.py +24 -0
  316. coco_graph/prebuilt/__init__.py +23 -0
  317. coco_graph/prebuilt/_tool_call_stream.py +117 -0
  318. coco_graph/prebuilt/_tool_call_transformer.py +165 -0
  319. coco_graph/prebuilt/chat_agent_executor.py +1015 -0
  320. coco_graph/prebuilt/interrupt.py +105 -0
  321. coco_graph/prebuilt/py.typed +0 -0
  322. coco_graph/prebuilt/tool_node.py +2030 -0
  323. coco_graph/prebuilt/tool_validator.py +221 -0
  324. coco_graph/pregel/__init__.py +3 -0
  325. coco_graph/pregel/_algo.py +1460 -0
  326. coco_graph/pregel/_call.py +298 -0
  327. coco_graph/pregel/_checkpoint.py +331 -0
  328. coco_graph/pregel/_config.py +0 -0
  329. coco_graph/pregel/_draw.py +294 -0
  330. coco_graph/pregel/_executor.py +223 -0
  331. coco_graph/pregel/_io.py +174 -0
  332. coco_graph/pregel/_log.py +3 -0
  333. coco_graph/pregel/_loop.py +1988 -0
  334. coco_graph/pregel/_messages.py +461 -0
  335. coco_graph/pregel/_read.py +312 -0
  336. coco_graph/pregel/_retry.py +854 -0
  337. coco_graph/pregel/_runner.py +941 -0
  338. coco_graph/pregel/_tools.py +268 -0
  339. coco_graph/pregel/_utils.py +221 -0
  340. coco_graph/pregel/_validate.py +120 -0
  341. coco_graph/pregel/_write.py +192 -0
  342. coco_graph/pregel/debug.py +302 -0
  343. coco_graph/pregel/main.py +4364 -0
  344. coco_graph/pregel/protocol.py +288 -0
  345. coco_graph/pregel/types.py +38 -0
  346. coco_graph/py.typed +0 -0
  347. coco_graph/runtime.py +350 -0
  348. coco_graph/store/base/__init__.py +1322 -0
  349. coco_graph/store/base/batch.py +371 -0
  350. coco_graph/store/base/embed.py +436 -0
  351. coco_graph/store/base/py.typed +0 -0
  352. coco_graph/store/memory/__init__.py +592 -0
  353. coco_graph/store/memory/py.typed +0 -0
  354. coco_graph/store/postgres/__init__.py +4 -0
  355. coco_graph/store/postgres/aio.py +592 -0
  356. coco_graph/store/postgres/base.py +1480 -0
  357. coco_graph/store/postgres/py.typed +0 -0
  358. coco_graph/store/sqlite/__init__.py +4 -0
  359. coco_graph/store/sqlite/aio.py +632 -0
  360. coco_graph/store/sqlite/base.py +1514 -0
  361. coco_graph/stream/__init__.py +45 -0
  362. coco_graph/stream/_convert.py +32 -0
  363. coco_graph/stream/_mux.py +523 -0
  364. coco_graph/stream/_types.py +330 -0
  365. coco_graph/stream/run_stream.py +735 -0
  366. coco_graph/stream/stream_channel.py +341 -0
  367. coco_graph/stream/transformers.py +1039 -0
  368. coco_graph/types.py +1079 -0
  369. coco_graph/typing.py +48 -0
  370. coco_graph/utils/__init__.py +1 -0
  371. coco_graph/utils/config.py +4 -0
  372. coco_graph/utils/runnable.py +3 -0
  373. coco_graph/version.py +13 -0
  374. coco_graph/warnings.py +69 -0
  375. coco_openai/__init__.py +20 -0
  376. coco_openai/_compat.py +45 -0
  377. coco_openai/_version.py +3 -0
  378. coco_openai/chat_models/__init__.py +6 -0
  379. coco_openai/chat_models/_client_utils.py +684 -0
  380. coco_openai/chat_models/_compat.py +525 -0
  381. coco_openai/chat_models/azure.py +1227 -0
  382. coco_openai/chat_models/base.py +5654 -0
  383. coco_openai/chat_models/codex.py +557 -0
  384. coco_openai/chatgpt_oauth.py +1063 -0
  385. coco_openai/data/__init__.py +1 -0
  386. coco_openai/data/_profiles.py +1655 -0
  387. coco_openai/data/profile_augmentations.toml +127 -0
  388. coco_openai/embeddings/__init__.py +6 -0
  389. coco_openai/embeddings/azure.py +240 -0
  390. coco_openai/embeddings/base.py +820 -0
  391. coco_openai/llms/__init__.py +6 -0
  392. coco_openai/llms/azure.py +249 -0
  393. coco_openai/llms/base.py +919 -0
  394. coco_openai/middleware/__init__.py +11 -0
  395. coco_openai/middleware/openai_moderation.py +484 -0
  396. coco_openai/output_parsers/__init__.py +9 -0
  397. coco_openai/output_parsers/tools.py +9 -0
  398. coco_openai/py.typed +0 -0
  399. coco_openai/tools/__init__.py +5 -0
  400. coco_openai/tools/custom_tool.py +111 -0
@@ -0,0 +1,1089 @@
1
+ """Derivations of standard content blocks from OpenAI content."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import warnings
7
+ from typing import TYPE_CHECKING, Any, Literal, cast
8
+
9
+ from coco_core.language_models._utils import (
10
+ _parse_data_uri,
11
+ is_openai_data_block,
12
+ )
13
+ from coco_core.messages import AIMessageChunk
14
+ from coco_core.messages import content as types
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Iterator
18
+
19
+ from coco_core.messages import AIMessage
20
+
21
+
22
+ def convert_to_openai_image_block(block: dict[str, Any]) -> dict[str, Any]:
23
+ """Convert `ImageContentBlock` to format expected by OpenAI Chat Completions.
24
+
25
+ Args:
26
+ block: The image content block to convert.
27
+
28
+ Raises:
29
+ ValueError: If required keys are missing.
30
+ ValueError: If source type is unsupported.
31
+
32
+ Returns:
33
+ The formatted image content block.
34
+ """
35
+ if "url" in block:
36
+ return {
37
+ "type": "image_url",
38
+ "image_url": {
39
+ "url": block["url"],
40
+ },
41
+ }
42
+ if "base64" in block or block.get("source_type") == "base64":
43
+ if "mime_type" not in block:
44
+ error_message = "mime_type key is required for base64 data."
45
+ raise ValueError(error_message)
46
+ mime_type = block["mime_type"]
47
+ base64_data = block["data"] if "data" in block else block["base64"]
48
+ return {
49
+ "type": "image_url",
50
+ "image_url": {
51
+ "url": f"data:{mime_type};base64,{base64_data}",
52
+ },
53
+ }
54
+ error_message = "Unsupported source type. Only 'url' and 'base64' are supported."
55
+ raise ValueError(error_message)
56
+
57
+
58
+ def convert_to_openai_data_block(
59
+ block: dict[str, Any],
60
+ api: Literal["chat/completions", "responses"] = "chat/completions",
61
+ ) -> dict[str, Any]:
62
+ """Format standard data content block to format expected by OpenAI.
63
+
64
+ "Standard data content block" can include old-style Coco v0 blocks
65
+ (URLContentBlock, Base64ContentBlock, IDContentBlock) or new ones.
66
+
67
+ Args:
68
+ block: The content block to convert.
69
+ api: The OpenAI API being targeted. Either "chat/completions" or "responses".
70
+
71
+ Raises:
72
+ ValueError: If required keys are missing.
73
+ ValueError: If file URLs are used with Chat Completions API.
74
+ ValueError: If block type is unsupported.
75
+
76
+ Returns:
77
+ The formatted content block.
78
+ """
79
+ if block["type"] == "image":
80
+ chat_completions_block = convert_to_openai_image_block(block)
81
+ if api == "responses":
82
+ formatted_block = {
83
+ "type": "input_image",
84
+ "image_url": chat_completions_block["image_url"]["url"],
85
+ }
86
+ if chat_completions_block["image_url"].get("detail"):
87
+ formatted_block["detail"] = chat_completions_block["image_url"][
88
+ "detail"
89
+ ]
90
+ else:
91
+ formatted_block = chat_completions_block
92
+
93
+ elif block["type"] == "file":
94
+ if block.get("source_type") == "base64" or "base64" in block:
95
+ # Handle v0 format (Base64CB): {"source_type": "base64", "data": "...", ...}
96
+ # Handle v1 format (IDCB): {"base64": "...", ...}
97
+ base64_data = block["data"] if "source_type" in block else block["base64"]
98
+ file = {"file_data": f"data:{block['mime_type']};base64,{base64_data}"}
99
+ if filename := block.get("filename"):
100
+ file["filename"] = filename
101
+ elif (extras := block.get("extras")) and ("filename" in extras):
102
+ file["filename"] = extras["filename"]
103
+ elif (extras := block.get("metadata")) and ("filename" in extras):
104
+ # Backward compat
105
+ file["filename"] = extras["filename"]
106
+ else:
107
+ # Can't infer filename; set a placeholder default for compatibility.
108
+ file["filename"] = "LC_AUTOGENERATED"
109
+ warnings.warn(
110
+ "OpenAI may require a filename for file uploads. Specify a filename"
111
+ " in the content block, e.g.: {'type': 'file', 'mime_type': "
112
+ "'...', 'base64': '...', 'filename': 'my-file.pdf'}. "
113
+ "Using placeholder filename 'LC_AUTOGENERATED'.",
114
+ stacklevel=1,
115
+ )
116
+ formatted_block = {"type": "file", "file": file}
117
+ if api == "responses":
118
+ formatted_block = {"type": "input_file", **formatted_block["file"]}
119
+ elif block.get("source_type") == "id" or "file_id" in block:
120
+ # Handle v0 format (IDContentBlock): {"source_type": "id", "id": "...", ...}
121
+ # Handle v1 format (IDCB): {"file_id": "...", ...}
122
+ file_id = block["id"] if "source_type" in block else block["file_id"]
123
+ formatted_block = {"type": "file", "file": {"file_id": file_id}}
124
+ if api == "responses":
125
+ formatted_block = {"type": "input_file", **formatted_block["file"]}
126
+ elif "url" in block: # Intentionally do not check for source_type="url"
127
+ if api == "chat/completions":
128
+ error_msg = "OpenAI Chat Completions does not support file URLs."
129
+ raise ValueError(error_msg)
130
+ # Only supported by Responses API; return in that format
131
+ formatted_block = {"type": "input_file", "file_url": block["url"]}
132
+ else:
133
+ error_msg = "Keys base64, url, or file_id required for file blocks."
134
+ raise ValueError(error_msg)
135
+
136
+ elif block["type"] == "audio":
137
+ if "base64" in block or block.get("source_type") == "base64":
138
+ # Handle v0 format: {"source_type": "base64", "data": "...", ...}
139
+ # Handle v1 format: {"base64": "...", ...}
140
+ base64_data = block["data"] if "source_type" in block else block["base64"]
141
+ audio_format = block["mime_type"].split("/")[-1]
142
+ formatted_block = {
143
+ "type": "input_audio",
144
+ "input_audio": {"data": base64_data, "format": audio_format},
145
+ }
146
+ else:
147
+ error_msg = "Key base64 is required for audio blocks."
148
+ raise ValueError(error_msg)
149
+ else:
150
+ error_msg = f"Block of type {block['type']} is not supported."
151
+ raise ValueError(error_msg)
152
+
153
+ return formatted_block
154
+
155
+
156
+ # v1 / Chat Completions
157
+ def _convert_to_v1_from_chat_completions(
158
+ message: AIMessage,
159
+ ) -> list[types.ContentBlock]:
160
+ """Mutate a Chat Completions message to v1 format."""
161
+ content_blocks: list[types.ContentBlock] = []
162
+ if isinstance(message.content, str):
163
+ if message.content:
164
+ content_blocks = [{"type": "text", "text": message.content}]
165
+ else:
166
+ content_blocks = []
167
+
168
+ for tool_call in message.tool_calls:
169
+ content_blocks.append(
170
+ {
171
+ "type": "tool_call",
172
+ "name": tool_call["name"],
173
+ "args": tool_call["args"],
174
+ "id": tool_call.get("id"),
175
+ }
176
+ )
177
+
178
+ return content_blocks
179
+
180
+
181
+ def _convert_to_v1_from_chat_completions_input(
182
+ content: list[types.ContentBlock],
183
+ ) -> list[types.ContentBlock]:
184
+ """Convert OpenAI Chat Completions format blocks to v1 format.
185
+
186
+ During the `content_blocks` parsing process, we wrap blocks not recognized as a v1
187
+ block as a `'non_standard'` block with the original block stored in the `value`
188
+ field. This function attempts to unpack those blocks and convert any blocks that
189
+ might be OpenAI format to v1 ContentBlocks.
190
+
191
+ If conversion fails, the block is left as a `'non_standard'` block.
192
+
193
+ Args:
194
+ content: List of content blocks to process.
195
+
196
+ Returns:
197
+ Updated list with OpenAI blocks converted to v1 format.
198
+ """
199
+ converted_blocks = []
200
+ unpacked_blocks: list[dict[str, Any]] = [
201
+ cast("dict[str, Any]", block)
202
+ if block.get("type") != "non_standard"
203
+ else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
204
+ for block in content
205
+ ]
206
+ for block in unpacked_blocks:
207
+ if block.get("type") in {
208
+ "image_url",
209
+ "input_audio",
210
+ "file",
211
+ } and is_openai_data_block(block):
212
+ converted_block = _convert_openai_format_to_data_block(block)
213
+ # If conversion succeeded, use it; otherwise keep as non_standard
214
+ if (
215
+ isinstance(converted_block, dict)
216
+ and converted_block.get("type") in types.KNOWN_BLOCK_TYPES
217
+ ):
218
+ converted_blocks.append(cast("types.ContentBlock", converted_block))
219
+ else:
220
+ converted_blocks.append({"type": "non_standard", "value": block})
221
+ elif block.get("type") in types.KNOWN_BLOCK_TYPES:
222
+ converted_blocks.append(cast("types.ContentBlock", block))
223
+ else:
224
+ converted_blocks.append({"type": "non_standard", "value": block})
225
+
226
+ return converted_blocks
227
+
228
+
229
+ def _convert_to_v1_from_chat_completions_chunk(
230
+ chunk: AIMessageChunk,
231
+ ) -> list[types.ContentBlock]:
232
+ """Mutate a Chat Completions chunk to v1 format."""
233
+ content_blocks: list[types.ContentBlock] = []
234
+ if isinstance(chunk.content, str):
235
+ if chunk.content:
236
+ content_blocks = [{"type": "text", "text": chunk.content}]
237
+ else:
238
+ content_blocks = []
239
+
240
+ if chunk.chunk_position == "last":
241
+ for tool_call in chunk.tool_calls:
242
+ content_blocks.append(
243
+ {
244
+ "type": "tool_call",
245
+ "name": tool_call["name"],
246
+ "args": tool_call["args"],
247
+ "id": tool_call.get("id"),
248
+ }
249
+ )
250
+
251
+ else:
252
+ for tool_call_chunk in chunk.tool_call_chunks:
253
+ tc: types.ToolCallChunk = {
254
+ "type": "tool_call_chunk",
255
+ "id": tool_call_chunk.get("id"),
256
+ "name": tool_call_chunk.get("name"),
257
+ "args": tool_call_chunk.get("args"),
258
+ }
259
+ if (idx := tool_call_chunk.get("index")) is not None:
260
+ tc["index"] = idx
261
+ content_blocks.append(tc)
262
+
263
+ return content_blocks
264
+
265
+
266
+ def _convert_from_v1_to_chat_completions(message: AIMessage) -> AIMessage:
267
+ """Convert a v1 message to the Chat Completions format."""
268
+ if isinstance(message.content, list):
269
+ new_content: list[Any] = []
270
+ for block in message.content:
271
+ if isinstance(block, dict):
272
+ block_type = block.get("type")
273
+ if block_type == "text":
274
+ # Strip annotations
275
+ new_content.append({"type": "text", "text": block["text"]})
276
+ elif block_type in {"reasoning", "tool_call"}:
277
+ pass
278
+ else:
279
+ new_content.append(block)
280
+ else:
281
+ new_content.append(block)
282
+ return message.model_copy(update={"content": new_content})
283
+
284
+ return message
285
+
286
+
287
+ # Responses
288
+ _FUNCTION_CALL_IDS_MAP_KEY = "__openai_function_call_ids__"
289
+
290
+
291
+ def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
292
+ """Convert v0 AIMessage into `output_version="responses/v1"` format."""
293
+ # Only update ChatOpenAI v0.3 AIMessages
294
+ is_chatopenai_v03 = (
295
+ isinstance(message.content, list)
296
+ and all(isinstance(b, dict) for b in message.content)
297
+ ) and (
298
+ any(
299
+ item in message.additional_kwargs
300
+ for item in [
301
+ "reasoning",
302
+ "tool_outputs",
303
+ "refusal",
304
+ _FUNCTION_CALL_IDS_MAP_KEY,
305
+ ]
306
+ )
307
+ or (
308
+ isinstance(message.id, str)
309
+ and message.id.startswith("msg_")
310
+ and (response_id := message.response_metadata.get("id"))
311
+ and isinstance(response_id, str)
312
+ and response_id.startswith("resp_")
313
+ )
314
+ )
315
+ if not is_chatopenai_v03:
316
+ return message
317
+
318
+ content_order = [
319
+ "reasoning",
320
+ "code_interpreter_call",
321
+ "mcp_call",
322
+ "image_generation_call",
323
+ "text",
324
+ "refusal",
325
+ "function_call",
326
+ "computer_call",
327
+ "mcp_list_tools",
328
+ "mcp_approval_request",
329
+ # N. B. "web_search_call" and "file_search_call" were not passed back in
330
+ # in v0.3
331
+ ]
332
+
333
+ # Build a bucket for every known block type
334
+ buckets: dict[str, list[Any]] = {key: [] for key in content_order}
335
+ unknown_blocks = []
336
+
337
+ # Reasoning
338
+ if reasoning := message.additional_kwargs.get("reasoning"):
339
+ if "type" not in reasoning:
340
+ reasoning = {**reasoning, "type": "reasoning"}
341
+ buckets["reasoning"].append(reasoning)
342
+
343
+ # Refusal
344
+ if refusal := message.additional_kwargs.get("refusal"):
345
+ buckets["refusal"].append({"type": "refusal", "refusal": refusal})
346
+
347
+ # Text
348
+ for block in message.content:
349
+ if isinstance(block, dict) and block.get("type") == "text":
350
+ block_copy = block.copy()
351
+ if isinstance(message.id, str) and message.id.startswith("msg_"):
352
+ block_copy["id"] = message.id
353
+ buckets["text"].append(block_copy)
354
+ else:
355
+ unknown_blocks.append(block)
356
+
357
+ # Function calls
358
+ function_call_ids = message.additional_kwargs.get(_FUNCTION_CALL_IDS_MAP_KEY)
359
+ if (
360
+ isinstance(message, AIMessageChunk)
361
+ and len(message.tool_call_chunks) == 1
362
+ and message.chunk_position != "last"
363
+ ):
364
+ # Isolated chunk
365
+ tool_call_chunk = message.tool_call_chunks[0]
366
+ function_call = {
367
+ "type": "function_call",
368
+ "name": tool_call_chunk.get("name"),
369
+ "arguments": tool_call_chunk.get("args"),
370
+ "call_id": tool_call_chunk.get("id"),
371
+ }
372
+ if function_call_ids is not None and (
373
+ id_ := function_call_ids.get(tool_call_chunk.get("id"))
374
+ ):
375
+ function_call["id"] = id_
376
+ buckets["function_call"].append(function_call)
377
+ else:
378
+ for tool_call in message.tool_calls:
379
+ function_call = {
380
+ "type": "function_call",
381
+ "name": tool_call["name"],
382
+ "arguments": json.dumps(tool_call["args"], ensure_ascii=False),
383
+ "call_id": tool_call["id"],
384
+ }
385
+ if function_call_ids is not None and (
386
+ id_ := function_call_ids.get(tool_call["id"])
387
+ ):
388
+ function_call["id"] = id_
389
+ buckets["function_call"].append(function_call)
390
+
391
+ # Tool outputs
392
+ tool_outputs = message.additional_kwargs.get("tool_outputs", [])
393
+ for block in tool_outputs:
394
+ if isinstance(block, dict) and (key := block.get("type")) and key in buckets:
395
+ buckets[key].append(block)
396
+ else:
397
+ unknown_blocks.append(block)
398
+
399
+ # Re-assemble the content list in the canonical order
400
+ new_content = []
401
+ for key in content_order:
402
+ new_content.extend(buckets[key])
403
+ new_content.extend(unknown_blocks)
404
+
405
+ new_additional_kwargs = dict(message.additional_kwargs)
406
+ new_additional_kwargs.pop("reasoning", None)
407
+ new_additional_kwargs.pop("refusal", None)
408
+ new_additional_kwargs.pop("tool_outputs", None)
409
+
410
+ if "id" in message.response_metadata:
411
+ new_id = message.response_metadata["id"]
412
+ else:
413
+ new_id = message.id
414
+
415
+ return message.model_copy(
416
+ update={
417
+ "content": new_content,
418
+ "additional_kwargs": new_additional_kwargs,
419
+ "id": new_id,
420
+ },
421
+ deep=False,
422
+ )
423
+
424
+
425
+ def _convert_openai_format_to_data_block(
426
+ block: dict[str, Any],
427
+ ) -> types.ContentBlock | dict[str, Any]:
428
+ """Convert OpenAI image/audio/file content block to respective v1 multimodal block.
429
+
430
+ We expect that the incoming block is verified to be in OpenAI Chat Completions
431
+ format.
432
+
433
+ If parsing fails, passes block through unchanged.
434
+
435
+ Mappings (Chat Completions to Coco v1):
436
+ - Image -> `ImageContentBlock`
437
+ - Audio -> `AudioContentBlock`
438
+ - File -> `FileContentBlock`
439
+
440
+ """
441
+
442
+ # Extract extra keys to put them in `extras`
443
+ def _extract_extras(
444
+ block_dict: dict[str, Any], known_keys: set[str]
445
+ ) -> dict[str, Any]:
446
+ """Extract unknown keys from block to preserve as extras."""
447
+ return {k: v for k, v in block_dict.items() if k not in known_keys}
448
+
449
+ # base64-style image block
450
+ if (block["type"] == "image_url") and (
451
+ parsed := _parse_data_uri(block["image_url"]["url"])
452
+ ):
453
+ known_keys = {"type", "image_url"}
454
+ extras = _extract_extras(block, known_keys)
455
+
456
+ # Also extract extras from nested image_url dict
457
+ image_url_known_keys = {"url"}
458
+ image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
459
+
460
+ # Merge extras
461
+ all_extras = {**extras}
462
+ for key, value in image_url_extras.items():
463
+ if key == "detail": # Don't rename
464
+ all_extras["detail"] = value
465
+ else:
466
+ all_extras[f"image_url_{key}"] = value
467
+
468
+ return types.create_image_block(
469
+ # Even though this is labeled as `url`, it can be base64-encoded
470
+ base64=parsed["data"],
471
+ mime_type=parsed["mime_type"],
472
+ **all_extras,
473
+ )
474
+
475
+ # url-style image block
476
+ if (block["type"] == "image_url") and isinstance(
477
+ block["image_url"].get("url"), str
478
+ ):
479
+ known_keys = {"type", "image_url"}
480
+ extras = _extract_extras(block, known_keys)
481
+
482
+ image_url_known_keys = {"url"}
483
+ image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
484
+
485
+ all_extras = {**extras}
486
+ for key, value in image_url_extras.items():
487
+ if key == "detail": # Don't rename
488
+ all_extras["detail"] = value
489
+ else:
490
+ all_extras[f"image_url_{key}"] = value
491
+
492
+ return types.create_image_block(
493
+ url=block["image_url"]["url"],
494
+ **all_extras,
495
+ )
496
+
497
+ # base64-style audio block
498
+ # audio is only represented via raw data, no url or ID option
499
+ if block["type"] == "input_audio":
500
+ known_keys = {"type", "input_audio"}
501
+ extras = _extract_extras(block, known_keys)
502
+
503
+ # Also extract extras from nested audio dict
504
+ audio_known_keys = {"data", "format"}
505
+ audio_extras = _extract_extras(block["input_audio"], audio_known_keys)
506
+
507
+ all_extras = {**extras}
508
+ for key, value in audio_extras.items():
509
+ all_extras[f"audio_{key}"] = value
510
+
511
+ return types.create_audio_block(
512
+ base64=block["input_audio"]["data"],
513
+ mime_type=f"audio/{block['input_audio']['format']}",
514
+ **all_extras,
515
+ )
516
+
517
+ # id-style file block
518
+ if block.get("type") == "file" and "file_id" in block.get("file", {}):
519
+ known_keys = {"type", "file"}
520
+ extras = _extract_extras(block, known_keys)
521
+
522
+ file_known_keys = {"file_id"}
523
+ file_extras = _extract_extras(block["file"], file_known_keys)
524
+
525
+ all_extras = {**extras}
526
+ for key, value in file_extras.items():
527
+ all_extras[f"file_{key}"] = value
528
+
529
+ return types.create_file_block(
530
+ file_id=block["file"]["file_id"],
531
+ **all_extras,
532
+ )
533
+
534
+ # base64-style file block
535
+ if (block["type"] == "file") and (
536
+ parsed := _parse_data_uri(block["file"]["file_data"])
537
+ ):
538
+ known_keys = {"type", "file"}
539
+ extras = _extract_extras(block, known_keys)
540
+
541
+ file_known_keys = {"file_data", "filename"}
542
+ file_extras = _extract_extras(block["file"], file_known_keys)
543
+
544
+ all_extras = {**extras}
545
+ for key, value in file_extras.items():
546
+ all_extras[f"file_{key}"] = value
547
+
548
+ filename = block["file"].get("filename")
549
+ return types.create_file_block(
550
+ base64=parsed["data"],
551
+ mime_type="application/pdf",
552
+ filename=filename,
553
+ **all_extras,
554
+ )
555
+
556
+ # Escape hatch
557
+ return block
558
+
559
+
560
+ # v1 / Responses
561
+ def _convert_annotation_to_v1(annotation: dict[str, Any]) -> types.Annotation:
562
+ annotation_type = annotation.get("type")
563
+
564
+ if annotation_type == "url_citation":
565
+ known_fields = {
566
+ "type",
567
+ "url",
568
+ "title",
569
+ "cited_text",
570
+ "start_index",
571
+ "end_index",
572
+ }
573
+ url_citation = cast("types.Citation", {})
574
+ for field in ("end_index", "start_index", "title"):
575
+ if field in annotation:
576
+ url_citation[field] = annotation[field]
577
+ url_citation["type"] = "citation"
578
+ url_citation["url"] = annotation["url"]
579
+ for field, value in annotation.items():
580
+ if field not in known_fields:
581
+ if "extras" not in url_citation:
582
+ url_citation["extras"] = {}
583
+ url_citation["extras"][field] = value
584
+ return url_citation
585
+
586
+ if annotation_type == "file_citation":
587
+ known_fields = {
588
+ "type",
589
+ "title",
590
+ "cited_text",
591
+ "start_index",
592
+ "end_index",
593
+ "filename",
594
+ }
595
+ document_citation: types.Citation = {"type": "citation"}
596
+ if "filename" in annotation:
597
+ document_citation["title"] = annotation["filename"]
598
+ for field, value in annotation.items():
599
+ if field not in known_fields:
600
+ if "extras" not in document_citation:
601
+ document_citation["extras"] = {}
602
+ document_citation["extras"][field] = value
603
+
604
+ return document_citation
605
+
606
+ # TODO: standardise container_file_citation?
607
+ non_standard_annotation: types.NonStandardAnnotation = {
608
+ "type": "non_standard_annotation",
609
+ "value": annotation,
610
+ }
611
+ return non_standard_annotation
612
+
613
+
614
+ def _explode_reasoning(block: dict[str, Any]) -> Iterator[types.ReasoningContentBlock]:
615
+ if "summary" not in block:
616
+ yield cast("types.ReasoningContentBlock", block)
617
+ return
618
+
619
+ known_fields = {"type", "reasoning", "id", "index"}
620
+ unknown_fields = [
621
+ field for field in block if field != "summary" and field not in known_fields
622
+ ]
623
+ if unknown_fields:
624
+ block["extras"] = {}
625
+ for field in unknown_fields:
626
+ block["extras"][field] = block.pop(field)
627
+
628
+ if not block["summary"]:
629
+ # [{'id': 'rs_...', 'summary': [], 'type': 'reasoning', 'index': 0}]
630
+ block = {k: v for k, v in block.items() if k != "summary"}
631
+ if "index" in block:
632
+ meaningful_idx = f"{block['index']}_0"
633
+ block["index"] = f"lc_rs_{meaningful_idx.encode().hex()}"
634
+ yield cast("types.ReasoningContentBlock", block)
635
+ return
636
+
637
+ # Common part for every exploded line, except 'summary'
638
+ common = {k: v for k, v in block.items() if k in known_fields}
639
+
640
+ # Optional keys that must appear only in the first exploded item
641
+ first_only = block.pop("extras", None)
642
+
643
+ for idx, part in enumerate(block["summary"]):
644
+ new_block = dict(common)
645
+ new_block["reasoning"] = part.get("text", "")
646
+ if idx == 0 and first_only:
647
+ new_block.update(first_only)
648
+ if "index" in new_block:
649
+ summary_index = part.get("index", 0)
650
+ meaningful_idx = f"{new_block['index']}_{summary_index}"
651
+ new_block["index"] = f"lc_rs_{meaningful_idx.encode().hex()}"
652
+
653
+ yield cast("types.ReasoningContentBlock", new_block)
654
+
655
+
656
+ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock]:
657
+ """Convert a Responses message to v1 format."""
658
+
659
+ def _iter_blocks() -> Iterator[types.ContentBlock]:
660
+ for raw_block in message.content:
661
+ if not isinstance(raw_block, dict):
662
+ continue
663
+ block = raw_block.copy()
664
+ block_type = block.get("type")
665
+
666
+ if block_type == "text":
667
+ if "text" not in block:
668
+ block["text"] = ""
669
+ if "annotations" in block:
670
+ block["annotations"] = [
671
+ _convert_annotation_to_v1(a) for a in block["annotations"]
672
+ ]
673
+ if "index" in block:
674
+ block["index"] = f"lc_txt_{block['index']}"
675
+ yield cast("types.TextContentBlock", block)
676
+
677
+ elif block_type == "reasoning":
678
+ yield from _explode_reasoning(block)
679
+
680
+ elif block_type == "image_generation_call" and (
681
+ result := block.get("result")
682
+ ):
683
+ new_block = {"type": "image", "base64": result}
684
+ if output_format := block.get("output_format"):
685
+ new_block["mime_type"] = f"image/{output_format}"
686
+ if "id" in block:
687
+ new_block["id"] = block["id"]
688
+ if "index" in block:
689
+ new_block["index"] = f"lc_img_{block['index']}"
690
+ for extra_key in (
691
+ "status",
692
+ "background",
693
+ "output_format",
694
+ "quality",
695
+ "revised_prompt",
696
+ "size",
697
+ ):
698
+ if extra_key in block:
699
+ if "extras" not in new_block:
700
+ new_block["extras"] = {}
701
+ new_block["extras"][extra_key] = block[extra_key]
702
+ yield cast("types.ImageContentBlock", new_block)
703
+
704
+ elif block_type == "function_call":
705
+ tool_call_block: (
706
+ types.ToolCall | types.InvalidToolCall | types.ToolCallChunk | None
707
+ ) = None
708
+ call_id = block.get("call_id", "")
709
+
710
+ if (
711
+ isinstance(message, AIMessageChunk)
712
+ and len(message.tool_call_chunks) == 1
713
+ and message.chunk_position != "last"
714
+ ):
715
+ tool_call_block = message.tool_call_chunks[0].copy() # type: ignore[assignment]
716
+ elif call_id:
717
+ for tool_call in message.tool_calls or []:
718
+ if tool_call.get("id") == call_id:
719
+ tool_call_block = {
720
+ "type": "tool_call",
721
+ "name": tool_call["name"],
722
+ "args": tool_call["args"],
723
+ "id": tool_call.get("id"),
724
+ }
725
+ break
726
+ else:
727
+ for invalid_tool_call in message.invalid_tool_calls or []:
728
+ if invalid_tool_call.get("id") == call_id:
729
+ tool_call_block = invalid_tool_call.copy()
730
+ break
731
+ if tool_call_block:
732
+ if "id" in block:
733
+ if "extras" not in tool_call_block:
734
+ tool_call_block["extras"] = {}
735
+ tool_call_block["extras"]["item_id"] = block["id"]
736
+ if "index" in block:
737
+ tool_call_block["index"] = f"lc_tc_{block['index']}"
738
+ for extra_key in ("status", "namespace", "async"):
739
+ if extra_key in block:
740
+ if "extras" not in tool_call_block:
741
+ tool_call_block["extras"] = {}
742
+ tool_call_block["extras"][extra_key] = block[extra_key]
743
+ yield tool_call_block
744
+
745
+ elif block_type == "web_search_call":
746
+ web_search_call = {
747
+ "type": "server_tool_call",
748
+ "name": "web_search",
749
+ "args": {},
750
+ "id": block["id"],
751
+ }
752
+ if "index" in block:
753
+ web_search_call["index"] = f"lc_wsc_{block['index']}"
754
+
755
+ sources: dict[str, Any] | None = None
756
+ if "action" in block and isinstance(block["action"], dict):
757
+ if "sources" in block["action"]:
758
+ sources = block["action"]["sources"]
759
+ web_search_call["args"] = {
760
+ k: v for k, v in block["action"].items() if k != "sources"
761
+ }
762
+ for key in block:
763
+ if key not in {"type", "id", "action", "status", "index"}:
764
+ web_search_call[key] = block[key]
765
+
766
+ yield cast("types.ServerToolCall", web_search_call)
767
+
768
+ # If .content already has web_search_result, don't add
769
+ if not any(
770
+ isinstance(other_block, dict)
771
+ and other_block.get("type") == "web_search_result"
772
+ and other_block.get("id") == block["id"]
773
+ for other_block in message.content
774
+ ):
775
+ web_search_result = {
776
+ "type": "server_tool_result",
777
+ "tool_call_id": block["id"],
778
+ }
779
+ if sources:
780
+ web_search_result["output"] = {"sources": sources}
781
+
782
+ status = block.get("status")
783
+ if status == "failed":
784
+ web_search_result["status"] = "error"
785
+ elif status == "completed":
786
+ web_search_result["status"] = "success"
787
+ elif status:
788
+ web_search_result["extras"] = {"status": status}
789
+ if "index" in block and isinstance(block["index"], int):
790
+ web_search_result["index"] = f"lc_wsr_{block['index'] + 1}"
791
+ yield cast("types.ServerToolResult", web_search_result)
792
+
793
+ elif block_type == "file_search_call":
794
+ file_search_call = {
795
+ "type": "server_tool_call",
796
+ "name": "file_search",
797
+ "id": block["id"],
798
+ "args": {"queries": block.get("queries", [])},
799
+ }
800
+ if "index" in block:
801
+ file_search_call["index"] = f"lc_fsc_{block['index']}"
802
+
803
+ for key in block:
804
+ if key not in {
805
+ "type",
806
+ "id",
807
+ "queries",
808
+ "results",
809
+ "status",
810
+ "index",
811
+ }:
812
+ file_search_call[key] = block[key]
813
+
814
+ yield cast("types.ServerToolCall", file_search_call)
815
+
816
+ file_search_result = {
817
+ "type": "server_tool_result",
818
+ "tool_call_id": block["id"],
819
+ }
820
+ if file_search_output := block.get("results"):
821
+ file_search_result["output"] = file_search_output
822
+
823
+ status = block.get("status")
824
+ if status == "failed":
825
+ file_search_result["status"] = "error"
826
+ elif status == "completed":
827
+ file_search_result["status"] = "success"
828
+ elif status:
829
+ file_search_result["extras"] = {"status": status}
830
+ if "index" in block and isinstance(block["index"], int):
831
+ file_search_result["index"] = f"lc_fsr_{block['index'] + 1}"
832
+ yield cast("types.ServerToolResult", file_search_result)
833
+
834
+ elif block_type == "code_interpreter_call":
835
+ code_interpreter_call = {
836
+ "type": "server_tool_call",
837
+ "name": "code_interpreter",
838
+ "id": block["id"],
839
+ }
840
+ if "code" in block:
841
+ code_interpreter_call["args"] = {"code": block["code"]}
842
+ if "index" in block:
843
+ code_interpreter_call["index"] = f"lc_cic_{block['index']}"
844
+ known_fields = {
845
+ "type",
846
+ "id",
847
+ "outputs",
848
+ "status",
849
+ "code",
850
+ "extras",
851
+ "index",
852
+ }
853
+ for key in block:
854
+ if key not in known_fields:
855
+ if "extras" not in code_interpreter_call:
856
+ code_interpreter_call["extras"] = {}
857
+ code_interpreter_call["extras"][key] = block[key]
858
+
859
+ code_interpreter_result = {
860
+ "type": "server_tool_result",
861
+ "tool_call_id": block["id"],
862
+ }
863
+ if "outputs" in block:
864
+ code_interpreter_result["output"] = block["outputs"]
865
+
866
+ status = block.get("status")
867
+ if status == "failed":
868
+ code_interpreter_result["status"] = "error"
869
+ elif status == "completed":
870
+ code_interpreter_result["status"] = "success"
871
+ elif status:
872
+ code_interpreter_result["extras"] = {"status": status}
873
+ if "index" in block and isinstance(block["index"], int):
874
+ code_interpreter_result["index"] = f"lc_cir_{block['index'] + 1}"
875
+
876
+ yield cast("types.ServerToolCall", code_interpreter_call)
877
+ yield cast("types.ServerToolResult", code_interpreter_result)
878
+
879
+ elif block_type == "mcp_call":
880
+ mcp_call = {
881
+ "type": "server_tool_call",
882
+ "name": "remote_mcp",
883
+ "id": block["id"],
884
+ }
885
+ if (arguments := block.get("arguments")) and isinstance(arguments, str):
886
+ try:
887
+ mcp_call["args"] = json.loads(block["arguments"])
888
+ except json.JSONDecodeError:
889
+ mcp_call["extras"] = {"arguments": arguments}
890
+ if "name" in block:
891
+ if "extras" not in mcp_call:
892
+ mcp_call["extras"] = {}
893
+ mcp_call["extras"]["tool_name"] = block["name"]
894
+ if "server_label" in block:
895
+ if "extras" not in mcp_call:
896
+ mcp_call["extras"] = {}
897
+ mcp_call["extras"]["server_label"] = block["server_label"]
898
+ if "index" in block:
899
+ mcp_call["index"] = f"lc_mcp_{block['index']}"
900
+ known_fields = {
901
+ "type",
902
+ "id",
903
+ "arguments",
904
+ "name",
905
+ "server_label",
906
+ "output",
907
+ "error",
908
+ "extras",
909
+ "index",
910
+ }
911
+ for key in block:
912
+ if key not in known_fields:
913
+ if "extras" not in mcp_call:
914
+ mcp_call["extras"] = {}
915
+ mcp_call["extras"][key] = block[key]
916
+
917
+ yield cast("types.ServerToolCall", mcp_call)
918
+
919
+ mcp_result = {
920
+ "type": "server_tool_result",
921
+ "tool_call_id": block["id"],
922
+ }
923
+ if mcp_output := block.get("output"):
924
+ mcp_result["output"] = mcp_output
925
+
926
+ error = block.get("error")
927
+ if error:
928
+ if "extras" not in mcp_result:
929
+ mcp_result["extras"] = {}
930
+ mcp_result["extras"]["error"] = error
931
+ mcp_result["status"] = "error"
932
+ else:
933
+ mcp_result["status"] = "success"
934
+
935
+ if "index" in block and isinstance(block["index"], int):
936
+ mcp_result["index"] = f"lc_mcpr_{block['index'] + 1}"
937
+ yield cast("types.ServerToolResult", mcp_result)
938
+
939
+ elif block_type == "mcp_list_tools":
940
+ mcp_list_tools_call = {
941
+ "type": "server_tool_call",
942
+ "name": "mcp_list_tools",
943
+ "args": {},
944
+ "id": block["id"],
945
+ }
946
+ if "server_label" in block:
947
+ mcp_list_tools_call["extras"] = {}
948
+ mcp_list_tools_call["extras"]["server_label"] = block[
949
+ "server_label"
950
+ ]
951
+ if "index" in block:
952
+ mcp_list_tools_call["index"] = f"lc_mlt_{block['index']}"
953
+ known_fields = {
954
+ "type",
955
+ "id",
956
+ "name",
957
+ "server_label",
958
+ "tools",
959
+ "error",
960
+ "extras",
961
+ "index",
962
+ }
963
+ for key in block:
964
+ if key not in known_fields:
965
+ if "extras" not in mcp_list_tools_call:
966
+ mcp_list_tools_call["extras"] = {}
967
+ mcp_list_tools_call["extras"][key] = block[key]
968
+
969
+ yield cast("types.ServerToolCall", mcp_list_tools_call)
970
+
971
+ mcp_list_tools_result = {
972
+ "type": "server_tool_result",
973
+ "tool_call_id": block["id"],
974
+ }
975
+ if mcp_output := block.get("tools"):
976
+ mcp_list_tools_result["output"] = mcp_output
977
+
978
+ error = block.get("error")
979
+ if error:
980
+ if "extras" not in mcp_list_tools_result:
981
+ mcp_list_tools_result["extras"] = {}
982
+ mcp_list_tools_result["extras"]["error"] = error
983
+ mcp_list_tools_result["status"] = "error"
984
+ else:
985
+ mcp_list_tools_result["status"] = "success"
986
+
987
+ if "index" in block and isinstance(block["index"], int):
988
+ mcp_list_tools_result["index"] = f"lc_mltr_{block['index'] + 1}"
989
+ yield cast("types.ServerToolResult", mcp_list_tools_result)
990
+
991
+ elif (
992
+ block_type == "tool_search_call" and block.get("execution") == "server"
993
+ ):
994
+ tool_search_call: dict[str, Any] = {
995
+ "type": "server_tool_call",
996
+ "name": "tool_search",
997
+ "id": block["id"],
998
+ "args": block.get("arguments", {}),
999
+ }
1000
+ if "index" in block:
1001
+ tool_search_call["index"] = f"lc_tsc_{block['index']}"
1002
+ extras: dict[str, Any] = {}
1003
+ known = {"type", "id", "arguments", "index"}
1004
+ for key in block:
1005
+ if key not in known:
1006
+ extras[key] = block[key]
1007
+ if extras:
1008
+ tool_search_call["extras"] = extras
1009
+ yield cast("types.ServerToolCall", tool_search_call)
1010
+
1011
+ elif (
1012
+ block_type == "tool_search_output"
1013
+ and block.get("execution") == "server"
1014
+ ):
1015
+ tool_search_output: dict[str, Any] = {
1016
+ "type": "server_tool_result",
1017
+ "tool_call_id": block["id"],
1018
+ "output": {"tools": block.get("tools", [])},
1019
+ }
1020
+ status = block.get("status")
1021
+ if status == "failed":
1022
+ tool_search_output["status"] = "error"
1023
+ elif status == "completed":
1024
+ tool_search_output["status"] = "success"
1025
+ if "index" in block and isinstance(block["index"], int):
1026
+ tool_search_output["index"] = f"lc_tso_{block['index']}"
1027
+ extras_out: dict[str, Any] = {"name": "tool_search"}
1028
+ known_out = {"type", "id", "status", "tools", "index"}
1029
+ for key in block:
1030
+ if key not in known_out:
1031
+ extras_out[key] = block[key]
1032
+ if extras_out:
1033
+ tool_search_output["extras"] = extras_out
1034
+ yield cast("types.ServerToolResult", tool_search_output)
1035
+
1036
+ elif block_type in types.KNOWN_BLOCK_TYPES:
1037
+ yield cast("types.ContentBlock", block)
1038
+ else:
1039
+ new_block = {"type": "non_standard", "value": block}
1040
+ if "index" in new_block["value"]:
1041
+ new_block["index"] = f"lc_ns_{new_block['value'].pop('index')}"
1042
+ yield cast("types.NonStandardContentBlock", new_block)
1043
+
1044
+ return list(_iter_blocks())
1045
+
1046
+
1047
+ def translate_content(message: AIMessage) -> list[types.ContentBlock]:
1048
+ """Derive standard content blocks from a message with OpenAI content.
1049
+
1050
+ Args:
1051
+ message: The message to translate.
1052
+
1053
+ Returns:
1054
+ The derived content blocks.
1055
+ """
1056
+ if isinstance(message.content, str):
1057
+ return _convert_to_v1_from_chat_completions(message)
1058
+ message = _convert_from_v03_ai_message(message)
1059
+ return _convert_to_v1_from_responses(message)
1060
+
1061
+
1062
+ def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
1063
+ """Derive standard content blocks from a message chunk with OpenAI content.
1064
+
1065
+ Args:
1066
+ message: The message chunk to translate.
1067
+
1068
+ Returns:
1069
+ The derived content blocks.
1070
+ """
1071
+ if isinstance(message.content, str):
1072
+ return _convert_to_v1_from_chat_completions_chunk(message)
1073
+ message = _convert_from_v03_ai_message(message) # type: ignore[assignment]
1074
+ return _convert_to_v1_from_responses(message)
1075
+
1076
+
1077
+ def _register_openai_translator() -> None:
1078
+ """Register the OpenAI translator with the central registry.
1079
+
1080
+ Run automatically when the module is imported.
1081
+ """
1082
+ from coco_core.messages.block_translators import ( # noqa: PLC0415
1083
+ register_translator,
1084
+ )
1085
+
1086
+ register_translator("openai", translate_content, translate_content_chunk)
1087
+
1088
+
1089
+ _register_openai_translator()