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,2748 @@
1
+ """Chat models for conversational AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import builtins # noqa: TC003 # runtime-evaluated; subclass `dict()` shadows the builtin
7
+ import contextlib
8
+ import inspect
9
+ import json
10
+ from abc import ABC, abstractmethod
11
+ from collections.abc import AsyncIterator, Callable, Iterator, Sequence
12
+ from functools import cached_property
13
+ from operator import itemgetter
14
+ from typing import TYPE_CHECKING, Any, Literal, cast, overload
15
+
16
+ from coco_core._protocol import MessageFinishData
17
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
18
+ from typing_extensions import Self, override
19
+
20
+ from coco_core._api import beta, deprecated, suppress_coco_deprecation_warning
21
+ from coco_core.caches import BaseCache
22
+ from coco_core.callbacks import (
23
+ AsyncCallbackManager,
24
+ AsyncCallbackManagerForLLMRun,
25
+ CallbackManager,
26
+ CallbackManagerForLLMRun,
27
+ Callbacks,
28
+ )
29
+ from coco_core.globals import get_llm_cache
30
+ from coco_core.language_models._compat_bridge import (
31
+ achunks_to_events,
32
+ amessage_to_events,
33
+ chunks_to_events,
34
+ message_to_events,
35
+ )
36
+ from coco_core.language_models._utils import (
37
+ _filter_invocation_params_for_tracing,
38
+ _normalize_messages,
39
+ _update_message_content_to_blocks,
40
+ )
41
+ from coco_core.language_models.base import (
42
+ BaseLanguageModel,
43
+ LangSmithParams,
44
+ LanguageModelInput,
45
+ )
46
+ from coco_core.language_models.chat_model_stream import (
47
+ AsyncChatModelStream,
48
+ ChatModelStream,
49
+ )
50
+ from coco_core.language_models.model_profile import (
51
+ ModelProfile,
52
+ _warn_unknown_profile_keys,
53
+ )
54
+ from coco_core.load import dumpd, dumps
55
+ from coco_core.messages import (
56
+ AIMessage,
57
+ AIMessageChunk,
58
+ AnyMessage,
59
+ BaseMessage,
60
+ convert_to_messages,
61
+ is_data_content_block,
62
+ message_chunk_to_message,
63
+ )
64
+ from coco_core.messages import content as types
65
+ from coco_core.messages.block_translators.openai import (
66
+ convert_to_openai_image_block,
67
+ )
68
+ from coco_core.output_parsers.openai_tools import (
69
+ JsonOutputKeyToolsParser,
70
+ JsonOutputToolsParser,
71
+ PydanticToolsParser,
72
+ )
73
+ from coco_core.outputs import (
74
+ ChatGeneration,
75
+ ChatGenerationChunk,
76
+ ChatResult,
77
+ Generation,
78
+ LLMResult,
79
+ RunInfo,
80
+ )
81
+ from coco_core.outputs.chat_generation import merge_chat_generation_chunks
82
+ from coco_core.prompt_values import ChatPromptValue, PromptValue, StringPromptValue
83
+ from coco_core.rate_limiters import BaseRateLimiter
84
+ from coco_core.runnables import RunnableBinding, RunnableMap, RunnablePassthrough
85
+ from coco_core.runnables.config import ensure_config, run_in_executor
86
+ from coco_core.tracers._streaming import (
87
+ _StreamingCallbackHandler,
88
+ _V2StreamingCallbackHandler,
89
+ )
90
+ from coco_core.utils._gateway import (
91
+ GATEWAY_METADATA_RESPONSE_KEY,
92
+ _parse_gateway_metadata,
93
+ )
94
+ from coco_core.utils.function_calling import (
95
+ convert_to_json_schema,
96
+ convert_to_openai_tool,
97
+ )
98
+ from coco_core.utils.pydantic import is_basemodel_subclass
99
+ from coco_core.utils.utils import LC_ID_PREFIX, from_env
100
+
101
+ if TYPE_CHECKING:
102
+ import uuid
103
+ from collections.abc import Awaitable
104
+
105
+ from coco_core._protocol import MessagesData
106
+
107
+ from coco_core.runnables import Runnable, RunnableConfig
108
+ from coco_core.runnables.schema import StreamEvent
109
+ from coco_core.tools import BaseTool
110
+
111
+
112
+ def _generate_response_from_error(error: BaseException) -> list[ChatGeneration]:
113
+ if hasattr(error, "response"):
114
+ response = error.response
115
+ metadata: dict[str, Any] = {}
116
+ generation_info: dict[str, Any] = {}
117
+ if hasattr(response, "json"):
118
+ try:
119
+ metadata["body"] = response.json()
120
+ except Exception:
121
+ try:
122
+ metadata["body"] = getattr(response, "text", None)
123
+ except Exception:
124
+ metadata["body"] = None
125
+ if hasattr(response, "headers"):
126
+ try:
127
+ headers = response.headers
128
+ metadata["headers"] = dict(headers)
129
+ gateway_metadata = _parse_gateway_metadata(headers)
130
+ if gateway_metadata is not None:
131
+ generation_info[GATEWAY_METADATA_RESPONSE_KEY] = gateway_metadata
132
+ except Exception:
133
+ metadata["headers"] = None
134
+ if hasattr(response, "status_code"):
135
+ metadata["status_code"] = response.status_code
136
+ if hasattr(error, "request_id"):
137
+ metadata["request_id"] = error.request_id
138
+ generations = [
139
+ ChatGeneration(
140
+ message=AIMessage(content="", response_metadata=metadata),
141
+ generation_info=generation_info or None,
142
+ )
143
+ ]
144
+ else:
145
+ generations = []
146
+
147
+ return generations
148
+
149
+
150
+ def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
151
+ """Format messages for tracing in `on_chat_model_start`.
152
+
153
+ - Update image content blocks to OpenAI Chat Completions format (backward
154
+ compatibility).
155
+ - Add `type` key to content blocks that have a single key.
156
+
157
+ Args:
158
+ messages: List of messages to format.
159
+
160
+ Returns:
161
+ List of messages formatted for tracing.
162
+
163
+ """
164
+ messages_to_trace = []
165
+ for message in messages:
166
+ message_to_trace = message
167
+ if isinstance(message.content, list):
168
+ for idx, block in enumerate(message.content):
169
+ if isinstance(block, dict):
170
+ # Update image content blocks to OpenAI # Chat Completions format.
171
+ if (
172
+ block.get("type") == "image"
173
+ and is_data_content_block(block)
174
+ and not ("file_id" in block or block.get("source_type") == "id")
175
+ ):
176
+ if message_to_trace is message:
177
+ # Shallow copy
178
+ message_to_trace = message.model_copy()
179
+ message_to_trace.content = list(message_to_trace.content)
180
+
181
+ message_to_trace.content[idx] = ( # type: ignore[index] # mypy confused by .model_copy
182
+ convert_to_openai_image_block(block)
183
+ )
184
+ elif (
185
+ block.get("type") == "file"
186
+ and is_data_content_block(block) # v0 (image/audio/file) or v1
187
+ and "base64" in block
188
+ # Backward compat: convert v1 base64 blocks to v0
189
+ ):
190
+ if message_to_trace is message:
191
+ # Shallow copy
192
+ message_to_trace = message.model_copy()
193
+ message_to_trace.content = list(message_to_trace.content)
194
+
195
+ message_to_trace.content[idx] = { # type: ignore[index]
196
+ **{k: v for k, v in block.items() if k != "base64"},
197
+ "data": block["base64"],
198
+ "source_type": "base64",
199
+ }
200
+ elif len(block) == 1 and "type" not in block:
201
+ # Tracing assumes all content blocks have a "type" key. Here
202
+ # we add this key if it is missing, and there's an obvious
203
+ # choice for the type (e.g., a single key in the block).
204
+ if message_to_trace is message:
205
+ # Shallow copy
206
+ message_to_trace = message.model_copy()
207
+ message_to_trace.content = list(message_to_trace.content)
208
+ key = next(iter(block))
209
+ message_to_trace.content[idx] = { # type: ignore[index]
210
+ "type": key,
211
+ key: block[key],
212
+ }
213
+ messages_to_trace.append(message_to_trace)
214
+
215
+ return messages_to_trace
216
+
217
+
218
+ def generate_from_stream(stream: Iterator[ChatGenerationChunk]) -> ChatResult:
219
+ """Generate from a stream.
220
+
221
+ Args:
222
+ stream: Iterator of `ChatGenerationChunk`.
223
+
224
+ Raises:
225
+ ValueError: If no generations are found in the stream.
226
+
227
+ Returns:
228
+ Chat result.
229
+
230
+ """
231
+ generation = next(stream, None)
232
+ if generation:
233
+ generation += list(stream)
234
+ if generation is None:
235
+ msg = "No generations found in stream."
236
+ raise ValueError(msg)
237
+ return ChatResult(
238
+ generations=[
239
+ ChatGeneration(
240
+ message=message_chunk_to_message(generation.message),
241
+ generation_info=generation.generation_info,
242
+ )
243
+ ]
244
+ )
245
+
246
+
247
+ async def agenerate_from_stream(
248
+ stream: AsyncIterator[ChatGenerationChunk],
249
+ ) -> ChatResult:
250
+ """Async generate from a stream.
251
+
252
+ Args:
253
+ stream: AsyncIterator of `ChatGenerationChunk`.
254
+
255
+ Returns:
256
+ Chat result.
257
+
258
+ """
259
+ chunks = [chunk async for chunk in stream]
260
+ return await run_in_executor(None, generate_from_stream, iter(chunks))
261
+
262
+
263
+ def _format_ls_structured_output(
264
+ ls_structured_output_format: dict[str, Any] | None,
265
+ ) -> dict[str, Any]:
266
+ if ls_structured_output_format:
267
+ try:
268
+ ls_structured_output_format_dict = {
269
+ "ls_structured_output_format": {
270
+ "kwargs": ls_structured_output_format.get("kwargs", {}),
271
+ "schema": convert_to_json_schema(
272
+ ls_structured_output_format["schema"]
273
+ ),
274
+ }
275
+ }
276
+ except ValueError:
277
+ ls_structured_output_format_dict = {}
278
+ else:
279
+ ls_structured_output_format_dict = {}
280
+
281
+ return ls_structured_output_format_dict
282
+
283
+
284
+ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
285
+ r"""Base class for chat models.
286
+
287
+ Key imperative methods:
288
+ Methods that actually call the underlying model.
289
+
290
+ This table provides a brief overview of the main imperative methods. Please see the base `Runnable` reference for full documentation.
291
+
292
+ | Method | Input | Output | Description |
293
+ | ---------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------- |
294
+ | `invoke` | `str` \| `list[dict | tuple | BaseMessage]` \| `PromptValue` | `BaseMessage` | A single chat model call. |
295
+ | `ainvoke` | `'''` | `BaseMessage` | Defaults to running `invoke` in an async executor. |
296
+ | `stream` | `'''` | `Iterator[BaseMessageChunk]` | Defaults to yielding output of `invoke`. |
297
+ | `astream` | `'''` | `AsyncIterator[BaseMessageChunk]` | Defaults to yielding output of `ainvoke`. |
298
+ | `astream_events` | `'''` | `AsyncIterator[StreamEvent]` | Event types: `on_chat_model_start`, `on_chat_model_stream`, `on_chat_model_end`. |
299
+ | `batch` | `list[''']` | `list[BaseMessage]` | Defaults to running `invoke` in concurrent threads. |
300
+ | `abatch` | `list[''']` | `list[BaseMessage]` | Defaults to running `ainvoke` in concurrent threads. |
301
+ | `batch_as_completed` | `list[''']` | `Iterator[tuple[int, Union[BaseMessage, Exception]]]` | Defaults to running `invoke` in concurrent threads. |
302
+ | `abatch_as_completed` | `list[''']` | `AsyncIterator[tuple[int, Union[BaseMessage, Exception]]]` | Defaults to running `ainvoke` in concurrent threads. |
303
+
304
+ Key declarative methods:
305
+ Methods for creating another `Runnable` using the chat model.
306
+
307
+ This table provides a brief overview of the main declarative methods. Please see the reference for each method for full documentation.
308
+
309
+ | Method | Description |
310
+ | ---------------------------- | ------------------------------------------------------------------------------------------ |
311
+ | `bind_tools` | Create chat model that can call tools. |
312
+ | `with_structured_output` | Create wrapper that structures model output using schema. |
313
+ | `with_retry` | Create wrapper that retries model calls on failure. |
314
+ | `with_fallbacks` | Create wrapper that falls back to other models on failure. |
315
+ | `configurable_fields` | Specify init args of the model that can be configured at runtime via the `RunnableConfig`. |
316
+ | `configurable_alternatives` | Specify alternative models which can be swapped in at runtime via the `RunnableConfig`. |
317
+
318
+ Creating custom chat model:
319
+ Custom chat model implementations should inherit from this class.
320
+ Please reference the table below for information about which
321
+ methods and properties are required or optional for implementations.
322
+
323
+ | Method/Property | Description | Required |
324
+ | -------------------------------- | ------------------------------------------------------------------ | ----------------- |
325
+ | `_generate` | Use to generate a chat result from a prompt | Required |
326
+ | `_llm_type` (property) | Used to uniquely identify the type of the model. Used for logging. | Required |
327
+ | `_identifying_params` (property) | Represent model parameterization for tracing purposes. | Optional |
328
+ | `_stream` | Use to implement streaming | Optional |
329
+ | `_agenerate` | Use to implement a native async method | Optional |
330
+ | `_astream` | Use to implement async version of `_stream` | Optional |
331
+
332
+ """ # noqa: E501
333
+
334
+ rate_limiter: BaseRateLimiter | None = Field(default=None, exclude=True)
335
+ "An optional rate limiter to use for limiting the number of requests."
336
+
337
+ disable_streaming: bool | Literal["tool_calling"] = False
338
+ """Whether to disable streaming for this model.
339
+
340
+ If streaming is bypassed, then `stream`/`astream`/`astream_events` will
341
+ defer to `invoke`/`ainvoke`.
342
+
343
+ - If `True`, will always bypass streaming case.
344
+ - If `'tool_calling'`, will bypass streaming case only when the model is called
345
+ with a `tools` keyword argument. In other words, Coco will automatically
346
+ switch to non-streaming behavior (`invoke`) only when the tools argument is
347
+ provided. This offers the best of both worlds.
348
+ - If `False` (Default), will always use streaming case if available.
349
+
350
+ The main reason for this flag is that code might be written using `stream` and
351
+ a user may want to swap out a given model for another model whose implementation
352
+ does not properly support streaming.
353
+ """
354
+
355
+ output_version: str | None = Field(
356
+ default_factory=from_env("LC_OUTPUT_VERSION", default=None)
357
+ )
358
+ """Version of `AIMessage` output format to store in message content.
359
+
360
+ `AIMessage.content_blocks` will lazily parse the contents of `content` into a
361
+ standard format. This flag can be used to additionally store the standard format
362
+ in message content, e.g., for serialization purposes.
363
+
364
+ Supported values:
365
+
366
+ - `'v0'`: provider-specific format in content (can lazily-parse with
367
+ `content_blocks`)
368
+ - `'v1'`: standardized format in content (consistent with `content_blocks`)
369
+
370
+ Partner packages (e.g.,
371
+ [`trustcoco-openai`](https://pypi.org/project/langchain-openai)) can also use this
372
+ field to roll out new content formats in a backward-compatible way.
373
+
374
+ !!! version-added "Added in `trustcoco-core` 1.0.0"
375
+
376
+ """
377
+
378
+ profile: ModelProfile | None = Field(default=None, exclude=True)
379
+ """Profile detailing model capabilities.
380
+
381
+ !!! warning "Beta feature"
382
+
383
+ This is a beta feature. The format of model profiles is subject to change.
384
+
385
+ If not specified, automatically loaded from the provider package on initialization
386
+ if data is available.
387
+
388
+ Example profile data includes context window sizes, supported modalities, or support
389
+ for tool calling, structured output, and other features.
390
+
391
+ !!! version-added "Added in `trustcoco-core` 1.1.0"
392
+ """
393
+
394
+ model_config = ConfigDict(
395
+ arbitrary_types_allowed=True,
396
+ )
397
+
398
+ def _resolve_model_profile(self) -> ModelProfile | None:
399
+ """Return the default model profile, or `None` if unavailable.
400
+
401
+ Override this in subclasses instead of `_set_model_profile`. The base
402
+ validator calls it automatically and handles assignment. This avoids
403
+ coupling partner code to Pydantic validator mechanics.
404
+
405
+ Each partner needs its own override because things can vary per-partner,
406
+ such as the attribute that identifies the model (e.g., `model`,
407
+ `model_name`, `model_id`, `deployment_name`) and the partner-local
408
+ `_get_default_model_profile` function that reads from each partner's own
409
+ profile data.
410
+ """
411
+ # TODO: consider adding a `_model_identifier` property on BaseChatModel
412
+ # to standardize how partners identify their model, which could allow a
413
+ # default implementation here that calls a shared
414
+ # profile-loading mechanism.
415
+ return None
416
+
417
+ @model_validator(mode="after")
418
+ def _set_model_profile(self) -> Self:
419
+ """Populate `profile` from `_resolve_model_profile` if not provided.
420
+
421
+ Partners should override `_resolve_model_profile` rather than this
422
+ validator. Overriding this with a new `@model_validator` replaces the
423
+ base validator (Pydantic v2 behavior), bypassing the standard resolution
424
+ path. A plain method override does not prevent the base validator from
425
+ running.
426
+ """
427
+ if self.profile is None:
428
+ # Suppress errors from partner overrides (e.g., missing profile
429
+ # files, broken imports) so model construction never fails over an
430
+ # optional field.
431
+ with contextlib.suppress(Exception):
432
+ self.profile = self._resolve_model_profile()
433
+ return self
434
+
435
+ # NOTE: _check_profile_keys must be defined AFTER _set_model_profile.
436
+ # Pydantic v2 runs mode="after" validators in definition order.
437
+ @model_validator(mode="after")
438
+ def _check_profile_keys(self) -> Self:
439
+ """Warn on unrecognized profile keys."""
440
+ # isinstance guard: ModelProfile is a TypedDict (always a dict), but
441
+ # protects against unexpected types from partner overrides.
442
+ if self.profile and isinstance(self.profile, dict):
443
+ _warn_unknown_profile_keys(self.profile)
444
+ return self
445
+
446
+ @cached_property
447
+ def _serialized(self) -> builtins.dict[str, Any]:
448
+ # self is always a Serializable object in this case, thus the result is
449
+ # guaranteed to be a dict since dumpd uses the default callback, which uses
450
+ # obj.to_json which always returns TypedDict subclasses
451
+ return cast("builtins.dict[str, Any]", dumpd(self))
452
+
453
+ # --- Runnable methods ---
454
+
455
+ @property
456
+ @override
457
+ def OutputType(self) -> Any:
458
+ """Get the output type for this `Runnable`."""
459
+ return AnyMessage
460
+
461
+ def _convert_input(self, model_input: LanguageModelInput) -> PromptValue:
462
+ if isinstance(model_input, PromptValue):
463
+ return model_input
464
+ if isinstance(model_input, str):
465
+ return StringPromptValue(text=model_input)
466
+ if isinstance(model_input, Sequence):
467
+ return ChatPromptValue(messages=convert_to_messages(model_input))
468
+ msg = ( # type: ignore[unreachable]
469
+ f"Invalid input type {type(model_input)}. "
470
+ "Must be a PromptValue, str, or list of BaseMessages."
471
+ )
472
+ raise ValueError(msg)
473
+
474
+ @override
475
+ def invoke(
476
+ self,
477
+ input: LanguageModelInput,
478
+ config: RunnableConfig | None = None,
479
+ *,
480
+ stop: list[str] | None = None,
481
+ **kwargs: Any,
482
+ ) -> AIMessage:
483
+ config = ensure_config(config)
484
+ return cast(
485
+ "AIMessage",
486
+ cast(
487
+ "ChatGeneration",
488
+ self.generate_prompt(
489
+ [self._convert_input(input)],
490
+ stop=stop,
491
+ callbacks=config.get("callbacks"),
492
+ tags=config.get("tags"),
493
+ metadata=config.get("metadata"),
494
+ run_name=config.get("run_name"),
495
+ run_id=config.pop("run_id", None),
496
+ **kwargs,
497
+ ).generations[0][0],
498
+ ).message,
499
+ )
500
+
501
+ @override
502
+ async def ainvoke(
503
+ self,
504
+ input: LanguageModelInput,
505
+ config: RunnableConfig | None = None,
506
+ *,
507
+ stop: list[str] | None = None,
508
+ **kwargs: Any,
509
+ ) -> AIMessage:
510
+ config = ensure_config(config)
511
+ llm_result = await self.agenerate_prompt(
512
+ [self._convert_input(input)],
513
+ stop=stop,
514
+ callbacks=config.get("callbacks"),
515
+ tags=config.get("tags"),
516
+ metadata=config.get("metadata"),
517
+ run_name=config.get("run_name"),
518
+ run_id=config.pop("run_id", None),
519
+ **kwargs,
520
+ )
521
+ return cast(
522
+ "AIMessage", cast("ChatGeneration", llm_result.generations[0][0]).message
523
+ )
524
+
525
+ def _streaming_disabled(self, **kwargs: Any) -> bool:
526
+ """Return whether streaming is hard-disabled for this call.
527
+
528
+ Shared opt-outs honored by both `_should_stream` and
529
+ `_should_use_protocol_streaming` — these override any affirmative trigger
530
+ (attached handler, `stream=True`, etc.):
531
+
532
+ - `self.disable_streaming is True`
533
+ - `self.disable_streaming == "tool_calling"` with `tools` passed
534
+ - `stream=<falsy>` in call kwargs
535
+ - `self.streaming is False` on the instance
536
+ """
537
+ if self.disable_streaming is True:
538
+ return True
539
+ # We assume tools are passed in via "tools" kwarg in all models.
540
+ if self.disable_streaming == "tool_calling" and kwargs.get("tools"):
541
+ return True
542
+ if "stream" in kwargs and not kwargs["stream"]:
543
+ return True
544
+ return (
545
+ "streaming" in self.model_fields_set
546
+ and getattr(self, "streaming", None) is False
547
+ )
548
+
549
+ def _should_stream(
550
+ self,
551
+ *,
552
+ async_api: bool,
553
+ run_manager: CallbackManagerForLLMRun
554
+ | AsyncCallbackManagerForLLMRun
555
+ | None = None,
556
+ **kwargs: Any,
557
+ ) -> bool:
558
+ """Determine if a given model call should hit the streaming API."""
559
+ sync_not_implemented = type(self)._stream == BaseChatModel._stream # noqa: SLF001
560
+ async_not_implemented = type(self)._astream == BaseChatModel._astream # noqa: SLF001
561
+
562
+ # Check if streaming is implemented.
563
+ if (not async_api) and sync_not_implemented:
564
+ return False
565
+ # Note, since async falls back to sync we check both here.
566
+ if async_api and async_not_implemented and sync_not_implemented:
567
+ return False
568
+
569
+ if self._streaming_disabled(**kwargs):
570
+ return False
571
+
572
+ # Affirmative: explicit `stream=<truthy>` kwarg.
573
+ if kwargs.get("stream"):
574
+ return True
575
+
576
+ # Affirmative: instance-level `streaming=True` attribute.
577
+ if (
578
+ "streaming" in self.model_fields_set
579
+ and getattr(self, "streaming", None) is True
580
+ ):
581
+ return True
582
+
583
+ # Affirmative: a v1 streaming callback handler is attached.
584
+ handlers = run_manager.handlers if run_manager else []
585
+ return any(isinstance(h, _StreamingCallbackHandler) for h in handlers)
586
+
587
+ def _should_use_protocol_streaming(
588
+ self,
589
+ *,
590
+ async_api: bool,
591
+ run_manager: CallbackManagerForLLMRun
592
+ | AsyncCallbackManagerForLLMRun
593
+ | None = None,
594
+ **kwargs: Any,
595
+ ) -> bool:
596
+ """Determine whether an invoke should route through the v2 event path.
597
+
598
+ Runs alongside `_should_stream` inside `_generate_with_cache` /
599
+ `_agenerate_with_cache` — after the run manager is open — and
600
+ wins over the v1 streaming branch when a handler has declared
601
+ itself a `_V2StreamingCallbackHandler`. Parallel to
602
+ `_should_stream` rather than a delegation — v1 and v2 have
603
+ disjoint affirmative triggers.
604
+
605
+ Args:
606
+ async_api: Whether the caller is on the async path.
607
+ run_manager: The active LLM run manager.
608
+ **kwargs: Call kwargs; inspected for `disable_streaming`
609
+ semantics and an explicit `stream=False` override.
610
+
611
+ Returns:
612
+ `True` if any attached handler inherits
613
+ `_V2StreamingCallbackHandler` and the model can drive the v2
614
+ event generator (natively or via the `_stream` compat
615
+ bridge).
616
+ """
617
+ # Opt-in: only route through v2 when a v2 handler is attached.
618
+ handlers = run_manager.handlers if run_manager else []
619
+ if not any(isinstance(h, _V2StreamingCallbackHandler) for h in handlers):
620
+ return False
621
+
622
+ # Need a source of v2 events on the requested flavor. A native
623
+ # `_(a)stream_chat_model_events` hook bypasses the bridge;
624
+ # otherwise the bridge wraps `_stream` / `_astream`. Async can
625
+ # fall back to sync.
626
+ #
627
+ # `cls._stream is not BaseChatModel._stream` is an identity
628
+ # check for "subclass overrode `_stream`" — same pattern as
629
+ # `_should_stream`.
630
+ cls = type(self)
631
+ has_native_sync = getattr(cls, "_stream_chat_model_events", None) is not None
632
+ has_native_async = getattr(cls, "_astream_chat_model_events", None) is not None
633
+ overrides_sync = cls._stream is not BaseChatModel._stream
634
+ overrides_async = cls._astream is not BaseChatModel._astream
635
+ has_sync_source = has_native_sync or overrides_sync
636
+ has_async_source = has_native_async or overrides_async
637
+ has_source = (
638
+ (has_sync_source or has_async_source) if async_api else has_sync_source
639
+ )
640
+ if not has_source:
641
+ return False
642
+
643
+ return not self._streaming_disabled(**kwargs)
644
+
645
+ def _iter_v2_events(
646
+ self,
647
+ messages: list[BaseMessage],
648
+ *,
649
+ run_manager: CallbackManagerForLLMRun,
650
+ stream: ChatModelStream,
651
+ stop: list[str] | None = None,
652
+ **kwargs: Any,
653
+ ) -> Iterator[MessagesData]:
654
+ """Drive the v2 event generator with per-event dispatch.
655
+
656
+ Shared between the `stream_events(version="v3")` pump and the
657
+ invoke-time v2 branch in `_generate_with_cache`. Picks the native
658
+ `_stream_chat_model_events` hook when the subclass provides one,
659
+ else bridges `_stream` chunks via `chunks_to_events`. Each event
660
+ is dispatched into `stream` and fired as `on_stream_event` on
661
+ the run manager. Run-lifecycle callbacks
662
+ (`on_chat_model_start` / `on_llm_end` / `on_llm_error`) and
663
+ rate-limiter acquisition are the caller's responsibility.
664
+
665
+ Args:
666
+ messages: Normalized input messages.
667
+ run_manager: Active LLM run manager; receives
668
+ `on_stream_event` per event.
669
+ stream: Accumulator owned by the caller; receives each
670
+ event via `stream.dispatch`.
671
+ stop: Optional stop sequences.
672
+ **kwargs: Forwarded to the event producer.
673
+
674
+ Yields:
675
+ Each protocol event produced by the model.
676
+ """
677
+ native = cast(
678
+ "Callable[..., Iterator[MessagesData]] | None",
679
+ getattr(self, "_stream_chat_model_events", None),
680
+ )
681
+ if native is not None:
682
+ event_iter: Iterator[MessagesData] = native(
683
+ messages, stop=stop, run_manager=run_manager, **kwargs
684
+ )
685
+ else:
686
+ event_iter = chunks_to_events(
687
+ self._stream(messages, stop=stop, run_manager=run_manager, **kwargs),
688
+ message_id=stream.message_id,
689
+ )
690
+ for event in event_iter:
691
+ stream.dispatch(event)
692
+ run_manager.on_stream_event(event)
693
+ yield event
694
+
695
+ async def _aiter_v2_events(
696
+ self,
697
+ messages: list[BaseMessage],
698
+ *,
699
+ run_manager: AsyncCallbackManagerForLLMRun,
700
+ stream: AsyncChatModelStream,
701
+ stop: list[str] | None = None,
702
+ **kwargs: Any,
703
+ ) -> AsyncIterator[MessagesData]:
704
+ """Async counterpart to `_iter_v2_events`.
705
+
706
+ See `_iter_v2_events` for the shared contract.
707
+ """
708
+ native = cast(
709
+ "Callable[..., AsyncIterator[MessagesData]] | None",
710
+ getattr(self, "_astream_chat_model_events", None),
711
+ )
712
+ if native is not None:
713
+ event_iter: AsyncIterator[MessagesData] = native(
714
+ messages, stop=stop, run_manager=run_manager, **kwargs
715
+ )
716
+ else:
717
+ event_iter = achunks_to_events(
718
+ self._astream(messages, stop=stop, run_manager=run_manager, **kwargs),
719
+ message_id=stream.message_id,
720
+ )
721
+ async for event in event_iter:
722
+ stream.dispatch(event)
723
+ await run_manager.on_stream_event(event)
724
+ yield event
725
+
726
+ @override
727
+ def stream(
728
+ self,
729
+ input: LanguageModelInput,
730
+ config: RunnableConfig | None = None,
731
+ *,
732
+ stop: list[str] | None = None,
733
+ **kwargs: Any,
734
+ ) -> Iterator[AIMessageChunk]:
735
+ if not self._should_stream(async_api=False, **{**kwargs, "stream": True}):
736
+ # Model doesn't implement streaming, so use default implementation
737
+ yield cast(
738
+ "AIMessageChunk",
739
+ self.invoke(input, config=config, stop=stop, **kwargs),
740
+ )
741
+ else:
742
+ config = ensure_config(config)
743
+ messages = self._convert_input(input).to_messages()
744
+ ls_structured_output_format = kwargs.pop(
745
+ "ls_structured_output_format", None
746
+ ) or kwargs.pop("structured_output_format", None)
747
+ ls_structured_output_format_dict = _format_ls_structured_output(
748
+ ls_structured_output_format
749
+ )
750
+
751
+ params = self._get_invocation_params(stop=stop, **kwargs)
752
+ options = {
753
+ "stop": stop,
754
+ **{key: params[key] for key in kwargs if key in params},
755
+ **ls_structured_output_format_dict,
756
+ }
757
+ inheritable_metadata = {
758
+ **(config.get("metadata") or {}),
759
+ **self._get_ls_params_with_defaults(stop=stop, **kwargs),
760
+ }
761
+ callback_manager = CallbackManager.configure(
762
+ config.get("callbacks"),
763
+ self.callbacks,
764
+ self.verbose,
765
+ config.get("tags"),
766
+ self.tags,
767
+ inheritable_metadata,
768
+ self.metadata,
769
+ langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
770
+ params
771
+ ),
772
+ )
773
+ (run_manager,) = callback_manager.on_chat_model_start(
774
+ self._serialized,
775
+ [_format_for_tracing(messages)],
776
+ invocation_params=params,
777
+ options=options,
778
+ name=config.get("run_name"),
779
+ run_id=config.pop("run_id", None),
780
+ batch_size=1,
781
+ )
782
+
783
+ chunks: list[ChatGenerationChunk] = []
784
+
785
+ if self.rate_limiter:
786
+ self.rate_limiter.acquire(blocking=True)
787
+
788
+ try:
789
+ input_messages = _normalize_messages(messages)
790
+ run_id = "-".join((LC_ID_PREFIX, str(run_manager.run_id)))
791
+ yielded = False
792
+ index = -1
793
+ index_type = ""
794
+ for chunk in self._stream(input_messages, stop=stop, **kwargs):
795
+ if chunk.message.id is None:
796
+ chunk.message.id = run_id
797
+ chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
798
+ if self.output_version == "v1":
799
+ # Overwrite .content with .content_blocks
800
+ chunk.message = _update_message_content_to_blocks(
801
+ chunk.message, "v1"
802
+ )
803
+ for block in cast(
804
+ "list[types.ContentBlock]", chunk.message.content
805
+ ):
806
+ if block["type"] != index_type:
807
+ index_type = block["type"]
808
+ index += 1
809
+ if "index" not in block:
810
+ block["index"] = index
811
+ run_manager.on_llm_new_token(chunk.message.content, chunk=chunk)
812
+ chunks.append(chunk)
813
+ yield cast("AIMessageChunk", chunk.message)
814
+ yielded = True
815
+
816
+ # Yield a final empty chunk with chunk_position="last" if not yet
817
+ # yielded
818
+ if (
819
+ yielded
820
+ and isinstance(chunk.message, AIMessageChunk)
821
+ and not chunk.message.chunk_position
822
+ ):
823
+ empty_content: str | list[str | dict[str, Any]] = (
824
+ "" if isinstance(chunk.message.content, str) else []
825
+ )
826
+ msg_chunk = AIMessageChunk(
827
+ content=empty_content, chunk_position="last", id=run_id
828
+ )
829
+ run_manager.on_llm_new_token(
830
+ "", chunk=ChatGenerationChunk(message=msg_chunk)
831
+ )
832
+ yield msg_chunk
833
+ except BaseException as e:
834
+ generations_with_error_metadata = _generate_response_from_error(e)
835
+ chat_generation_chunk = merge_chat_generation_chunks(chunks)
836
+ if chat_generation_chunk:
837
+ generations = [
838
+ [chat_generation_chunk],
839
+ generations_with_error_metadata,
840
+ ]
841
+ else:
842
+ generations = [generations_with_error_metadata]
843
+ run_manager.on_llm_error(
844
+ e,
845
+ response=LLMResult(generations=generations),
846
+ )
847
+ raise
848
+
849
+ generation = merge_chat_generation_chunks(chunks)
850
+ if generation is None:
851
+ err = ValueError("No generation chunks were returned")
852
+ run_manager.on_llm_error(err, response=LLMResult(generations=[]))
853
+ raise err
854
+
855
+ run_manager.on_llm_end(LLMResult(generations=[[generation]]))
856
+
857
+ @override
858
+ async def astream(
859
+ self,
860
+ input: LanguageModelInput,
861
+ config: RunnableConfig | None = None,
862
+ *,
863
+ stop: list[str] | None = None,
864
+ **kwargs: Any,
865
+ ) -> AsyncIterator[AIMessageChunk]:
866
+ if not self._should_stream(async_api=True, **{**kwargs, "stream": True}):
867
+ # No async or sync stream is implemented, so fall back to ainvoke
868
+ yield cast(
869
+ "AIMessageChunk",
870
+ await self.ainvoke(input, config=config, stop=stop, **kwargs),
871
+ )
872
+ return
873
+
874
+ config = ensure_config(config)
875
+ messages = self._convert_input(input).to_messages()
876
+
877
+ ls_structured_output_format = kwargs.pop(
878
+ "ls_structured_output_format", None
879
+ ) or kwargs.pop("structured_output_format", None)
880
+ ls_structured_output_format_dict = _format_ls_structured_output(
881
+ ls_structured_output_format
882
+ )
883
+
884
+ params = self._get_invocation_params(stop=stop, **kwargs)
885
+ options = {
886
+ "stop": stop,
887
+ **{key: params[key] for key in kwargs if key in params},
888
+ **ls_structured_output_format_dict,
889
+ }
890
+ inheritable_metadata = {
891
+ **(config.get("metadata") or {}),
892
+ **self._get_ls_params_with_defaults(stop=stop, **kwargs),
893
+ }
894
+ callback_manager = AsyncCallbackManager.configure(
895
+ config.get("callbacks"),
896
+ self.callbacks,
897
+ self.verbose,
898
+ config.get("tags"),
899
+ self.tags,
900
+ inheritable_metadata,
901
+ self.metadata,
902
+ langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
903
+ params
904
+ ),
905
+ )
906
+ (run_manager,) = await callback_manager.on_chat_model_start(
907
+ self._serialized,
908
+ [_format_for_tracing(messages)],
909
+ invocation_params=params,
910
+ options=options,
911
+ name=config.get("run_name"),
912
+ run_id=config.pop("run_id", None),
913
+ batch_size=1,
914
+ )
915
+
916
+ if self.rate_limiter:
917
+ await self.rate_limiter.aacquire(blocking=True)
918
+
919
+ chunks: list[ChatGenerationChunk] = []
920
+
921
+ try:
922
+ input_messages = _normalize_messages(messages)
923
+ run_id = "-".join((LC_ID_PREFIX, str(run_manager.run_id)))
924
+ yielded = False
925
+ index = -1
926
+ index_type = ""
927
+ async for chunk in self._astream(
928
+ input_messages,
929
+ stop=stop,
930
+ **kwargs,
931
+ ):
932
+ if chunk.message.id is None:
933
+ chunk.message.id = run_id
934
+ chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
935
+ if self.output_version == "v1":
936
+ # Overwrite .content with .content_blocks
937
+ chunk.message = _update_message_content_to_blocks(
938
+ chunk.message, "v1"
939
+ )
940
+ for block in cast(
941
+ "list[types.ContentBlock]", chunk.message.content
942
+ ):
943
+ if block["type"] != index_type:
944
+ index_type = block["type"]
945
+ index += 1
946
+ if "index" not in block:
947
+ block["index"] = index
948
+ await run_manager.on_llm_new_token(chunk.message.content, chunk=chunk)
949
+ chunks.append(chunk)
950
+ yield cast("AIMessageChunk", chunk.message)
951
+ yielded = True
952
+
953
+ # Yield a final empty chunk with chunk_position="last" if not yet yielded
954
+ if (
955
+ yielded
956
+ and isinstance(chunk.message, AIMessageChunk)
957
+ and not chunk.message.chunk_position
958
+ ):
959
+ empty_content: str | list[str | dict[str, Any]] = (
960
+ "" if isinstance(chunk.message.content, str) else []
961
+ )
962
+ msg_chunk = AIMessageChunk(
963
+ content=empty_content, chunk_position="last", id=run_id
964
+ )
965
+ await run_manager.on_llm_new_token(
966
+ "", chunk=ChatGenerationChunk(message=msg_chunk)
967
+ )
968
+ yield msg_chunk
969
+ except BaseException as e:
970
+ generations_with_error_metadata = _generate_response_from_error(e)
971
+ chat_generation_chunk = merge_chat_generation_chunks(chunks)
972
+ if chat_generation_chunk:
973
+ generations = [[chat_generation_chunk], generations_with_error_metadata]
974
+ else:
975
+ generations = [generations_with_error_metadata]
976
+ await run_manager.on_llm_error(
977
+ e,
978
+ response=LLMResult(generations=generations),
979
+ )
980
+ raise
981
+
982
+ generation = merge_chat_generation_chunks(chunks)
983
+ if not generation:
984
+ err = ValueError("No generation chunks were returned")
985
+ await run_manager.on_llm_error(err, response=LLMResult(generations=[]))
986
+ raise err
987
+
988
+ await run_manager.on_llm_end(
989
+ LLMResult(generations=[[generation]]),
990
+ )
991
+
992
+ # --- stream_events v3 ---
993
+
994
+ @beta()
995
+ def _chat_model_stream_v3(
996
+ self,
997
+ input: LanguageModelInput,
998
+ config: RunnableConfig | None = None,
999
+ *,
1000
+ stop: list[str] | None = None,
1001
+ **kwargs: Any,
1002
+ ) -> ChatModelStream:
1003
+ """Internal v3 sync streaming implementation.
1004
+
1005
+ Public entry point: `stream_events(version='v3')`.
1006
+ """
1007
+ config = ensure_config(config)
1008
+ messages = self._convert_input(input).to_messages()
1009
+ input_messages = _normalize_messages(messages)
1010
+
1011
+ # Strip tracing-only kwargs before forwarding to `_stream` — matches
1012
+ # `stream()` / `astream()`. Provider clients reject unknown kwargs,
1013
+ # so `.with_structured_output().stream_events(version="v3", ...)`
1014
+ # and any other binding that carries `ls_structured_output_format`
1015
+ # / `structured_output_format` would raise without this pop.
1016
+ ls_structured_output_format = kwargs.pop(
1017
+ "ls_structured_output_format", None
1018
+ ) or kwargs.pop("structured_output_format", None)
1019
+ ls_structured_output_format_dict = _format_ls_structured_output(
1020
+ ls_structured_output_format
1021
+ )
1022
+
1023
+ params = self._get_invocation_params(stop=stop, **kwargs)
1024
+ options = {
1025
+ "stop": stop,
1026
+ **{key: params[key] for key in kwargs if key in params},
1027
+ **ls_structured_output_format_dict,
1028
+ }
1029
+ inheritable_metadata = {
1030
+ **(config.get("metadata") or {}),
1031
+ **self._get_ls_params_with_defaults(stop=stop, **kwargs),
1032
+ }
1033
+ callback_manager = CallbackManager.configure(
1034
+ config.get("callbacks"),
1035
+ self.callbacks,
1036
+ self.verbose,
1037
+ config.get("tags"),
1038
+ self.tags,
1039
+ inheritable_metadata,
1040
+ self.metadata,
1041
+ langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
1042
+ params
1043
+ ),
1044
+ )
1045
+ stream = ChatModelStream()
1046
+ run_manager: CallbackManagerForLLMRun | None = None
1047
+ event_iter_ref: Iterator[MessagesData] | None = None
1048
+ rate_limiter_acquired = self.rate_limiter is None
1049
+ run_name = config.get("run_name")
1050
+ run_id = config.pop("run_id", None)
1051
+
1052
+ def ensure_started() -> None:
1053
+ nonlocal event_iter_ref, run_manager
1054
+ if event_iter_ref is not None:
1055
+ return
1056
+
1057
+ (run_manager,) = callback_manager.on_chat_model_start(
1058
+ self._serialized,
1059
+ [_format_for_tracing(messages)],
1060
+ invocation_params=params,
1061
+ options=options,
1062
+ name=run_name,
1063
+ run_id=run_id,
1064
+ batch_size=1,
1065
+ )
1066
+ stream.set_message_id("-".join((LC_ID_PREFIX, str(run_manager.run_id))))
1067
+ event_iter_ref = iter(
1068
+ self._iter_v2_events(
1069
+ input_messages,
1070
+ run_manager=run_manager,
1071
+ stream=stream,
1072
+ stop=stop,
1073
+ **kwargs,
1074
+ )
1075
+ )
1076
+
1077
+ def pump_one() -> bool:
1078
+ nonlocal rate_limiter_acquired
1079
+ ensure_started()
1080
+ if not rate_limiter_acquired:
1081
+ assert self.rate_limiter is not None # noqa: S101
1082
+ self.rate_limiter.acquire(blocking=True)
1083
+ rate_limiter_acquired = True
1084
+ assert event_iter_ref is not None # noqa: S101
1085
+ assert run_manager is not None # noqa: S101
1086
+ try:
1087
+ next(event_iter_ref)
1088
+ except StopIteration:
1089
+ if not stream.done:
1090
+ if stream.has_events:
1091
+ # Native event producers may omit the terminal
1092
+ # `message-finish`. Close the lifecycle here so
1093
+ # `on_llm_end` still observes the assembled
1094
+ # message. A truly empty stream remains an error
1095
+ # for parity with `stream()`.
1096
+ stream.dispatch(MessageFinishData(event="message-finish"))
1097
+ else:
1098
+ err = ValueError("No generation chunks were returned")
1099
+ stream.fail(err)
1100
+ run_manager.on_llm_error(
1101
+ err,
1102
+ response=LLMResult(generations=[]),
1103
+ )
1104
+ return False
1105
+ if stream.done and stream.output_message is not None:
1106
+ run_manager.on_llm_end(
1107
+ LLMResult(
1108
+ generations=[
1109
+ [ChatGeneration(message=stream.output_message)],
1110
+ ],
1111
+ ),
1112
+ )
1113
+ return False
1114
+ except BaseException as exc:
1115
+ stream.fail(exc)
1116
+ run_manager.on_llm_error(
1117
+ exc,
1118
+ response=LLMResult(generations=[]),
1119
+ )
1120
+ return False
1121
+ if stream.done and stream.output_message is not None:
1122
+ run_manager.on_llm_end(
1123
+ LLMResult(
1124
+ generations=[
1125
+ [ChatGeneration(message=stream.output_message)],
1126
+ ],
1127
+ ),
1128
+ )
1129
+ return True
1130
+
1131
+ stream.set_start(ensure_started)
1132
+ stream.bind_pump(pump_one)
1133
+ return stream
1134
+
1135
+ @beta()
1136
+ async def _achat_model_stream_v3(
1137
+ self,
1138
+ input: LanguageModelInput,
1139
+ config: RunnableConfig | None = None,
1140
+ *,
1141
+ stop: list[str] | None = None,
1142
+ **kwargs: Any,
1143
+ ) -> AsyncChatModelStream:
1144
+ """Internal v3 async streaming implementation.
1145
+
1146
+ Public entry point: `astream_events(version='v3')`.
1147
+ """
1148
+ config = ensure_config(config)
1149
+ messages = self._convert_input(input).to_messages()
1150
+ input_messages = _normalize_messages(messages)
1151
+
1152
+ # Strip tracing-only kwargs before forwarding — see the sync v3
1153
+ # implementation for the full rationale.
1154
+ ls_structured_output_format = kwargs.pop(
1155
+ "ls_structured_output_format", None
1156
+ ) or kwargs.pop("structured_output_format", None)
1157
+ ls_structured_output_format_dict = _format_ls_structured_output(
1158
+ ls_structured_output_format
1159
+ )
1160
+
1161
+ params = self._get_invocation_params(stop=stop, **kwargs)
1162
+ options = {
1163
+ "stop": stop,
1164
+ **{key: params[key] for key in kwargs if key in params},
1165
+ **ls_structured_output_format_dict,
1166
+ }
1167
+ inheritable_metadata = {
1168
+ **(config.get("metadata") or {}),
1169
+ **self._get_ls_params_with_defaults(stop=stop, **kwargs),
1170
+ }
1171
+ callback_manager = AsyncCallbackManager.configure(
1172
+ config.get("callbacks"),
1173
+ self.callbacks,
1174
+ self.verbose,
1175
+ config.get("tags"),
1176
+ self.tags,
1177
+ inheritable_metadata,
1178
+ self.metadata,
1179
+ langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
1180
+ params
1181
+ ),
1182
+ )
1183
+ stream = AsyncChatModelStream()
1184
+ run_manager: AsyncCallbackManagerForLLMRun | None = None
1185
+ run_name = config.get("run_name")
1186
+ run_id = config.pop("run_id", None)
1187
+ start_lock = asyncio.Lock()
1188
+
1189
+ async def _produce() -> None:
1190
+ assert run_manager is not None # noqa: S101
1191
+ try:
1192
+ if self.rate_limiter:
1193
+ await self.rate_limiter.aacquire(blocking=True)
1194
+
1195
+ async for _event in self._aiter_v2_events(
1196
+ input_messages,
1197
+ run_manager=run_manager,
1198
+ stream=stream,
1199
+ stop=stop,
1200
+ **kwargs,
1201
+ ):
1202
+ pass
1203
+ if not stream.done:
1204
+ if stream.has_events:
1205
+ # Native event producers may omit the terminal
1206
+ # `message-finish`. Close the lifecycle here so
1207
+ # `on_llm_end` sees the finalized message. A
1208
+ # truly empty stream remains an error for parity
1209
+ # with `astream()`.
1210
+ stream.dispatch(MessageFinishData(event="message-finish"))
1211
+ else:
1212
+ err = ValueError("No generation chunks were returned")
1213
+ stream.fail(err)
1214
+ await run_manager.on_llm_error(
1215
+ err,
1216
+ response=LLMResult(generations=[]),
1217
+ )
1218
+ return
1219
+ if stream.done and stream.output_message is not None:
1220
+ await run_manager.on_llm_end(
1221
+ LLMResult(
1222
+ generations=[
1223
+ [ChatGeneration(message=stream.output_message)],
1224
+ ],
1225
+ ),
1226
+ )
1227
+ except asyncio.CancelledError as exc:
1228
+ stream.fail(exc)
1229
+ # Close the callback lifecycle so tracing observes a
1230
+ # matching end event for the earlier `on_chat_model_start`.
1231
+ # `on_llm_error` is `@shielded`, so the callback runs to
1232
+ # completion in the background even though the `await`
1233
+ # here re-raises our cancellation.
1234
+ with contextlib.suppress(Exception):
1235
+ await run_manager.on_llm_error(
1236
+ exc,
1237
+ response=LLMResult(generations=[]),
1238
+ )
1239
+ raise
1240
+ except BaseException as exc:
1241
+ stream.fail(exc)
1242
+ await run_manager.on_llm_error(
1243
+ exc,
1244
+ response=LLMResult(generations=[]),
1245
+ )
1246
+
1247
+ async def ensure_started() -> None:
1248
+ nonlocal run_manager
1249
+ if stream._producer_task is not None: # noqa: SLF001
1250
+ return
1251
+
1252
+ async with start_lock:
1253
+ if stream._producer_task is not None: # noqa: SLF001
1254
+ return # type: ignore[unreachable]
1255
+
1256
+ (run_manager,) = await callback_manager.on_chat_model_start(
1257
+ self._serialized,
1258
+ [_format_for_tracing(messages)],
1259
+ invocation_params=params,
1260
+ options=options,
1261
+ name=run_name,
1262
+ run_id=run_id,
1263
+ batch_size=1,
1264
+ )
1265
+ stream.set_message_id("-".join((LC_ID_PREFIX, str(run_manager.run_id))))
1266
+ stream._producer_task = asyncio.get_running_loop().create_task( # noqa: SLF001
1267
+ _produce()
1268
+ )
1269
+
1270
+ async def _on_aclose_fail(exc: BaseException) -> None:
1271
+ assert run_manager is not None # noqa: S101
1272
+ # Invoked by `stream.aclose()` only when the producer was
1273
+ # cancelled before `_produce` ran — so `on_llm_error` from
1274
+ # the CancelledError handler never fired. Shielded by the
1275
+ # callback manager; runs to completion even if our caller
1276
+ # is being cancelled.
1277
+ await run_manager.on_llm_error(
1278
+ exc,
1279
+ response=LLMResult(generations=[]),
1280
+ )
1281
+
1282
+ stream.set_start(ensure_started)
1283
+ stream._on_aclose_fail = _on_aclose_fail # noqa: SLF001
1284
+ return stream
1285
+
1286
+ @overload # type: ignore[override]
1287
+ def stream_events(
1288
+ self,
1289
+ input: LanguageModelInput,
1290
+ config: RunnableConfig | None = None,
1291
+ *,
1292
+ version: Literal["v1", "v2"] = "v2",
1293
+ **kwargs: Any,
1294
+ ) -> Iterator[StreamEvent]: ...
1295
+
1296
+ @overload
1297
+ def stream_events(
1298
+ self,
1299
+ input: LanguageModelInput,
1300
+ config: RunnableConfig | None = None,
1301
+ *,
1302
+ version: Literal["v3"],
1303
+ stop: list[str] | None = None,
1304
+ **kwargs: Any,
1305
+ ) -> ChatModelStream: ...
1306
+
1307
+ def stream_events(
1308
+ self,
1309
+ input: LanguageModelInput,
1310
+ config: RunnableConfig | None = None,
1311
+ *,
1312
+ version: Literal["v1", "v2", "v3"] = "v2",
1313
+ stop: list[str] | None = None,
1314
+ **kwargs: Any,
1315
+ ) -> Iterator[StreamEvent] | ChatModelStream:
1316
+ """Stream events from this chat model.
1317
+
1318
+ For `version="v1"` / `"v2"`, yields `StreamEvent` dicts (see
1319
+ `Runnable.stream_events`). For `version="v3"`, returns a
1320
+ `ChatModelStream` exposing typed projections (`.text`,
1321
+ `.reasoning`, `.tool_calls`, `.output`).
1322
+
1323
+ !!! warning "Beta"
1324
+
1325
+ `version="v3"` is in beta. The protocol shape, return type,
1326
+ and surface area may change in future releases. Calling it
1327
+ emits a `CocoBetaWarning` at runtime.
1328
+
1329
+ !!! note "v3 always produces v1-shaped content"
1330
+
1331
+ `ChatModelStream.output.content` is always a list of v1
1332
+ content blocks (text / reasoning / tool_call / image / …),
1333
+ regardless of the model's `output_version` attribute. The
1334
+ setting only affects the legacy `stream()` / `astream()` /
1335
+ `invoke()` paths. If you're mixing
1336
+ `stream_events(version="v3")` with those paths in the same
1337
+ pipeline and need a consistent output shape across them,
1338
+ set `output_version="v1"` on the model.
1339
+
1340
+ Args:
1341
+ input: The model input.
1342
+ config: Optional runnable config.
1343
+ version: Streaming-event schema version. `"v3"` selects the
1344
+ content-block-centric streaming protocol.
1345
+ stop: Optional stop sequences. Only used for `version="v3"`;
1346
+ ignored otherwise.
1347
+ **kwargs: Additional keyword arguments. For `version="v3"`,
1348
+ forwarded to the model.
1349
+
1350
+ Returns:
1351
+ For `version="v3"`, a `ChatModelStream` with typed
1352
+ projections. Otherwise an `Iterator[StreamEvent]`.
1353
+ """
1354
+ if version == "v3":
1355
+ return self._chat_model_stream_v3(input, config, stop=stop, **kwargs)
1356
+ return super().stream_events(
1357
+ input, config, version=version, stop=stop, **kwargs
1358
+ )
1359
+
1360
+ @overload
1361
+ def astream_events(
1362
+ self,
1363
+ input: LanguageModelInput,
1364
+ config: RunnableConfig | None = None,
1365
+ *,
1366
+ version: Literal["v1", "v2"] = "v2",
1367
+ **kwargs: Any,
1368
+ ) -> AsyncIterator[StreamEvent]: ...
1369
+
1370
+ @overload
1371
+ def astream_events(
1372
+ self,
1373
+ input: LanguageModelInput,
1374
+ config: RunnableConfig | None = None,
1375
+ *,
1376
+ version: Literal["v3"],
1377
+ stop: list[str] | None = None,
1378
+ **kwargs: Any,
1379
+ ) -> Awaitable[AsyncChatModelStream]: ...
1380
+
1381
+ def astream_events(
1382
+ self,
1383
+ input: LanguageModelInput,
1384
+ config: RunnableConfig | None = None,
1385
+ *,
1386
+ version: Literal["v1", "v2", "v3"] = "v2",
1387
+ stop: list[str] | None = None,
1388
+ **kwargs: Any,
1389
+ ) -> AsyncIterator[StreamEvent] | Awaitable[AsyncChatModelStream]:
1390
+ """Async variant of `stream_events`. See `stream_events` for full docs."""
1391
+ if version == "v3":
1392
+ return self._achat_model_stream_v3(input, config, stop=stop, **kwargs)
1393
+ # v1/v2: forward to Runnable.astream_events (async generator).
1394
+ return super().astream_events(
1395
+ input, config, version=version, stop=stop, **kwargs
1396
+ )
1397
+
1398
+ # --- Custom methods ---
1399
+
1400
+ def _combine_llm_outputs(
1401
+ self, _llm_outputs: list[builtins.dict[str, Any] | None], /
1402
+ ) -> builtins.dict[str, Any]:
1403
+ return {}
1404
+
1405
+ def _convert_cached_generations(
1406
+ self, cache_val: list[Generation]
1407
+ ) -> list[ChatGeneration]:
1408
+ """Convert cached Generation objects to ChatGeneration objects.
1409
+
1410
+ Handle case where cache contains Generation objects instead of
1411
+ ChatGeneration objects. This can happen due to serialization/deserialization
1412
+ issues or legacy cache data (see #22389).
1413
+
1414
+ Args:
1415
+ cache_val: List of cached generation objects.
1416
+
1417
+ Returns:
1418
+ List of ChatGeneration objects.
1419
+
1420
+ """
1421
+ converted_generations = []
1422
+ for gen in cache_val:
1423
+ if isinstance(gen, Generation) and not isinstance(gen, ChatGeneration):
1424
+ # Convert Generation to ChatGeneration by creating AIMessage
1425
+ # from the text content
1426
+ chat_gen = ChatGeneration(
1427
+ message=AIMessage(content=gen.text),
1428
+ generation_info=gen.generation_info,
1429
+ )
1430
+ converted_generations.append(chat_gen)
1431
+ else:
1432
+ # Already a ChatGeneration or other expected type
1433
+ if hasattr(gen, "message") and isinstance(gen.message, AIMessage):
1434
+ # We zero out cost on cache hits
1435
+ gen.message = gen.message.model_copy(
1436
+ update={
1437
+ "usage_metadata": {
1438
+ **(gen.message.usage_metadata or {}),
1439
+ "total_cost": 0,
1440
+ }
1441
+ }
1442
+ )
1443
+ converted_generations.append(gen)
1444
+ return converted_generations
1445
+
1446
+ def _replay_v2_events_for_cache_hit(
1447
+ self,
1448
+ generations: list[ChatGeneration],
1449
+ *,
1450
+ run_manager: CallbackManagerForLLMRun | None,
1451
+ **kwargs: Any,
1452
+ ) -> None:
1453
+ """Replay cached messages as v2 events when a v2 handler is attached.
1454
+
1455
+ A warm cache must produce the same `on_stream_event` stream as a
1456
+ cold call so CocoGraph-style consumers do not observe behavior
1457
+ that depends on cache state. Gated by
1458
+ `_should_use_protocol_streaming` so a `disable_streaming` config
1459
+ that suppresses v2 on cold calls also suppresses it here.
1460
+ """
1461
+ if run_manager is None or not self._should_use_protocol_streaming(
1462
+ async_api=False, run_manager=run_manager, **kwargs
1463
+ ):
1464
+ return
1465
+ message_id = f"{LC_ID_PREFIX}-{run_manager.run_id}"
1466
+ for gen in generations:
1467
+ msg = getattr(gen, "message", None)
1468
+ if not isinstance(msg, AIMessage):
1469
+ continue
1470
+ for event in message_to_events(msg, message_id=message_id):
1471
+ run_manager.on_stream_event(event)
1472
+
1473
+ async def _areplay_v2_events_for_cache_hit(
1474
+ self,
1475
+ generations: list[ChatGeneration],
1476
+ *,
1477
+ run_manager: AsyncCallbackManagerForLLMRun | None,
1478
+ **kwargs: Any,
1479
+ ) -> None:
1480
+ """Async counterpart to `_replay_v2_events_for_cache_hit`."""
1481
+ if run_manager is None or not self._should_use_protocol_streaming(
1482
+ async_api=True, run_manager=run_manager, **kwargs
1483
+ ):
1484
+ return
1485
+ message_id = f"{LC_ID_PREFIX}-{run_manager.run_id}"
1486
+ for gen in generations:
1487
+ msg = getattr(gen, "message", None)
1488
+ if not isinstance(msg, AIMessage):
1489
+ continue
1490
+ async for event in amessage_to_events(msg, message_id=message_id):
1491
+ await run_manager.on_stream_event(event)
1492
+
1493
+ def _get_invocation_params(
1494
+ self,
1495
+ stop: list[str] | None = None,
1496
+ **kwargs: Any,
1497
+ ) -> builtins.dict[str, Any]:
1498
+ params = self._dict_for_compat()
1499
+ params["stop"] = stop
1500
+ return {**params, **kwargs}
1501
+
1502
+ def _get_ls_params(
1503
+ self,
1504
+ stop: list[str] | None = None,
1505
+ **kwargs: Any,
1506
+ ) -> LangSmithParams:
1507
+ """Get standard params for LangSmith tracing.
1508
+
1509
+ Subclasses **should override** this method to populate `ls_provider`
1510
+ and `ls_model_name` from provider-specific attributes (e.g. `self.model`,
1511
+ `self.model_name`, `self.model_id`) and to honor per-call overrides
1512
+ passed via `kwargs["model"]` so that runtime `bind`/`invoke` model
1513
+ changes are reflected in traces.
1514
+
1515
+ The implementation here is a best-effort fallback for subclasses that
1516
+ do not override it. It is not part of a stable contract and the
1517
+ derivation rules may change:
1518
+
1519
+ - `ls_provider` is derived from the class name by stripping a leading
1520
+ or trailing `"Chat"` and lowercasing the remainder. This produces
1521
+ ugly values for multi-word providers (e.g. `ChatGoogleGenerativeAI`
1522
+ would become `"googlegenerativeai"`).
1523
+
1524
+ Override to set a stable, conventional value
1525
+ such as `"google_genai"`.
1526
+ - `ls_model_name` is resolved from `kwargs["model"]`, then
1527
+ `self.model`, then `self.model_name`.
1528
+
1529
+ Subclasses whose model attribute has a different name
1530
+ (`model_id`, `deployment_name`, ...) must override.
1531
+ """
1532
+ # get default provider from class name
1533
+ default_provider = self.__class__.__name__
1534
+ if default_provider.startswith("Chat"):
1535
+ default_provider = default_provider[4:].lower()
1536
+ elif default_provider.endswith("Chat"):
1537
+ default_provider = default_provider[:-4]
1538
+ default_provider = default_provider.lower()
1539
+
1540
+ ls_params = LangSmithParams(ls_provider=default_provider, ls_model_type="chat")
1541
+ if stop:
1542
+ ls_params["ls_stop"] = stop
1543
+
1544
+ # model
1545
+ if "model" in kwargs and isinstance(kwargs["model"], str):
1546
+ ls_params["ls_model_name"] = kwargs["model"]
1547
+ elif hasattr(self, "model") and isinstance(self.model, str):
1548
+ ls_params["ls_model_name"] = self.model
1549
+ elif hasattr(self, "model_name") and isinstance(self.model_name, str):
1550
+ ls_params["ls_model_name"] = self.model_name
1551
+
1552
+ # temperature
1553
+ if "temperature" in kwargs and isinstance(kwargs["temperature"], (int, float)):
1554
+ ls_params["ls_temperature"] = kwargs["temperature"]
1555
+ elif hasattr(self, "temperature") and isinstance(
1556
+ self.temperature, (int, float)
1557
+ ):
1558
+ ls_params["ls_temperature"] = self.temperature
1559
+
1560
+ # max_tokens
1561
+ if "max_tokens" in kwargs and isinstance(kwargs["max_tokens"], int):
1562
+ ls_params["ls_max_tokens"] = kwargs["max_tokens"]
1563
+ elif hasattr(self, "max_tokens") and isinstance(self.max_tokens, int):
1564
+ ls_params["ls_max_tokens"] = self.max_tokens
1565
+
1566
+ return ls_params
1567
+
1568
+ def _get_ls_params_with_defaults(
1569
+ self,
1570
+ stop: list[str] | None = None,
1571
+ **kwargs: Any,
1572
+ ) -> LangSmithParams:
1573
+ """Wrap _get_ls_params to always include ls_integration."""
1574
+ ls_params = self._get_ls_params(stop=stop, **kwargs)
1575
+ ls_params["ls_integration"] = "coco_chat_model"
1576
+ return ls_params
1577
+
1578
+ def _get_llm_string(self, stop: list[str] | None = None, **kwargs: Any) -> str:
1579
+ if self.is_lc_serializable():
1580
+ params = {**kwargs, "stop": stop}
1581
+ param_string = str(sorted(params.items()))
1582
+ # This code is not super efficient as it goes back and forth between
1583
+ # json and dict.
1584
+ serialized_repr = self._serialized
1585
+ _cleanup_llm_representation(serialized_repr, 1)
1586
+ llm_string = json.dumps(serialized_repr, sort_keys=True)
1587
+ return llm_string + "---" + param_string
1588
+ params = self._get_invocation_params(stop=stop, **kwargs)
1589
+ params = {**params, **kwargs}
1590
+ return str(sorted(params.items()))
1591
+
1592
+ def generate(
1593
+ self,
1594
+ messages: list[list[BaseMessage]],
1595
+ stop: list[str] | None = None,
1596
+ callbacks: Callbacks = None,
1597
+ *,
1598
+ tags: list[str] | None = None,
1599
+ metadata: builtins.dict[str, Any] | None = None,
1600
+ run_name: str | None = None,
1601
+ run_id: uuid.UUID | None = None,
1602
+ **kwargs: Any,
1603
+ ) -> LLMResult:
1604
+ """Pass a sequence of prompts to the model and return model generations.
1605
+
1606
+ This method should make use of batched calls for models that expose a batched
1607
+ API.
1608
+
1609
+ Use this method when you want to:
1610
+
1611
+ 1. Take advantage of batched calls,
1612
+ 2. Need more output from the model than just the top generated value,
1613
+ 3. Are building chains that are agnostic to the underlying language model
1614
+ type (e.g., pure text completion models vs chat models).
1615
+
1616
+ Args:
1617
+ messages: List of list of messages.
1618
+ stop: Stop words to use when generating.
1619
+
1620
+ Model output is cut off at the first occurrence of any of these
1621
+ substrings.
1622
+ callbacks: `Callbacks` to pass through.
1623
+
1624
+ Used for executing additional functionality, such as logging or
1625
+ streaming, throughout generation.
1626
+ tags: The tags to apply.
1627
+ metadata: The metadata to apply.
1628
+ run_name: The name of the run.
1629
+ run_id: The ID of the run.
1630
+ **kwargs: Arbitrary additional keyword arguments.
1631
+
1632
+ These are usually passed to the model provider API call.
1633
+
1634
+ Returns:
1635
+ An `LLMResult`, which contains a list of candidate `Generations` for each
1636
+ input prompt and additional model provider-specific output.
1637
+
1638
+ """
1639
+ ls_structured_output_format = kwargs.pop(
1640
+ "ls_structured_output_format", None
1641
+ ) or kwargs.pop("structured_output_format", None)
1642
+ ls_structured_output_format_dict = _format_ls_structured_output(
1643
+ ls_structured_output_format
1644
+ )
1645
+
1646
+ params = self._get_invocation_params(stop=stop, **kwargs)
1647
+ options = {"stop": stop, **ls_structured_output_format_dict}
1648
+ inheritable_metadata = {
1649
+ **(metadata or {}),
1650
+ **self._get_ls_params_with_defaults(stop=stop, **kwargs),
1651
+ }
1652
+
1653
+ callback_manager = CallbackManager.configure(
1654
+ callbacks,
1655
+ self.callbacks,
1656
+ self.verbose,
1657
+ tags,
1658
+ self.tags,
1659
+ inheritable_metadata,
1660
+ self.metadata,
1661
+ langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
1662
+ params
1663
+ ),
1664
+ )
1665
+ messages_to_trace = [
1666
+ _format_for_tracing(message_list) for message_list in messages
1667
+ ]
1668
+ run_managers = callback_manager.on_chat_model_start(
1669
+ self._serialized,
1670
+ messages_to_trace,
1671
+ invocation_params=params,
1672
+ options=options,
1673
+ name=run_name,
1674
+ run_id=run_id,
1675
+ batch_size=len(messages),
1676
+ )
1677
+ results = []
1678
+ input_messages = [
1679
+ _normalize_messages(message_list) for message_list in messages
1680
+ ]
1681
+ for i, m in enumerate(input_messages):
1682
+ try:
1683
+ results.append(
1684
+ self._generate_with_cache(
1685
+ m,
1686
+ stop=stop,
1687
+ run_manager=run_managers[i] if run_managers else None,
1688
+ **kwargs,
1689
+ )
1690
+ )
1691
+ except BaseException as e:
1692
+ if run_managers:
1693
+ generations_with_error_metadata = _generate_response_from_error(e)
1694
+ run_managers[i].on_llm_error(
1695
+ e,
1696
+ response=LLMResult(
1697
+ generations=[generations_with_error_metadata]
1698
+ ),
1699
+ )
1700
+ raise
1701
+ flattened_outputs = [
1702
+ LLMResult(generations=[res.generations], llm_output=res.llm_output)
1703
+ for res in results
1704
+ ]
1705
+ llm_output = self._combine_llm_outputs([res.llm_output for res in results])
1706
+ generations = [res.generations for res in results]
1707
+ output = LLMResult(generations=generations, llm_output=llm_output)
1708
+ if run_managers:
1709
+ run_infos = []
1710
+ for manager, flattened_output in zip(
1711
+ run_managers, flattened_outputs, strict=False
1712
+ ):
1713
+ manager.on_llm_end(flattened_output)
1714
+ run_infos.append(RunInfo(run_id=manager.run_id))
1715
+ output.run = run_infos
1716
+ return output
1717
+
1718
+ async def agenerate(
1719
+ self,
1720
+ messages: list[list[BaseMessage]],
1721
+ stop: list[str] | None = None,
1722
+ callbacks: Callbacks = None,
1723
+ *,
1724
+ tags: list[str] | None = None,
1725
+ metadata: builtins.dict[str, Any] | None = None,
1726
+ run_name: str | None = None,
1727
+ run_id: uuid.UUID | None = None,
1728
+ **kwargs: Any,
1729
+ ) -> LLMResult:
1730
+ """Asynchronously pass a sequence of prompts to a model and return generations.
1731
+
1732
+ This method should make use of batched calls for models that expose a batched
1733
+ API.
1734
+
1735
+ Use this method when you want to:
1736
+
1737
+ 1. Take advantage of batched calls,
1738
+ 2. Need more output from the model than just the top generated value,
1739
+ 3. Are building chains that are agnostic to the underlying language model
1740
+ type (e.g., pure text completion models vs chat models).
1741
+
1742
+ Args:
1743
+ messages: List of list of messages.
1744
+ stop: Stop words to use when generating.
1745
+
1746
+ Model output is cut off at the first occurrence of any of these
1747
+ substrings.
1748
+ callbacks: `Callbacks` to pass through.
1749
+
1750
+ Used for executing additional functionality, such as logging or
1751
+ streaming, throughout generation.
1752
+ tags: The tags to apply.
1753
+ metadata: The metadata to apply.
1754
+ run_name: The name of the run.
1755
+ run_id: The ID of the run.
1756
+ **kwargs: Arbitrary additional keyword arguments.
1757
+
1758
+ These are usually passed to the model provider API call.
1759
+
1760
+ Returns:
1761
+ An `LLMResult`, which contains a list of candidate `Generations` for each
1762
+ input prompt and additional model provider-specific output.
1763
+
1764
+ """
1765
+ ls_structured_output_format = kwargs.pop(
1766
+ "ls_structured_output_format", None
1767
+ ) or kwargs.pop("structured_output_format", None)
1768
+ ls_structured_output_format_dict = _format_ls_structured_output(
1769
+ ls_structured_output_format
1770
+ )
1771
+
1772
+ params = self._get_invocation_params(stop=stop, **kwargs)
1773
+ options = {"stop": stop, **ls_structured_output_format_dict}
1774
+ inheritable_metadata = {
1775
+ **(metadata or {}),
1776
+ **self._get_ls_params_with_defaults(stop=stop, **kwargs),
1777
+ }
1778
+
1779
+ callback_manager = AsyncCallbackManager.configure(
1780
+ callbacks,
1781
+ self.callbacks,
1782
+ self.verbose,
1783
+ tags,
1784
+ self.tags,
1785
+ inheritable_metadata,
1786
+ self.metadata,
1787
+ langsmith_inheritable_metadata=_filter_invocation_params_for_tracing(
1788
+ params
1789
+ ),
1790
+ )
1791
+
1792
+ messages_to_trace = [
1793
+ _format_for_tracing(message_list) for message_list in messages
1794
+ ]
1795
+ run_managers = await callback_manager.on_chat_model_start(
1796
+ self._serialized,
1797
+ messages_to_trace,
1798
+ invocation_params=params,
1799
+ options=options,
1800
+ name=run_name,
1801
+ batch_size=len(messages),
1802
+ run_id=run_id,
1803
+ )
1804
+
1805
+ input_messages = [
1806
+ _normalize_messages(message_list) for message_list in messages
1807
+ ]
1808
+ results = await asyncio.gather(
1809
+ *[
1810
+ self._agenerate_with_cache(
1811
+ m,
1812
+ stop=stop,
1813
+ run_manager=run_managers[i] if run_managers else None,
1814
+ **kwargs,
1815
+ )
1816
+ for i, m in enumerate(input_messages)
1817
+ ],
1818
+ return_exceptions=True,
1819
+ )
1820
+ exceptions = []
1821
+ for i, res in enumerate(results):
1822
+ if isinstance(res, BaseException):
1823
+ if run_managers:
1824
+ generations_with_error_metadata = _generate_response_from_error(res)
1825
+ await run_managers[i].on_llm_error(
1826
+ res,
1827
+ response=LLMResult(
1828
+ generations=[generations_with_error_metadata]
1829
+ ),
1830
+ )
1831
+ exceptions.append(res)
1832
+ if exceptions:
1833
+ if run_managers:
1834
+ await asyncio.gather(
1835
+ *[
1836
+ run_manager.on_llm_end(
1837
+ LLMResult(
1838
+ generations=[res.generations], # type: ignore[union-attr]
1839
+ llm_output=res.llm_output, # type: ignore[union-attr]
1840
+ )
1841
+ )
1842
+ for run_manager, res in zip(run_managers, results, strict=False)
1843
+ if not isinstance(res, Exception)
1844
+ ]
1845
+ )
1846
+ raise exceptions[0]
1847
+ flattened_outputs = [
1848
+ LLMResult(generations=[res.generations], llm_output=res.llm_output) # type: ignore[union-attr]
1849
+ for res in results
1850
+ ]
1851
+ llm_output = self._combine_llm_outputs([res.llm_output for res in results]) # type: ignore[union-attr]
1852
+ generations = [res.generations for res in results] # type: ignore[union-attr]
1853
+ output = LLMResult(generations=generations, llm_output=llm_output)
1854
+ await asyncio.gather(
1855
+ *[
1856
+ run_manager.on_llm_end(flattened_output)
1857
+ for run_manager, flattened_output in zip(
1858
+ run_managers, flattened_outputs, strict=False
1859
+ )
1860
+ ]
1861
+ )
1862
+ if run_managers:
1863
+ output.run = [
1864
+ RunInfo(run_id=run_manager.run_id) for run_manager in run_managers
1865
+ ]
1866
+ return output
1867
+
1868
+ @override
1869
+ def generate_prompt(
1870
+ self,
1871
+ prompts: list[PromptValue],
1872
+ stop: list[str] | None = None,
1873
+ callbacks: Callbacks = None,
1874
+ **kwargs: Any,
1875
+ ) -> LLMResult:
1876
+ prompt_messages = [p.to_messages() for p in prompts]
1877
+ return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)
1878
+
1879
+ @override
1880
+ async def agenerate_prompt(
1881
+ self,
1882
+ prompts: list[PromptValue],
1883
+ stop: list[str] | None = None,
1884
+ callbacks: Callbacks = None,
1885
+ **kwargs: Any,
1886
+ ) -> LLMResult:
1887
+ prompt_messages = [p.to_messages() for p in prompts]
1888
+ return await self.agenerate(
1889
+ prompt_messages, stop=stop, callbacks=callbacks, **kwargs
1890
+ )
1891
+
1892
+ def _generate_with_cache(
1893
+ self,
1894
+ messages: list[BaseMessage],
1895
+ stop: list[str] | None = None,
1896
+ run_manager: CallbackManagerForLLMRun | None = None,
1897
+ **kwargs: Any,
1898
+ ) -> ChatResult:
1899
+ llm_cache = self.cache if isinstance(self.cache, BaseCache) else get_llm_cache()
1900
+ # We should check the cache unless it's explicitly set to False
1901
+ # A None cache means we should use the default global cache
1902
+ # if it's configured.
1903
+ check_cache = self.cache is not False
1904
+ if check_cache:
1905
+ if llm_cache is not None:
1906
+ llm_string = self._get_llm_string(stop=stop, **kwargs)
1907
+ normalized_messages = [
1908
+ (
1909
+ msg.model_copy(update={"id": None})
1910
+ if getattr(msg, "id", None) is not None
1911
+ else msg
1912
+ )
1913
+ for msg in messages
1914
+ ]
1915
+ prompt = dumps(normalized_messages)
1916
+ cache_val = llm_cache.lookup(prompt, llm_string)
1917
+ if isinstance(cache_val, list):
1918
+ converted_generations = self._convert_cached_generations(cache_val)
1919
+ self._replay_v2_events_for_cache_hit(
1920
+ converted_generations,
1921
+ run_manager=run_manager,
1922
+ **kwargs,
1923
+ )
1924
+ return ChatResult(generations=converted_generations)
1925
+ elif self.cache is None:
1926
+ pass
1927
+ else:
1928
+ msg = "Asked to cache, but no cache found at `coco.cache`."
1929
+ raise ValueError(msg)
1930
+
1931
+ # Apply the rate limiter after checking the cache, since
1932
+ # we usually don't want to rate limit cache lookups, but
1933
+ # we do want to rate limit API requests.
1934
+ if self.rate_limiter:
1935
+ self.rate_limiter.acquire(blocking=True)
1936
+
1937
+ # v2 streaming: preferred over v1 when any attached handler opts in via
1938
+ # `_V2StreamingCallbackHandler`. Drives the protocol event generator
1939
+ # (native or `_stream` compat bridge) through the shared helper so
1940
+ # `on_stream_event` fires per event, then returns a normal `ChatResult`
1941
+ # so caching / `on_llm_end` stay on the existing generate path.
1942
+ if self._should_use_protocol_streaming(
1943
+ async_api=False,
1944
+ run_manager=run_manager,
1945
+ **kwargs,
1946
+ ):
1947
+ stream_accum = ChatModelStream(
1948
+ message_id=(
1949
+ f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
1950
+ )
1951
+ )
1952
+ assert run_manager is not None # noqa: S101
1953
+ for _event in self._iter_v2_events(
1954
+ messages,
1955
+ run_manager=run_manager,
1956
+ stream=stream_accum,
1957
+ stop=stop,
1958
+ **kwargs,
1959
+ ):
1960
+ pass
1961
+ if stream_accum.output_message is None:
1962
+ msg = "v2 stream finished without producing a message"
1963
+ raise RuntimeError(msg)
1964
+ result = ChatResult(
1965
+ generations=[ChatGeneration(message=stream_accum.output_message)]
1966
+ )
1967
+ # If stream is not explicitly set, check if implicitly requested by
1968
+ # astream_events() or astream_log(). Bail out if _stream not implemented
1969
+ elif self._should_stream(
1970
+ async_api=False,
1971
+ run_manager=run_manager,
1972
+ **kwargs,
1973
+ ):
1974
+ chunks: list[ChatGenerationChunk] = []
1975
+ run_id: str | None = (
1976
+ f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
1977
+ )
1978
+ yielded = False
1979
+ index = -1
1980
+ index_type = ""
1981
+ for chunk in self._stream(messages, stop=stop, **kwargs):
1982
+ chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
1983
+ if self.output_version == "v1":
1984
+ # Overwrite .content with .content_blocks
1985
+ chunk.message = _update_message_content_to_blocks(
1986
+ chunk.message, "v1"
1987
+ )
1988
+ for block in cast(
1989
+ "list[types.ContentBlock]", chunk.message.content
1990
+ ):
1991
+ if block["type"] != index_type:
1992
+ index_type = block["type"]
1993
+ index += 1
1994
+ if "index" not in block:
1995
+ block["index"] = index
1996
+ if run_manager:
1997
+ if chunk.message.id is None:
1998
+ chunk.message.id = run_id
1999
+ run_manager.on_llm_new_token(chunk.message.content, chunk=chunk)
2000
+ chunks.append(chunk)
2001
+ yielded = True
2002
+
2003
+ # Yield a final empty chunk with chunk_position="last" if not yet yielded
2004
+ if (
2005
+ yielded
2006
+ and isinstance(chunk.message, AIMessageChunk)
2007
+ and not chunk.message.chunk_position
2008
+ ):
2009
+ empty_content: str | list[str | dict[str, Any]] = (
2010
+ "" if isinstance(chunk.message.content, str) else []
2011
+ )
2012
+ chunk = ChatGenerationChunk(
2013
+ message=AIMessageChunk(
2014
+ content=empty_content, chunk_position="last", id=run_id
2015
+ )
2016
+ )
2017
+ if run_manager:
2018
+ run_manager.on_llm_new_token("", chunk=chunk)
2019
+ chunks.append(chunk)
2020
+ result = generate_from_stream(iter(chunks))
2021
+ elif inspect.signature(self._generate).parameters.get("run_manager"):
2022
+ result = self._generate(
2023
+ messages, stop=stop, run_manager=run_manager, **kwargs
2024
+ )
2025
+ else:
2026
+ result = self._generate(messages, stop=stop, **kwargs)
2027
+
2028
+ if self.output_version == "v1":
2029
+ # Overwrite .content with .content_blocks
2030
+ for generation in result.generations:
2031
+ generation.message = _update_message_content_to_blocks(
2032
+ generation.message, "v1"
2033
+ )
2034
+
2035
+ # Add response metadata to each generation
2036
+ for idx, generation in enumerate(result.generations):
2037
+ if run_manager and generation.message.id is None:
2038
+ generation.message.id = f"{LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
2039
+ generation.message.response_metadata = _gen_info_and_msg_metadata(
2040
+ generation
2041
+ )
2042
+ if len(result.generations) == 1 and result.llm_output is not None:
2043
+ result.generations[0].message.response_metadata = {
2044
+ **result.llm_output,
2045
+ **result.generations[0].message.response_metadata,
2046
+ }
2047
+ if check_cache and llm_cache is not None:
2048
+ llm_cache.update(prompt, llm_string, result.generations)
2049
+ return result
2050
+
2051
+ async def _agenerate_with_cache(
2052
+ self,
2053
+ messages: list[BaseMessage],
2054
+ stop: list[str] | None = None,
2055
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
2056
+ **kwargs: Any,
2057
+ ) -> ChatResult:
2058
+ llm_cache = self.cache if isinstance(self.cache, BaseCache) else get_llm_cache()
2059
+ # We should check the cache unless it's explicitly set to False
2060
+ # A None cache means we should use the default global cache
2061
+ # if it's configured.
2062
+ check_cache = self.cache is not False
2063
+ if check_cache:
2064
+ if llm_cache is not None:
2065
+ llm_string = self._get_llm_string(stop=stop, **kwargs)
2066
+ normalized_messages = [
2067
+ (
2068
+ msg.model_copy(update={"id": None})
2069
+ if getattr(msg, "id", None) is not None
2070
+ else msg
2071
+ )
2072
+ for msg in messages
2073
+ ]
2074
+ prompt = dumps(normalized_messages)
2075
+ cache_val = await llm_cache.alookup(prompt, llm_string)
2076
+ if isinstance(cache_val, list):
2077
+ converted_generations = self._convert_cached_generations(cache_val)
2078
+ await self._areplay_v2_events_for_cache_hit(
2079
+ converted_generations,
2080
+ run_manager=run_manager,
2081
+ **kwargs,
2082
+ )
2083
+ return ChatResult(generations=converted_generations)
2084
+ elif self.cache is None:
2085
+ pass
2086
+ else:
2087
+ msg = "Asked to cache, but no cache found at `coco.cache`."
2088
+ raise ValueError(msg)
2089
+
2090
+ # Apply the rate limiter after checking the cache, since
2091
+ # we usually don't want to rate limit cache lookups, but
2092
+ # we do want to rate limit API requests.
2093
+ if self.rate_limiter:
2094
+ await self.rate_limiter.aacquire(blocking=True)
2095
+
2096
+ # v2 streaming: see sync counterpart in `_generate_with_cache`.
2097
+ if self._should_use_protocol_streaming(
2098
+ async_api=True,
2099
+ run_manager=run_manager,
2100
+ **kwargs,
2101
+ ):
2102
+ stream_accum = AsyncChatModelStream(
2103
+ message_id=(
2104
+ f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
2105
+ )
2106
+ )
2107
+ assert run_manager is not None # noqa: S101
2108
+ async for _event in self._aiter_v2_events(
2109
+ messages,
2110
+ run_manager=run_manager,
2111
+ stream=stream_accum,
2112
+ stop=stop,
2113
+ **kwargs,
2114
+ ):
2115
+ pass
2116
+ if stream_accum.output_message is None:
2117
+ msg = "v2 stream finished without producing a message"
2118
+ raise RuntimeError(msg)
2119
+ result = ChatResult(
2120
+ generations=[ChatGeneration(message=stream_accum.output_message)]
2121
+ )
2122
+ # If stream is not explicitly set, check if implicitly requested by
2123
+ # astream_events() or astream_log(). Bail out if _astream not implemented
2124
+ elif self._should_stream(
2125
+ async_api=True,
2126
+ run_manager=run_manager,
2127
+ **kwargs,
2128
+ ):
2129
+ chunks: list[ChatGenerationChunk] = []
2130
+ run_id: str | None = (
2131
+ f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
2132
+ )
2133
+ yielded = False
2134
+ index = -1
2135
+ index_type = ""
2136
+ async for chunk in self._astream(messages, stop=stop, **kwargs):
2137
+ chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
2138
+ if self.output_version == "v1":
2139
+ # Overwrite .content with .content_blocks
2140
+ chunk.message = _update_message_content_to_blocks(
2141
+ chunk.message, "v1"
2142
+ )
2143
+ for block in cast(
2144
+ "list[types.ContentBlock]", chunk.message.content
2145
+ ):
2146
+ if block["type"] != index_type:
2147
+ index_type = block["type"]
2148
+ index += 1
2149
+ if "index" not in block:
2150
+ block["index"] = index
2151
+ if run_manager:
2152
+ if chunk.message.id is None:
2153
+ chunk.message.id = run_id
2154
+ await run_manager.on_llm_new_token(
2155
+ chunk.message.content, chunk=chunk
2156
+ )
2157
+ chunks.append(chunk)
2158
+ yielded = True
2159
+
2160
+ # Yield a final empty chunk with chunk_position="last" if not yet yielded
2161
+ if (
2162
+ yielded
2163
+ and isinstance(chunk.message, AIMessageChunk)
2164
+ and not chunk.message.chunk_position
2165
+ ):
2166
+ empty_content: str | list[str | dict[str, Any]] = (
2167
+ "" if isinstance(chunk.message.content, str) else []
2168
+ )
2169
+ chunk = ChatGenerationChunk(
2170
+ message=AIMessageChunk(
2171
+ content=empty_content, chunk_position="last", id=run_id
2172
+ )
2173
+ )
2174
+ if run_manager:
2175
+ await run_manager.on_llm_new_token("", chunk=chunk)
2176
+ chunks.append(chunk)
2177
+ result = generate_from_stream(iter(chunks))
2178
+ elif inspect.signature(self._agenerate).parameters.get("run_manager"):
2179
+ result = await self._agenerate(
2180
+ messages, stop=stop, run_manager=run_manager, **kwargs
2181
+ )
2182
+ else:
2183
+ result = await self._agenerate(messages, stop=stop, **kwargs)
2184
+
2185
+ if self.output_version == "v1":
2186
+ # Overwrite .content with .content_blocks
2187
+ for generation in result.generations:
2188
+ generation.message = _update_message_content_to_blocks(
2189
+ generation.message, "v1"
2190
+ )
2191
+
2192
+ # Add response metadata to each generation
2193
+ for idx, generation in enumerate(result.generations):
2194
+ if run_manager and generation.message.id is None:
2195
+ generation.message.id = f"{LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
2196
+ generation.message.response_metadata = _gen_info_and_msg_metadata(
2197
+ generation
2198
+ )
2199
+ if len(result.generations) == 1 and result.llm_output is not None:
2200
+ result.generations[0].message.response_metadata = {
2201
+ **result.llm_output,
2202
+ **result.generations[0].message.response_metadata,
2203
+ }
2204
+ if check_cache and llm_cache is not None:
2205
+ await llm_cache.aupdate(prompt, llm_string, result.generations)
2206
+ return result
2207
+
2208
+ @abstractmethod
2209
+ def _generate(
2210
+ self,
2211
+ messages: list[BaseMessage],
2212
+ stop: list[str] | None = None,
2213
+ run_manager: CallbackManagerForLLMRun | None = None,
2214
+ **kwargs: Any,
2215
+ ) -> ChatResult:
2216
+ """Generate the result.
2217
+
2218
+ Args:
2219
+ messages: The messages to generate from.
2220
+ stop: Optional list of stop words to use when generating.
2221
+ run_manager: Optional callback manager to use for this call.
2222
+ **kwargs: Additional keyword arguments to pass to the model.
2223
+
2224
+ Returns:
2225
+ The chat result.
2226
+ """
2227
+
2228
+ async def _agenerate(
2229
+ self,
2230
+ messages: list[BaseMessage],
2231
+ stop: list[str] | None = None,
2232
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
2233
+ **kwargs: Any,
2234
+ ) -> ChatResult:
2235
+ """Generate the result.
2236
+
2237
+ Args:
2238
+ messages: The messages to generate from.
2239
+ stop: Optional list of stop words to use when generating.
2240
+ run_manager: Optional callback manager to use for this call.
2241
+ **kwargs: Additional keyword arguments to pass to the model.
2242
+
2243
+ Returns:
2244
+ The chat result.
2245
+ """
2246
+ return await run_in_executor(
2247
+ None,
2248
+ self._generate,
2249
+ messages,
2250
+ stop,
2251
+ run_manager.get_sync() if run_manager else None,
2252
+ **kwargs,
2253
+ )
2254
+
2255
+ def _stream(
2256
+ self,
2257
+ messages: list[BaseMessage],
2258
+ stop: list[str] | None = None,
2259
+ run_manager: CallbackManagerForLLMRun | None = None,
2260
+ **kwargs: Any,
2261
+ ) -> Iterator[ChatGenerationChunk]:
2262
+ """Stream the output of the model.
2263
+
2264
+ Args:
2265
+ messages: The messages to generate from.
2266
+ stop: Optional list of stop words to use when generating.
2267
+ run_manager: Optional callback manager to use for this call.
2268
+ **kwargs: Additional keyword arguments to pass to the model.
2269
+
2270
+ Yields:
2271
+ The chat generation chunks.
2272
+ """
2273
+ raise NotImplementedError
2274
+
2275
+ async def _astream(
2276
+ self,
2277
+ messages: list[BaseMessage],
2278
+ stop: list[str] | None = None,
2279
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
2280
+ **kwargs: Any,
2281
+ ) -> AsyncIterator[ChatGenerationChunk]:
2282
+ """Stream the output of the model.
2283
+
2284
+ Args:
2285
+ messages: The messages to generate from.
2286
+ stop: Optional list of stop words to use when generating.
2287
+ run_manager: Optional callback manager to use for this call.
2288
+ **kwargs: Additional keyword arguments to pass to the model.
2289
+
2290
+ Yields:
2291
+ The chat generation chunks.
2292
+ """
2293
+ iterator = await run_in_executor(
2294
+ None,
2295
+ self._stream,
2296
+ messages,
2297
+ stop,
2298
+ run_manager.get_sync() if run_manager else None,
2299
+ **kwargs,
2300
+ )
2301
+ done = object()
2302
+ while True:
2303
+ item = await run_in_executor(
2304
+ None,
2305
+ next,
2306
+ iterator,
2307
+ done,
2308
+ )
2309
+ if item is done:
2310
+ break
2311
+ yield item # type: ignore[misc]
2312
+
2313
+ async def _call_async(
2314
+ self,
2315
+ messages: list[BaseMessage],
2316
+ stop: list[str] | None = None,
2317
+ callbacks: Callbacks = None,
2318
+ **kwargs: Any,
2319
+ ) -> BaseMessage:
2320
+ result = await self.agenerate(
2321
+ [messages], stop=stop, callbacks=callbacks, **kwargs
2322
+ )
2323
+ generation = result.generations[0][0]
2324
+ if isinstance(generation, ChatGeneration):
2325
+ return generation.message
2326
+ msg = "Unexpected generation type"
2327
+ raise ValueError(msg)
2328
+
2329
+ @property
2330
+ @abstractmethod
2331
+ def _llm_type(self) -> str:
2332
+ """Return type of chat model."""
2333
+
2334
+ @deprecated("1.4.2", alternative="asdict", removal="2.0.0")
2335
+ @override
2336
+ def dict(self, **_kwargs: Any) -> builtins.dict[str, Any]:
2337
+ """DEPRECATED - use `asdict()` instead.
2338
+
2339
+ Return a dictionary representation of the chat model.
2340
+ """
2341
+ return self.asdict()
2342
+
2343
+ def asdict(self) -> builtins.dict[str, Any]:
2344
+ """Return a dictionary representation of the chat model."""
2345
+ starter_dict = dict(self._identifying_params)
2346
+ starter_dict["_type"] = self._llm_type
2347
+ return starter_dict
2348
+
2349
+ def _dict_for_compat(self) -> builtins.dict[str, Any]:
2350
+ """Return the chat model dictionary while preserving deprecated overrides."""
2351
+ with suppress_coco_deprecation_warning():
2352
+ return self.dict()
2353
+
2354
+ @override
2355
+ def bind(self, **kwargs: Any) -> _ChatModelBinding:
2356
+ """Bind kwargs to this chat model, returning a typed `_ChatModelBinding`.
2357
+
2358
+ Overrides `Runnable.bind` so the result preserves chat-model-specific
2359
+ `stream_events` / `astream_events` overloads. Without this override,
2360
+ `model.bind(...).stream_events(version="v3")` would type as
2361
+ `Iterator[Any]` and `await model.bind(...).astream_events(version="v3")`
2362
+ as `Any`, forcing callers to `cast`.
2363
+ """
2364
+ return _ChatModelBinding(bound=self, kwargs=kwargs, config={})
2365
+
2366
+ def bind_tools(
2367
+ self,
2368
+ tools: Sequence[builtins.dict[str, Any] | type | Callable[..., Any] | BaseTool],
2369
+ *,
2370
+ tool_choice: str | None = None,
2371
+ **kwargs: Any,
2372
+ ) -> Runnable[LanguageModelInput, AIMessage]:
2373
+ """Bind tools to the model.
2374
+
2375
+ Args:
2376
+ tools: Sequence of tools to bind to the model.
2377
+ tool_choice: The tool to use. If "any" then any tool can be used.
2378
+
2379
+ Returns:
2380
+ A Runnable that returns a message.
2381
+
2382
+ """
2383
+ raise NotImplementedError
2384
+
2385
+ def with_structured_output(
2386
+ self,
2387
+ schema: builtins.dict[str, Any] | type,
2388
+ *,
2389
+ include_raw: bool = False,
2390
+ **kwargs: Any,
2391
+ ) -> Runnable[LanguageModelInput, builtins.dict[str, Any] | BaseModel]:
2392
+ """Model wrapper that returns outputs formatted to match the given schema.
2393
+
2394
+ Args:
2395
+ schema: The output schema. Can be passed in as:
2396
+
2397
+ - An OpenAI function/tool schema,
2398
+ - A JSON Schema,
2399
+ - A `TypedDict` class,
2400
+ - Or a Pydantic class.
2401
+
2402
+ If `schema` is a Pydantic class then the model output will be a
2403
+ Pydantic instance of that class, and the model-generated fields will be
2404
+ validated by the Pydantic class. Otherwise the model output will be a
2405
+ dict and will not be validated.
2406
+
2407
+ See `coco_core.utils.function_calling.convert_to_openai_tool` for
2408
+ more on how to properly specify types and descriptions of schema fields
2409
+ when specifying a Pydantic or `TypedDict` class.
2410
+
2411
+ include_raw:
2412
+ If `False` then only the parsed structured output is returned.
2413
+
2414
+ If an error occurs during model output parsing it will be raised.
2415
+
2416
+ If `True` then both the raw model response (a `BaseMessage`) and the
2417
+ parsed model response will be returned.
2418
+
2419
+ If an error occurs during output parsing it will be caught and returned
2420
+ as well.
2421
+
2422
+ The final output is always a `dict` with keys `'raw'`, `'parsed'`, and
2423
+ `'parsing_error'`.
2424
+
2425
+ Raises:
2426
+ ValueError: If there are any unsupported `kwargs`.
2427
+ NotImplementedError: If the model does not implement
2428
+ `with_structured_output()`.
2429
+
2430
+ Returns:
2431
+ A `Runnable` that takes same inputs as a
2432
+ `coco_core.language_models.chat.BaseChatModel`. If `include_raw` is
2433
+ `False` and `schema` is a Pydantic class, `Runnable` outputs an instance
2434
+ of `schema` (i.e., a Pydantic object). Otherwise, if `include_raw` is
2435
+ `False` then `Runnable` outputs a `dict`.
2436
+
2437
+ If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:
2438
+
2439
+ - `'raw'`: `BaseMessage`
2440
+ - `'parsed'`: `None` if there was a parsing error, otherwise the type
2441
+ depends on the `schema` as described above.
2442
+ - `'parsing_error'`: `BaseException | None`
2443
+
2444
+ ???+ example "Pydantic schema (`include_raw=False`)"
2445
+
2446
+ ```python
2447
+ from pydantic import BaseModel
2448
+
2449
+
2450
+ class AnswerWithJustification(BaseModel):
2451
+ '''An answer to the user question along with justification for the answer.'''
2452
+
2453
+ answer: str
2454
+ justification: str
2455
+
2456
+
2457
+ model = ChatModel(model="model-name", temperature=0)
2458
+ structured_model = model.with_structured_output(AnswerWithJustification)
2459
+
2460
+ structured_model.invoke(
2461
+ "What weighs more a pound of bricks or a pound of feathers"
2462
+ )
2463
+
2464
+ # -> AnswerWithJustification(
2465
+ # answer='They weigh the same',
2466
+ # justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'
2467
+ # )
2468
+ ```
2469
+
2470
+ ??? example "Pydantic schema (`include_raw=True`)"
2471
+
2472
+ ```python
2473
+ from pydantic import BaseModel
2474
+
2475
+
2476
+ class AnswerWithJustification(BaseModel):
2477
+ '''An answer to the user question along with justification for the answer.'''
2478
+
2479
+ answer: str
2480
+ justification: str
2481
+
2482
+
2483
+ model = ChatModel(model="model-name", temperature=0)
2484
+ structured_model = model.with_structured_output(
2485
+ AnswerWithJustification, include_raw=True
2486
+ )
2487
+
2488
+ structured_model.invoke(
2489
+ "What weighs more a pound of bricks or a pound of feathers"
2490
+ )
2491
+ # -> {
2492
+ # 'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Ao02pnFYXD6GN1yzc0uXPsvF', 'function': {'arguments': '{"answer":"They weigh the same.","justification":"Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ."}', 'name': 'AnswerWithJustification'}, 'type': 'function'}]}),
2493
+ # 'parsed': AnswerWithJustification(answer='They weigh the same.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'),
2494
+ # 'parsing_error': None
2495
+ # }
2496
+ ```
2497
+
2498
+ ??? example "Dictionary schema (`include_raw=False`)"
2499
+
2500
+ ```python
2501
+ from pydantic import BaseModel
2502
+ from coco_core.utils.function_calling import convert_to_openai_tool
2503
+
2504
+
2505
+ class AnswerWithJustification(BaseModel):
2506
+ '''An answer to the user question along with justification for the answer.'''
2507
+
2508
+ answer: str
2509
+ justification: str
2510
+
2511
+
2512
+ dict_schema = convert_to_openai_tool(AnswerWithJustification)
2513
+ model = ChatModel(model="model-name", temperature=0)
2514
+ structured_model = model.with_structured_output(dict_schema)
2515
+
2516
+ structured_model.invoke(
2517
+ "What weighs more a pound of bricks or a pound of feathers"
2518
+ )
2519
+ # -> {
2520
+ # 'answer': 'They weigh the same',
2521
+ # 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'
2522
+ # }
2523
+ ```
2524
+
2525
+ !!! warning "Behavior changed in `trustcoco-core` 0.2.26"
2526
+
2527
+ Added support for `TypedDict` class.
2528
+
2529
+ """ # noqa: E501
2530
+ _ = kwargs.pop("method", None)
2531
+ _ = kwargs.pop("strict", None)
2532
+ if kwargs:
2533
+ msg = f"Received unsupported arguments {kwargs}"
2534
+ raise ValueError(msg)
2535
+
2536
+ if type(self).bind_tools is BaseChatModel.bind_tools:
2537
+ msg = "with_structured_output is not implemented for this model."
2538
+ raise NotImplementedError(msg)
2539
+
2540
+ llm = self.bind_tools(
2541
+ [schema],
2542
+ tool_choice="any",
2543
+ ls_structured_output_format={
2544
+ "kwargs": {"method": "function_calling"},
2545
+ "schema": schema,
2546
+ },
2547
+ )
2548
+ output_parser: JsonOutputToolsParser
2549
+ if isinstance(schema, type) and is_basemodel_subclass(schema):
2550
+ output_parser = PydanticToolsParser(tools=[schema], first_tool_only=True)
2551
+ else:
2552
+ key_name = convert_to_openai_tool(schema)["function"]["name"]
2553
+ output_parser = JsonOutputKeyToolsParser(
2554
+ key_name=key_name, first_tool_only=True
2555
+ )
2556
+ if include_raw:
2557
+ parser_assign = RunnablePassthrough.assign(
2558
+ parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None
2559
+ )
2560
+ parser_none = RunnablePassthrough.assign(parsed=lambda _: None)
2561
+ parser_with_fallback = parser_assign.with_fallbacks(
2562
+ [parser_none], exception_key="parsing_error"
2563
+ )
2564
+ return RunnableMap(raw=llm) | parser_with_fallback
2565
+ return llm | output_parser
2566
+
2567
+
2568
+ class _ChatModelBinding(RunnableBinding[LanguageModelInput, AIMessage]): # type: ignore[no-redef]
2569
+ """`RunnableBinding` that preserves chat-model-typed v3 overloads.
2570
+
2571
+ Returned by `BaseChatModel.bind` so that callers of the bound runnable's
2572
+ `stream_events(version="v3")` / `astream_events(version="v3")` get the
2573
+ typed `ChatModelStream` / `AsyncChatModelStream` back without needing
2574
+ `cast`. At runtime this is a plain `RunnableBinding`; the subclass
2575
+ exists purely to give the type checker a more specific surface.
2576
+
2577
+ The chat-model narrowing is preserved across further `bind` /
2578
+ `with_config` calls because `RunnableBinding.bind` constructs its
2579
+ result via `self.__class__(...)`.
2580
+ """
2581
+
2582
+ @classmethod
2583
+ @override
2584
+ def lc_id(cls) -> list[str]:
2585
+ """Serialize as `RunnableBinding`.
2586
+
2587
+ At runtime this class is behaviorally identical to `RunnableBinding`;
2588
+ keeping the serialized id stable means existing snapshots and the
2589
+ load mapping continue to work without registering a new entry.
2590
+ """
2591
+ return [*cls.get_lc_namespace(), "RunnableBinding"]
2592
+
2593
+ @overload # type: ignore[override]
2594
+ def stream_events(
2595
+ self,
2596
+ input: LanguageModelInput,
2597
+ config: RunnableConfig | None = None,
2598
+ *,
2599
+ version: Literal["v1", "v2"] = "v2",
2600
+ **kwargs: Any,
2601
+ ) -> Iterator[StreamEvent]: ...
2602
+
2603
+ @overload
2604
+ def stream_events(
2605
+ self,
2606
+ input: LanguageModelInput,
2607
+ config: RunnableConfig | None = None,
2608
+ *,
2609
+ version: Literal["v3"],
2610
+ stop: list[str] | None = None,
2611
+ **kwargs: Any,
2612
+ ) -> ChatModelStream: ...
2613
+
2614
+ def stream_events(
2615
+ self,
2616
+ input: LanguageModelInput,
2617
+ config: RunnableConfig | None = None,
2618
+ *,
2619
+ version: Literal["v1", "v2", "v3"] = "v2",
2620
+ **kwargs: Any,
2621
+ ) -> Iterator[StreamEvent] | ChatModelStream:
2622
+ return super().stream_events(input, config, version=version, **kwargs)
2623
+
2624
+ @overload
2625
+ def astream_events(
2626
+ self,
2627
+ input: LanguageModelInput,
2628
+ config: RunnableConfig | None = None,
2629
+ *,
2630
+ version: Literal["v1", "v2"] = "v2",
2631
+ **kwargs: Any,
2632
+ ) -> AsyncIterator[StreamEvent]: ...
2633
+
2634
+ @overload
2635
+ def astream_events(
2636
+ self,
2637
+ input: LanguageModelInput,
2638
+ config: RunnableConfig | None = None,
2639
+ *,
2640
+ version: Literal["v3"],
2641
+ stop: list[str] | None = None,
2642
+ **kwargs: Any,
2643
+ ) -> Awaitable[AsyncChatModelStream]: ...
2644
+
2645
+ def astream_events(
2646
+ self,
2647
+ input: LanguageModelInput,
2648
+ config: RunnableConfig | None = None,
2649
+ **kwargs: Any,
2650
+ ) -> AsyncIterator[StreamEvent] | Awaitable[AsyncChatModelStream]:
2651
+ return cast(
2652
+ "AsyncIterator[StreamEvent] | Awaitable[AsyncChatModelStream]",
2653
+ super().astream_events(input, config, **kwargs),
2654
+ )
2655
+
2656
+
2657
+ class SimpleChatModel(BaseChatModel):
2658
+ """Simplified implementation for a chat model to inherit from.
2659
+
2660
+ !!! note
2661
+ This implementation is primarily here for backwards compatibility. For new
2662
+ implementations, please use `BaseChatModel` directly.
2663
+
2664
+ """
2665
+
2666
+ def _generate(
2667
+ self,
2668
+ messages: list[BaseMessage],
2669
+ stop: list[str] | None = None,
2670
+ run_manager: CallbackManagerForLLMRun | None = None,
2671
+ **kwargs: Any,
2672
+ ) -> ChatResult:
2673
+ output_str = self._call(messages, stop=stop, run_manager=run_manager, **kwargs)
2674
+ message = AIMessage(content=output_str)
2675
+ generation = ChatGeneration(message=message)
2676
+ return ChatResult(generations=[generation])
2677
+
2678
+ @abstractmethod
2679
+ def _call(
2680
+ self,
2681
+ messages: list[BaseMessage],
2682
+ stop: list[str] | None = None,
2683
+ run_manager: CallbackManagerForLLMRun | None = None,
2684
+ **kwargs: Any,
2685
+ ) -> str:
2686
+ """Simpler interface."""
2687
+
2688
+ async def _agenerate(
2689
+ self,
2690
+ messages: list[BaseMessage],
2691
+ stop: list[str] | None = None,
2692
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
2693
+ **kwargs: Any,
2694
+ ) -> ChatResult:
2695
+ return await run_in_executor(
2696
+ None,
2697
+ self._generate,
2698
+ messages,
2699
+ stop=stop,
2700
+ run_manager=run_manager.get_sync() if run_manager else None,
2701
+ **kwargs,
2702
+ )
2703
+
2704
+
2705
+ def _gen_info_and_msg_metadata(
2706
+ generation: ChatGeneration | ChatGenerationChunk,
2707
+ ) -> dict[str, Any]:
2708
+ """Extract metadata for the response metadata.
2709
+
2710
+ Removes GATEWAY_METADATA from the generation info.
2711
+ """
2712
+ generation_info = generation.generation_info or {}
2713
+ return {
2714
+ **{
2715
+ key: value
2716
+ for key, value in generation_info.items()
2717
+ if key != GATEWAY_METADATA_RESPONSE_KEY
2718
+ },
2719
+ **generation.message.response_metadata,
2720
+ }
2721
+
2722
+
2723
+ _MAX_CLEANUP_DEPTH = 100
2724
+
2725
+
2726
+ def _cleanup_llm_representation(serialized: Any, depth: int) -> None:
2727
+ """Remove non-serializable objects from a serialized object."""
2728
+ if depth > _MAX_CLEANUP_DEPTH: # Don't cooperate for pathological cases
2729
+ return
2730
+
2731
+ if not isinstance(serialized, dict):
2732
+ return
2733
+
2734
+ if (
2735
+ "type" in serialized
2736
+ and serialized["type"] == "not_implemented"
2737
+ and "repr" in serialized
2738
+ ):
2739
+ del serialized["repr"]
2740
+
2741
+ if "graph" in serialized:
2742
+ del serialized["graph"]
2743
+
2744
+ if "kwargs" in serialized:
2745
+ kwargs = serialized["kwargs"]
2746
+
2747
+ for value in kwargs.values():
2748
+ _cleanup_llm_representation(value, depth + 1)