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,274 @@
1
+ """Private helpers for resolving LangSmith gateway configuration.
2
+
3
+ The [LangSmith LLM gateway](https://docs.langchain.com/langsmith/llm-gateway)
4
+ lets a chat model reach a provider through a proxy configured via environment
5
+ variables. These helpers centralize the (non-trivial) precedence rules so that
6
+ each provider integration resolves its base URL and API key identically.
7
+
8
+ This module is private: the API is not stable and may change without notice.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ from collections.abc import Mapping
16
+ from typing import TYPE_CHECKING, Any, NamedTuple
17
+
18
+ from pydantic import SecretStr
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Sequence
22
+
23
+ from pydantic import BaseModel
24
+
25
+ # Key under which integrations place parsed gateway metadata on a generation's
26
+ # `generation_info`. This is the contract consumed by the LangSmith tracer, which
27
+ # promotes it to run metadata. Defined here so integrations and the tracer agree
28
+ # on a single name.
29
+ GATEWAY_METADATA_RESPONSE_KEY = "lc_gateway_metadata"
30
+
31
+ # HTTP response header the LangSmith gateway uses to return per-request metadata
32
+ # (e.g. the resolved provider and model). The value is JSON-encoded.
33
+ GATEWAY_METADATA_HEADER = "X-LangSmith-Gateway-Metadata"
34
+
35
+ _LANGSMITH_GATEWAY_ENV = "LANGSMITH_GATEWAY"
36
+ _LANGSMITH_GATEWAY_API_KEY_ENV = "LANGSMITH_GATEWAY_API_KEY"
37
+ _LANGSMITH_API_KEY_ENV = "LANGSMITH_API_KEY"
38
+ _LANGSMITH_GATEWAY_DEFAULT_BASE = "https://gateway.smith.langchain.com"
39
+
40
+ _TRUE_VALUES = ("true", "1", "yes")
41
+ _FALSE_VALUES = ("false", "0", "no")
42
+
43
+
44
+ class GatewayConfig(NamedTuple):
45
+ """Resolved gateway configuration.
46
+
47
+ Attributes:
48
+ base_url: The base URL the client should use, or None to defer to the
49
+ provider SDK's own default.
50
+ api_key: The API key to use. A ``SecretStr`` when derived from the
51
+ environment; the caller's value returned unchanged when one was
52
+ passed explicitly; None when no key could be resolved.
53
+ base_url_from_gateway: Whether ``base_url`` was populated from the
54
+ LangSmith gateway (as opposed to an explicit value, a provider env
55
+ var, or a default).
56
+ """
57
+
58
+ base_url: str | None
59
+ api_key: Any
60
+ base_url_from_gateway: bool
61
+
62
+
63
+ def _first_env(names: str | Sequence[str]) -> str | None:
64
+ """Return the first non-empty value among ``names``, or None."""
65
+ if isinstance(names, str):
66
+ names = (names,)
67
+ for name in names:
68
+ value = os.getenv(name)
69
+ if value:
70
+ return value
71
+ return None
72
+
73
+
74
+ def _resolve_gateway_base_url(provider_path: str) -> str | None:
75
+ """Resolve the LangSmith gateway base URL for a provider.
76
+
77
+ ``LANGSMITH_GATEWAY`` accepts either a boolean-ish string or an explicit
78
+ base URL:
79
+
80
+ - ``true`` / ``1`` / ``yes`` -> the default gateway host.
81
+ - ``false`` / ``0`` / ``no`` / unset / empty -> the gateway is disabled
82
+ (None).
83
+ - anything else -> treated as a custom gateway base URL.
84
+
85
+ The provider-specific path is appended in all enabled cases.
86
+
87
+ Args:
88
+ provider_path: Path segment for the provider, e.g. ``"openai/v1"`` or
89
+ ``"anthropic"``.
90
+
91
+ Returns:
92
+ The provider base URL on the gateway, or None if the gateway is
93
+ disabled.
94
+ """
95
+ raw = os.getenv(_LANGSMITH_GATEWAY_ENV)
96
+ if not raw or raw.lower() in _FALSE_VALUES:
97
+ return None
98
+ base = (
99
+ _LANGSMITH_GATEWAY_DEFAULT_BASE
100
+ if raw.lower() in _TRUE_VALUES
101
+ else raw.rstrip("/")
102
+ )
103
+ return f"{base}/{provider_path}"
104
+
105
+
106
+ def _resolve_gateway_config(
107
+ *,
108
+ base_url: str | None,
109
+ api_key: Any,
110
+ provider_path: str,
111
+ base_url_env: str | Sequence[str] = (),
112
+ api_key_env: str | Sequence[str] = (),
113
+ default_base_url: str | None = None,
114
+ ) -> GatewayConfig:
115
+ """Resolve a provider's base URL and API key, applying gateway settings.
116
+
117
+ Precedence:
118
+
119
+ - **base_url:** explicit ``base_url`` > ``base_url_env`` > LangSmith gateway
120
+ > ``default_base_url``.
121
+ - **api_key:** explicit ``api_key`` (returned unchanged) > the gateway key if
122
+ the base URL came from the gateway, otherwise the provider key > the other.
123
+ The gateway key uses ``LANGSMITH_GATEWAY_API_KEY`` when the gateway is
124
+ enabled, with ``LANGSMITH_API_KEY`` as a fallback only when the base URL came
125
+ from the gateway.
126
+
127
+ The provenance flip on the key means an ``OPENAI_API_KEY``-style provider key
128
+ is preferred whenever the caller pointed the base URL at a non-gateway
129
+ endpoint (so a stray gateway key is not sent to the provider), while the
130
+ gateway key wins for the common "just enable the gateway" setup even if a
131
+ provider key happens to be present in the environment.
132
+
133
+ Args:
134
+ base_url: Explicitly-provided base URL (e.g. a ``base_url`` kwarg), or
135
+ None if not set by the caller.
136
+ api_key: Explicitly-provided API key, or None if not set by the caller.
137
+ Returned unchanged when not None, so a caller-supplied value (secret
138
+ or callable) always wins.
139
+ provider_path: Path segment appended to the gateway host.
140
+ base_url_env: Env var name(s) for the provider base URL, in priority
141
+ order.
142
+ api_key_env: Env var name(s) for the provider API key, in priority order.
143
+ default_base_url: Base URL used when nothing else is set.
144
+
145
+ Returns:
146
+ The resolved `GatewayConfig`.
147
+ """
148
+ gateway_base_url = _resolve_gateway_base_url(provider_path)
149
+
150
+ resolved_base_url = base_url
151
+ base_url_from_gateway = False
152
+ if resolved_base_url is None:
153
+ resolved_base_url = _first_env(base_url_env)
154
+ if resolved_base_url is None:
155
+ if gateway_base_url is not None:
156
+ resolved_base_url = gateway_base_url
157
+ base_url_from_gateway = True
158
+ else:
159
+ resolved_base_url = default_base_url
160
+
161
+ if api_key is not None:
162
+ resolved_api_key: Any = api_key
163
+ else:
164
+ gateway_api_key = (
165
+ os.getenv(_LANGSMITH_GATEWAY_API_KEY_ENV)
166
+ if gateway_base_url is not None
167
+ else None
168
+ )
169
+ if not gateway_api_key and base_url_from_gateway:
170
+ gateway_api_key = os.getenv(_LANGSMITH_API_KEY_ENV)
171
+ provider_api_key = _first_env(api_key_env)
172
+ chosen = (
173
+ (gateway_api_key or provider_api_key)
174
+ if base_url_from_gateway
175
+ else (provider_api_key or gateway_api_key)
176
+ )
177
+ resolved_api_key = SecretStr(chosen) if chosen else None
178
+
179
+ return GatewayConfig(resolved_base_url, resolved_api_key, base_url_from_gateway)
180
+
181
+
182
+ def _pop_provided(values: dict[str, Any], cls: type[BaseModel], field: str) -> Any:
183
+ """Pop a caller-provided field value by name or alias; None if absent.
184
+
185
+ Handles models with ``populate_by_name=True``, where a field may be supplied
186
+ under either its name or its alias. Both keys are removed so the resolved
187
+ value can be written back canonically under the field name. The alias is read
188
+ from the model rather than hard-coded, so callers pass only field names.
189
+ """
190
+ value = values.pop(field, None)
191
+ alias = cls.model_fields[field].alias
192
+ if alias is not None:
193
+ alias_value = values.pop(alias, None)
194
+ if value is None:
195
+ value = alias_value
196
+ return value
197
+
198
+
199
+ def _apply_gateway_config(
200
+ values: dict[str, Any],
201
+ cls: type[BaseModel],
202
+ *,
203
+ base_url_field: str,
204
+ api_key_field: str,
205
+ provider_path: str,
206
+ base_url_env: str | Sequence[str] = (),
207
+ api_key_env: str | Sequence[str] = (),
208
+ default_base_url: str | None = None,
209
+ ) -> GatewayConfig:
210
+ """Resolve gateway settings from a model's raw input in a "before" validator.
211
+
212
+ Reads the caller-provided base URL and API key (by field name or alias),
213
+ resolves them against the gateway and provider env vars via
214
+ `_resolve_gateway_config`, then writes the results back into ``values`` under
215
+ the canonical field names. This lets a provider integration keep a
216
+ non-optional key field: the resolved value is injected before field
217
+ validation runs, so the field always receives a concrete value.
218
+
219
+ The resolved API key is written only when non-None, so the field's own
220
+ default applies when no key is found. Returns the `GatewayConfig` so the
221
+ caller can, for example, raise when a required key is missing.
222
+
223
+ Args:
224
+ values: The raw input mapping passed to the model, mutated in place.
225
+ cls: The model class, used to look up field aliases.
226
+ base_url_field: Name of the base URL field (e.g. ``"anthropic_api_url"``).
227
+ api_key_field: Name of the API key field (e.g. ``"anthropic_api_key"``).
228
+ provider_path: Path segment appended to the gateway host.
229
+ base_url_env: Env var name(s) for the provider base URL.
230
+ api_key_env: Env var name(s) for the provider API key.
231
+ default_base_url: Base URL used when nothing else is set.
232
+
233
+ Returns:
234
+ The resolved `GatewayConfig`.
235
+ """
236
+ config = _resolve_gateway_config(
237
+ base_url=_pop_provided(values, cls, base_url_field),
238
+ api_key=_pop_provided(values, cls, api_key_field),
239
+ provider_path=provider_path,
240
+ base_url_env=base_url_env,
241
+ api_key_env=api_key_env,
242
+ default_base_url=default_base_url,
243
+ )
244
+ values[base_url_field] = config.base_url
245
+ if config.api_key is not None:
246
+ values[api_key_field] = config.api_key
247
+ return config
248
+
249
+
250
+ def _parse_gateway_metadata(headers: Any) -> dict[str, Any] | None:
251
+ """Parse LangSmith gateway metadata from HTTP response headers.
252
+
253
+ Provider integrations call this on the raw response headers so the parsed
254
+ result can be surfaced under `GATEWAY_METADATA_RESPONSE_KEY` and later
255
+ promoted to run metadata by the LangSmith tracer.
256
+
257
+ Args:
258
+ headers: The response headers.
259
+
260
+ Returns:
261
+ The deserialized gateway metadata, or None when the header is absent,
262
+ not a mapping, or not a JSON object.
263
+ """
264
+ if not isinstance(headers, Mapping):
265
+ return None
266
+
267
+ raw = headers.get(GATEWAY_METADATA_HEADER)
268
+ if not raw:
269
+ return None
270
+ try:
271
+ parsed = json.loads(raw)
272
+ except (json.JSONDecodeError, TypeError):
273
+ return None
274
+ return parsed if isinstance(parsed, dict) else None
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def merge_dicts(left: dict[str, Any], *others: dict[str, Any]) -> dict[str, Any]:
7
+ r"""Merge dictionaries.
8
+
9
+ Merge many dicts, handling specific scenarios where a key exists in both
10
+ dictionaries but has a value of `None` in `'left'`. In such cases, the method uses
11
+ the value from `'right'` for that key in the merged dictionary.
12
+
13
+ Args:
14
+ left: The first dictionary to merge.
15
+ others: The other dictionaries to merge.
16
+
17
+ Returns:
18
+ The merged dictionary.
19
+
20
+ Raises:
21
+ TypeError: If the key exists in both dictionaries but has a different type.
22
+ TypeError: If the value has an unsupported type.
23
+
24
+ Example:
25
+ If `left = {"function_call": {"arguments": None}}` and
26
+ `right = {"function_call": {"arguments": "{\n"}}`, then, after merging, for the
27
+ key `'function_call'`, the value from `'right'` is used, resulting in
28
+ `merged = {"function_call": {"arguments": "{\n"}}`.
29
+ """
30
+ merged = left.copy()
31
+ for right in others:
32
+ for right_k, right_v in right.items():
33
+ if right_k not in merged or (
34
+ right_v is not None and merged[right_k] is None
35
+ ):
36
+ merged[right_k] = right_v
37
+ elif right_v is None:
38
+ continue
39
+ elif type(merged[right_k]) is not type(right_v):
40
+ msg = (
41
+ f'additional_kwargs["{right_k}"] already exists in this message,'
42
+ " but with a different type."
43
+ )
44
+ raise TypeError(msg)
45
+ elif isinstance(merged[right_k], str):
46
+ # TODO: Add below special handling for 'type' key in 0.3 and remove
47
+ # merge_lists 'type' logic.
48
+ #
49
+ # if right_k == "type":
50
+ # if merged[right_k] == right_v:
51
+ # continue
52
+ # else:
53
+ # raise ValueError(
54
+ # "Unable to merge. Two different values seen for special "
55
+ # f"key 'type': {merged[right_k]} and {right_v}. 'type' "
56
+ # "should either occur once or have the same value across "
57
+ # "all dicts."
58
+ # )
59
+ if (right_k == "index" and merged[right_k].startswith("lc_")) or (
60
+ right_k in {"id", "output_version", "model_provider"}
61
+ and merged[right_k] == right_v
62
+ ):
63
+ continue
64
+ merged[right_k] += right_v
65
+ elif isinstance(merged[right_k], dict):
66
+ merged[right_k] = merge_dicts(merged[right_k], right_v)
67
+ elif isinstance(merged[right_k], list):
68
+ merged[right_k] = merge_lists(merged[right_k], right_v)
69
+ elif merged[right_k] == right_v:
70
+ continue
71
+ elif isinstance(merged[right_k], bool):
72
+ # `bool` is a subclass of `int`, so without this check differing
73
+ # booleans would fall into the int branch below and get summed,
74
+ # silently turning e.g. `True + False` into the int `1`.
75
+ msg = (
76
+ f"Additional kwargs key {right_k} already exists in left dict and "
77
+ f"value has unsupported type {type(merged[right_k])}."
78
+ )
79
+ raise TypeError(msg)
80
+ elif isinstance(merged[right_k], int):
81
+ # Preserve identification and temporal fields using last-wins strategy
82
+ # instead of summing:
83
+ # - index: identifies which tool call a chunk belongs to
84
+ # - created/timestamp: temporal values that shouldn't be accumulated
85
+ if right_k in {"index", "created", "timestamp"}:
86
+ merged[right_k] = right_v
87
+ else:
88
+ merged[right_k] += right_v
89
+ else:
90
+ msg = (
91
+ f"Additional kwargs key {right_k} already exists in left dict and "
92
+ f"value has unsupported type {type(merged[right_k])}."
93
+ )
94
+ raise TypeError(msg)
95
+ return merged
96
+
97
+
98
+ def merge_lists(left: list[Any] | None, *others: list[Any] | None) -> list[Any] | None:
99
+ """Add many lists, handling `None`.
100
+
101
+ Args:
102
+ left: The first list to merge.
103
+ others: The other lists to merge.
104
+
105
+ Returns:
106
+ The merged list.
107
+ """
108
+ merged = left.copy() if left is not None else None
109
+ for other in others:
110
+ if other is None:
111
+ continue
112
+ if merged is None:
113
+ merged = other.copy()
114
+ else:
115
+ for e in other:
116
+ if (
117
+ isinstance(e, dict)
118
+ and "index" in e
119
+ and (
120
+ isinstance(e["index"], int)
121
+ or (
122
+ isinstance(e["index"], str) and e["index"].startswith("lc_")
123
+ )
124
+ )
125
+ ):
126
+ to_merge = [
127
+ i
128
+ for i, e_left in enumerate(merged)
129
+ if (
130
+ isinstance(e_left, dict)
131
+ and "index" in e_left
132
+ and e_left["index"] == e["index"] # index matches
133
+ and ( # IDs not inconsistent
134
+ e_left.get("id") in {None, ""}
135
+ or e.get("id") in {None, ""}
136
+ or e_left.get("id") == e.get("id")
137
+ )
138
+ )
139
+ ]
140
+ if to_merge:
141
+ # TODO: Remove this once merge_dict is updated with special
142
+ # handling for 'type'.
143
+ if (left_type := merged[to_merge[0]].get("type")) and (
144
+ e.get("type") == "non_standard" and "value" in e
145
+ ):
146
+ if left_type != "non_standard":
147
+ # standard + non_standard
148
+ new_e: dict[str, Any] = {
149
+ "extras": {
150
+ k: v
151
+ for k, v in e["value"].items()
152
+ if k != "type"
153
+ }
154
+ }
155
+ else:
156
+ # non_standard + non_standard
157
+ new_e = {
158
+ "value": {
159
+ k: v
160
+ for k, v in e["value"].items()
161
+ if k != "type"
162
+ }
163
+ }
164
+ if "index" in e:
165
+ new_e["index"] = e["index"]
166
+ else:
167
+ new_e = (
168
+ {k: v for k, v in e.items() if k != "type"}
169
+ if "type" in e
170
+ else e
171
+ )
172
+ merged[to_merge[0]] = merge_dicts(merged[to_merge[0]], new_e)
173
+ else:
174
+ merged.append(e)
175
+ else:
176
+ merged.append(e)
177
+ return merged
178
+
179
+
180
+ def merge_obj(left: Any, right: Any) -> Any:
181
+ """Merge two objects.
182
+
183
+ It handles specific scenarios where a key exists in both dictionaries but has a
184
+ value of `None` in `'left'`. In such cases, the method uses the value from `'right'`
185
+ for that key in the merged dictionary.
186
+
187
+ Args:
188
+ left: The first object to merge.
189
+ right: The other object to merge.
190
+
191
+ Returns:
192
+ The merged object.
193
+
194
+ Raises:
195
+ TypeError: If the key exists in both dictionaries but has a different type.
196
+ ValueError: If the two objects cannot be merged.
197
+ """
198
+ if left is None or right is None:
199
+ return left if left is not None else right
200
+ if type(left) is not type(right):
201
+ msg = (
202
+ f"left and right are of different types. Left type: {type(left)}. Right "
203
+ f"type: {type(right)}."
204
+ )
205
+ raise TypeError(msg)
206
+ if isinstance(left, str):
207
+ return left + right
208
+ if isinstance(left, dict):
209
+ return merge_dicts(left, right)
210
+ if isinstance(left, list):
211
+ return merge_lists(left, right)
212
+ if left == right:
213
+ return left
214
+ msg = (
215
+ f"Unable to merge {left=} and {right=}. Both must be of type str, dict, or "
216
+ f"list, or else be two equal objects."
217
+ )
218
+ raise ValueError(msg)