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,1227 @@
1
+ """Azure OpenAI chat wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
8
+ from importlib import import_module
9
+ from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar
10
+
11
+ import openai
12
+ from coco_core.language_models import LanguageModelInput
13
+ from coco_core.language_models.chat_models import LangSmithParams
14
+ from coco_core.outputs import ChatGenerationChunk, ChatResult
15
+ from coco_core.runnables import Runnable
16
+ from coco_core.utils import from_env, secret_from_env
17
+ from coco_core.utils.pydantic import is_basemodel_subclass
18
+ from pydantic import BaseModel, Field, SecretStr, model_validator
19
+ from typing_extensions import Self
20
+
21
+ from coco_openai.chat_models.base import BaseChatOpenAI, _get_default_model_profile
22
+
23
+ if TYPE_CHECKING:
24
+ from coco_core.language_models import ModelProfile
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ _BM = TypeVar("_BM", bound=BaseModel)
30
+ _DictOrPydanticClass: TypeAlias = dict[str, Any] | type[_BM] | type
31
+ _DictOrPydantic: TypeAlias = dict | _BM
32
+ _AZURE_OPENAI_SCOPE = "https://cognitiveservices.azure.com/.default"
33
+ _AZURE_WORKLOAD_IDENTITY_ENV_VARS = (
34
+ "AZURE_CLIENT_ID",
35
+ "AZURE_TENANT_ID",
36
+ "AZURE_FEDERATED_TOKEN_FILE",
37
+ )
38
+
39
+
40
+ def _get_azure_workload_identity_token_providers() -> tuple[
41
+ Callable[[], str] | None, Callable[[], Awaitable[str]] | None
42
+ ]:
43
+ if not all(os.getenv(key) for key in _AZURE_WORKLOAD_IDENTITY_ENV_VARS):
44
+ return None, None
45
+ try:
46
+ azure_identity = import_module("azure.identity")
47
+ azure_identity_aio = import_module("azure.identity.aio")
48
+ except ImportError:
49
+ return None, None
50
+ sync_provider = azure_identity.get_bearer_token_provider(
51
+ azure_identity.WorkloadIdentityCredential(),
52
+ _AZURE_OPENAI_SCOPE,
53
+ )
54
+ async_provider = azure_identity_aio.get_bearer_token_provider(
55
+ azure_identity_aio.WorkloadIdentityCredential(),
56
+ _AZURE_OPENAI_SCOPE,
57
+ )
58
+ return sync_provider, async_provider
59
+
60
+
61
+ def _is_pydantic_class(obj: Any) -> bool:
62
+ return isinstance(obj, type) and is_basemodel_subclass(obj)
63
+
64
+
65
+ class AzureChatOpenAI(BaseChatOpenAI):
66
+ r"""Azure OpenAI chat model integration.
67
+
68
+ Setup:
69
+ Head to the Azure [OpenAI quickstart guide](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/chatgpt-quickstart?tabs=keyless%2Ctypescript-keyless%2Cpython-new%2Ccommand-line&pivots=programming-language-python)
70
+ to create your Azure OpenAI deployment.
71
+
72
+ Then install `trustcoco-openai` and set environment variables
73
+ `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT`:
74
+
75
+ ```bash
76
+ pip install -U trustcoco-openai
77
+
78
+ export AZURE_OPENAI_API_KEY="your-api-key"
79
+ export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
80
+ ```
81
+
82
+ Key init args — completion params:
83
+ azure_deployment:
84
+ Name of Azure OpenAI deployment to use.
85
+ temperature:
86
+ Sampling temperature.
87
+ max_tokens:
88
+ Max number of tokens to generate.
89
+ logprobs:
90
+ Whether to return logprobs.
91
+
92
+ Key init args — client params:
93
+ api_version:
94
+ Azure OpenAI REST API version to use (distinct from the version of the
95
+ underlying model). [See more on the different versions.](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning)
96
+ timeout:
97
+ Timeout for requests.
98
+ max_retries:
99
+ Max number of retries.
100
+ organization:
101
+ OpenAI organization ID. If not passed in will be read from env
102
+ var `OPENAI_ORG_ID`.
103
+ model:
104
+ The name of the underlying OpenAI model. Used for tracing and token
105
+ counting. Does not affect completion.
106
+ model_version:
107
+ The version of the underlying OpenAI model. Used for tracing and token
108
+ counting. Does not affect completion. E.g., `'0125'`, `'0125-preview'`, etc.
109
+
110
+ See full list of supported init args and their descriptions in the params section.
111
+
112
+ Instantiate:
113
+ ```python
114
+ from coco_openai import AzureChatOpenAI
115
+
116
+ model = AzureChatOpenAI(
117
+ azure_deployment="your-deployment",
118
+ api_version="2024-05-01-preview",
119
+ temperature=0,
120
+ max_tokens=None,
121
+ timeout=None,
122
+ max_retries=2,
123
+ # organization="...",
124
+ # model="gpt-35-turbo",
125
+ # model_version="0125",
126
+ # other params...
127
+ )
128
+ ```
129
+
130
+ !!! note
131
+ Any param which is not explicitly supported will be passed directly to the
132
+ `openai.AzureOpenAI.chat.completions.create(...)` API every time to the model is
133
+ invoked.
134
+
135
+ For example:
136
+
137
+ ```python
138
+ from coco_openai import AzureChatOpenAI
139
+ import openai
140
+
141
+ AzureChatOpenAI(..., logprobs=True).invoke(...)
142
+
143
+ # results in underlying API call of:
144
+
145
+ openai.AzureOpenAI(..).chat.completions.create(..., logprobs=True)
146
+
147
+ # which is also equivalent to:
148
+
149
+ AzureChatOpenAI(...).invoke(..., logprobs=True)
150
+ ```
151
+
152
+ Invoke:
153
+ ```python
154
+ messages = [
155
+ (
156
+ "system",
157
+ "You are a helpful translator. Translate the user sentence to French.",
158
+ ),
159
+ ("human", "I love programming."),
160
+ ]
161
+ model.invoke(messages)
162
+ ```
163
+
164
+ ```python
165
+ AIMessage(
166
+ content="J'adore programmer.",
167
+ usage_metadata={
168
+ "input_tokens": 28,
169
+ "output_tokens": 6,
170
+ "total_tokens": 34,
171
+ },
172
+ response_metadata={
173
+ "token_usage": {
174
+ "completion_tokens": 6,
175
+ "prompt_tokens": 28,
176
+ "total_tokens": 34,
177
+ },
178
+ "model_name": "gpt-5.5",
179
+ "system_fingerprint": "fp_7ec89fabc6",
180
+ "prompt_filter_results": [
181
+ {
182
+ "prompt_index": 0,
183
+ "content_filter_results": {
184
+ "hate": {"filtered": False, "severity": "safe"},
185
+ "self_harm": {"filtered": False, "severity": "safe"},
186
+ "sexual": {"filtered": False, "severity": "safe"},
187
+ "violence": {"filtered": False, "severity": "safe"},
188
+ },
189
+ }
190
+ ],
191
+ "finish_reason": "stop",
192
+ "logprobs": None,
193
+ "content_filter_results": {
194
+ "hate": {"filtered": False, "severity": "safe"},
195
+ "self_harm": {"filtered": False, "severity": "safe"},
196
+ "sexual": {"filtered": False, "severity": "safe"},
197
+ "violence": {"filtered": False, "severity": "safe"},
198
+ },
199
+ },
200
+ id="run-6d7a5282-0de0-4f27-9cc0-82a9db9a3ce9-0",
201
+ )
202
+ ```
203
+
204
+ Stream:
205
+ ```python
206
+ for chunk in model.stream(messages):
207
+ print(chunk.text, end="")
208
+ ```
209
+
210
+ ```python
211
+ AIMessageChunk(content="", id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f")
212
+ AIMessageChunk(content="J", id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f")
213
+ AIMessageChunk(content="'", id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f")
214
+ AIMessageChunk(content="ad", id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f")
215
+ AIMessageChunk(content="ore", id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f")
216
+ AIMessageChunk(content=" la", id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f")
217
+ AIMessageChunk(
218
+ content=" programm", id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f"
219
+ )
220
+ AIMessageChunk(content="ation", id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f")
221
+ AIMessageChunk(content=".", id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f")
222
+ AIMessageChunk(
223
+ content="",
224
+ response_metadata={
225
+ "finish_reason": "stop",
226
+ "model_name": "gpt-5.5",
227
+ "system_fingerprint": "fp_811936bd4f",
228
+ },
229
+ id="run-a6f294d3-0700-4f6a-abc2-c6ef1178c37f",
230
+ )
231
+ ```
232
+
233
+ ```python
234
+ stream = model.stream(messages)
235
+ full = next(stream)
236
+ for chunk in stream:
237
+ full += chunk
238
+ full
239
+ ```
240
+
241
+ ```python
242
+ AIMessageChunk(
243
+ content="J'adore la programmation.",
244
+ response_metadata={
245
+ "finish_reason": "stop",
246
+ "model_name": "gpt-5.5",
247
+ "system_fingerprint": "fp_811936bd4f",
248
+ },
249
+ id="run-ba60e41c-9258-44b8-8f3a-2f10599643b3",
250
+ )
251
+ ```
252
+
253
+ Async:
254
+ ```python
255
+ await model.ainvoke(messages)
256
+
257
+ # stream:
258
+ # async for chunk in (await model.astream(messages))
259
+
260
+ # batch:
261
+ # await model.abatch([messages])
262
+ ```
263
+
264
+ Tool calling:
265
+ ```python
266
+ from pydantic import BaseModel, Field
267
+
268
+
269
+ class GetWeather(BaseModel):
270
+ '''Get the current weather in a given location'''
271
+
272
+ location: str = Field(
273
+ ..., description="The city and state, e.g. San Francisco, CA"
274
+ )
275
+
276
+
277
+ class GetPopulation(BaseModel):
278
+ '''Get the current population in a given location'''
279
+
280
+ location: str = Field(
281
+ ..., description="The city and state, e.g. San Francisco, CA"
282
+ )
283
+
284
+
285
+ model_with_tools = model.bind_tools([GetWeather, GetPopulation])
286
+ ai_msg = model_with_tools.invoke(
287
+ "Which city is hotter today and which is bigger: LA or NY?"
288
+ )
289
+ ai_msg.tool_calls
290
+ ```
291
+
292
+ ```python
293
+ [
294
+ {
295
+ "name": "GetWeather",
296
+ "args": {"location": "Los Angeles, CA"},
297
+ "id": "call_6XswGD5Pqk8Tt5atYr7tfenU",
298
+ },
299
+ {
300
+ "name": "GetWeather",
301
+ "args": {"location": "New York, NY"},
302
+ "id": "call_ZVL15vA8Y7kXqOy3dtmQgeCi",
303
+ },
304
+ {
305
+ "name": "GetPopulation",
306
+ "args": {"location": "Los Angeles, CA"},
307
+ "id": "call_49CFW8zqC9W7mh7hbMLSIrXw",
308
+ },
309
+ {
310
+ "name": "GetPopulation",
311
+ "args": {"location": "New York, NY"},
312
+ "id": "call_6ghfKxV264jEfe1mRIkS3PE7",
313
+ },
314
+ ]
315
+ ```
316
+
317
+ Structured output:
318
+ ```python
319
+ from typing import Optional
320
+
321
+ from pydantic import BaseModel, Field
322
+
323
+
324
+ class Joke(BaseModel):
325
+ '''Joke to tell user.'''
326
+
327
+ setup: str = Field(description="The setup of the joke")
328
+ punchline: str = Field(description="The punchline to the joke")
329
+ rating: int | None = Field(
330
+ description="How funny the joke is, from 1 to 10"
331
+ )
332
+
333
+
334
+ structured_model = model.with_structured_output(Joke)
335
+ structured_model.invoke("Tell me a joke about cats")
336
+ ```
337
+
338
+ ```python
339
+ Joke(
340
+ setup="Why was the cat sitting on the computer?",
341
+ punchline="To keep an eye on the mouse!",
342
+ rating=None,
343
+ )
344
+ ```
345
+
346
+ See `AzureChatOpenAI.with_structured_output()` for more.
347
+
348
+ JSON mode:
349
+ ```python
350
+ json_model = model.bind(response_format={"type": "json_object"})
351
+ ai_msg = json_model.invoke(
352
+ "Return a JSON object with key 'random_ints' and a value of 10 random ints in [0-99]"
353
+ )
354
+ ai_msg.content
355
+ ```
356
+
357
+ ```python
358
+ '\\n{\\n "random_ints": [23, 87, 45, 12, 78, 34, 56, 90, 11, 67]\\n}'
359
+ ```
360
+
361
+ Image input:
362
+ ```python
363
+ import base64
364
+ import httpx
365
+ from coco_core.messages import HumanMessage
366
+
367
+ image_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
368
+ image_data = base64.b64encode(httpx.get(image_url).content).decode("utf-8")
369
+ message = HumanMessage(
370
+ content=[
371
+ {"type": "text", "text": "describe the weather in this image"},
372
+ {
373
+ "type": "image_url",
374
+ "image_url": {"url": f"data:image/jpeg;base64,{image_data}"},
375
+ },
376
+ ]
377
+ )
378
+ ai_msg = model.invoke([message])
379
+ ai_msg.content
380
+ ```
381
+
382
+ ```python
383
+ "The weather in the image appears to be quite pleasant. The sky is mostly clear"
384
+ ```
385
+
386
+ Token usage:
387
+ ```python
388
+ ai_msg = model.invoke(messages)
389
+ ai_msg.usage_metadata
390
+ ```
391
+
392
+ ```python
393
+ {"input_tokens": 28, "output_tokens": 5, "total_tokens": 33}
394
+ ```
395
+ Logprobs:
396
+ ```python
397
+ logprobs_model = model.bind(logprobs=True)
398
+ ai_msg = logprobs_model.invoke(messages)
399
+ ai_msg.response_metadata["logprobs"]
400
+ ```
401
+
402
+ ```python
403
+ {
404
+ "content": [
405
+ {
406
+ "token": "J",
407
+ "bytes": [74],
408
+ "logprob": -4.9617593e-06,
409
+ "top_logprobs": [],
410
+ },
411
+ {
412
+ "token": "'adore",
413
+ "bytes": [39, 97, 100, 111, 114, 101],
414
+ "logprob": -0.25202933,
415
+ "top_logprobs": [],
416
+ },
417
+ {
418
+ "token": " la",
419
+ "bytes": [32, 108, 97],
420
+ "logprob": -0.20141791,
421
+ "top_logprobs": [],
422
+ },
423
+ {
424
+ "token": " programmation",
425
+ "bytes": [
426
+ 32,
427
+ 112,
428
+ 114,
429
+ 111,
430
+ 103,
431
+ 114,
432
+ 97,
433
+ 109,
434
+ 109,
435
+ 97,
436
+ 116,
437
+ 105,
438
+ 111,
439
+ 110,
440
+ ],
441
+ "logprob": -1.9361265e-07,
442
+ "top_logprobs": [],
443
+ },
444
+ {
445
+ "token": ".",
446
+ "bytes": [46],
447
+ "logprob": -1.2233183e-05,
448
+ "top_logprobs": [],
449
+ },
450
+ ]
451
+ }
452
+ ```
453
+
454
+ Response metadata
455
+ ```python
456
+ ai_msg = model.invoke(messages)
457
+ ai_msg.response_metadata
458
+ ```
459
+
460
+ ```python
461
+ {
462
+ "token_usage": {
463
+ "completion_tokens": 6,
464
+ "prompt_tokens": 28,
465
+ "total_tokens": 34,
466
+ },
467
+ "model_name": "gpt-35-turbo",
468
+ "system_fingerprint": None,
469
+ "prompt_filter_results": [
470
+ {
471
+ "prompt_index": 0,
472
+ "content_filter_results": {
473
+ "hate": {"filtered": False, "severity": "safe"},
474
+ "self_harm": {"filtered": False, "severity": "safe"},
475
+ "sexual": {"filtered": False, "severity": "safe"},
476
+ "violence": {"filtered": False, "severity": "safe"},
477
+ },
478
+ }
479
+ ],
480
+ "finish_reason": "stop",
481
+ "logprobs": None,
482
+ "content_filter_results": {
483
+ "hate": {"filtered": False, "severity": "safe"},
484
+ "self_harm": {"filtered": False, "severity": "safe"},
485
+ "sexual": {"filtered": False, "severity": "safe"},
486
+ "violence": {"filtered": False, "severity": "safe"},
487
+ },
488
+ }
489
+ ```
490
+ """ # noqa: E501
491
+
492
+ azure_endpoint: str | None = Field(
493
+ default_factory=from_env("AZURE_OPENAI_ENDPOINT", default=None)
494
+ )
495
+ """Your Azure endpoint, including the resource.
496
+
497
+ Automatically inferred from env var `AZURE_OPENAI_ENDPOINT` if not provided.
498
+
499
+ Example: `https://example-resource.azure.openai.com/`
500
+ """
501
+ deployment_name: str | None = Field(default=None, alias="azure_deployment")
502
+ """A model deployment.
503
+
504
+ If given sets the base client URL to include `/deployments/{azure_deployment}`
505
+
506
+ !!! note
507
+ This means you won't be able to use non-deployment endpoints.
508
+ """
509
+ openai_api_version: str | None = Field(
510
+ alias="api_version",
511
+ default_factory=from_env("OPENAI_API_VERSION", default=None),
512
+ )
513
+ """Automatically inferred from env var `OPENAI_API_VERSION` if not provided."""
514
+ # Check OPENAI_API_KEY for backwards compatibility.
515
+ # TODO: Remove OPENAI_API_KEY support to avoid possible conflict when using
516
+ # other forms of azure credentials.
517
+ openai_api_key: SecretStr | None = Field(
518
+ alias="api_key",
519
+ default_factory=secret_from_env(
520
+ ["AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"], default=None
521
+ ),
522
+ )
523
+ """Automatically inferred from env var `AZURE_OPENAI_API_KEY` if not provided."""
524
+ azure_ad_token: SecretStr | None = Field(
525
+ default_factory=secret_from_env("AZURE_OPENAI_AD_TOKEN", default=None)
526
+ )
527
+ """Your Azure Active Directory token.
528
+
529
+ Automatically inferred from env var `AZURE_OPENAI_AD_TOKEN` if not provided.
530
+
531
+ For more, see [this page](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id).
532
+ """
533
+ azure_ad_token_provider: Callable[[], str] | None = None
534
+ """A function that returns an Azure Active Directory token.
535
+
536
+ Will be invoked on every sync request. For async requests,
537
+ will be invoked if `azure_ad_async_token_provider` is not provided.
538
+ """
539
+
540
+ azure_ad_async_token_provider: Callable[[], Awaitable[str]] | None = None
541
+ """A function that returns an Azure Active Directory token.
542
+
543
+ Will be invoked on every async request.
544
+ """
545
+
546
+ model_version: str = ""
547
+ """The version of the model (e.g. `'0125'` for `'gpt-3.5-0125'`).
548
+
549
+ Azure OpenAI doesn't return model version with the response by default so it must
550
+ be manually specified if you want to use this information downstream, e.g. when
551
+ calculating costs.
552
+
553
+ When you specify the version, it will be appended to the model name in the
554
+ response. Setting correct version will help you to calculate the cost properly.
555
+ Model version is not validated, so make sure you set it correctly to get the
556
+ correct cost.
557
+ """
558
+
559
+ openai_api_type: str | None = Field(
560
+ default_factory=from_env("OPENAI_API_TYPE", default="azure")
561
+ )
562
+ """Legacy, for `openai<1.0.0` support."""
563
+
564
+ validate_base_url: bool = True
565
+ """If legacy arg `openai_api_base` is passed in, try to infer if it is a
566
+ `base_url` or `azure_endpoint` and update client params accordingly.
567
+ """
568
+
569
+ model_name: str | None = Field(default=None, alias="model") # type: ignore[assignment]
570
+ """Name of the deployed OpenAI model.
571
+
572
+ Distinct from the Azure deployment name, which is set by the Azure user.
573
+ Used for tracing and token counting.
574
+
575
+ !!! warning
576
+
577
+ Does NOT affect completion.
578
+ """
579
+
580
+ disabled_params: dict[str, Any] | None = Field(default=None)
581
+ """Parameters of the OpenAI client or chat.completions endpoint that should be
582
+ disabled for the given model.
583
+
584
+ Should be specified as `{"param": None | ['val1', 'val2']}` where the key is the
585
+ parameter and the value is either None, meaning that parameter should never be
586
+ used, or it's a list of disabled values for the parameter.
587
+
588
+ For example, older models may not support the `'parallel_tool_calls'` parameter at
589
+ all, in which case `disabled_params={"parallel_tool_calls: None}` can ben passed
590
+ in.
591
+
592
+ If a parameter is disabled then it will not be used by default in any methods, e.g.
593
+ in
594
+ `coco_openai.chat_models.azure.AzureChatOpenAI.with_structured_output`.
595
+ However this does not prevent a user from directly passed in the parameter during
596
+ invocation.
597
+
598
+ By default, unless `model_name="gpt-4o"` is specified, then
599
+ `'parallel_tools_calls'` will be disabled.
600
+ """
601
+
602
+ max_tokens: int | None = Field(default=None, alias="max_completion_tokens") # type: ignore[assignment]
603
+ """Maximum number of tokens to generate."""
604
+
605
+ @classmethod
606
+ def get_lc_namespace(cls) -> list[str]:
607
+ """Get the namespace of the Coco object.
608
+
609
+ Returns:
610
+ `["coco", "chat_models", "azure_openai"]`
611
+ """
612
+ return ["coco", "chat_models", "azure_openai"]
613
+
614
+ @property
615
+ def lc_secrets(self) -> dict[str, str]:
616
+ """Get the mapping of secret environment variables."""
617
+ return {
618
+ "openai_api_key": "AZURE_OPENAI_API_KEY",
619
+ "azure_ad_token": "AZURE_OPENAI_AD_TOKEN",
620
+ }
621
+
622
+ @classmethod
623
+ def is_lc_serializable(cls) -> bool:
624
+ """Check if the class is serializable in coco."""
625
+ return True
626
+
627
+ @model_validator(mode="after")
628
+ def validate_environment(self) -> Self:
629
+ """Validate that api key and python package exists in environment."""
630
+ if self.n is not None and self.n < 1:
631
+ msg = "n must be at least 1."
632
+ raise ValueError(msg)
633
+ if self.n is not None and self.n > 1 and self.streaming:
634
+ msg = "n must be 1 when streaming."
635
+ raise ValueError(msg)
636
+
637
+ if self.disabled_params is None:
638
+ # As of 09-17-2024 'parallel_tool_calls' param is only supported for gpt-4o.
639
+ if self.model_name and self.model_name == "gpt-4o":
640
+ pass
641
+ else:
642
+ self.disabled_params = {"parallel_tool_calls": None}
643
+
644
+ # Check OPENAI_ORGANIZATION for backwards compatibility.
645
+ self.openai_organization = (
646
+ self.openai_organization
647
+ or os.getenv("OPENAI_ORG_ID")
648
+ or os.getenv("OPENAI_ORGANIZATION")
649
+ )
650
+
651
+ # Enable stream_usage by default if using default base URL and client
652
+ if all(
653
+ getattr(self, key, None) is None
654
+ for key in (
655
+ "stream_usage",
656
+ "openai_proxy",
657
+ "openai_api_base",
658
+ "base_url",
659
+ "client",
660
+ "root_client",
661
+ "async_client",
662
+ "root_async_client",
663
+ "http_client",
664
+ "http_async_client",
665
+ )
666
+ ):
667
+ self.stream_usage = True
668
+
669
+ # For backwards compatibility. Before openai v1, no distinction was made
670
+ # between azure_endpoint and base_url (openai_api_base).
671
+ openai_api_base = self.openai_api_base
672
+ if openai_api_base and self.validate_base_url:
673
+ if "/openai" not in openai_api_base:
674
+ msg = (
675
+ "As of openai>=1.0.0, Azure endpoints should be specified via "
676
+ "the `azure_endpoint` param not `openai_api_base` "
677
+ "(or alias `base_url`)."
678
+ )
679
+ raise ValueError(msg)
680
+ if self.deployment_name:
681
+ msg = (
682
+ "As of openai>=1.0.0, if `azure_deployment` (or alias "
683
+ "`deployment_name`) is specified then "
684
+ "`base_url` (or alias `openai_api_base`) should not be. "
685
+ "If specifying `azure_deployment`/`deployment_name` then use "
686
+ "`azure_endpoint` instead of `base_url`.\n\n"
687
+ "For example, you could specify:\n\n"
688
+ 'azure_endpoint="https://xxx.openai.azure.com/", '
689
+ 'azure_deployment="my-deployment"\n\n'
690
+ "Or you can equivalently specify:\n\n"
691
+ 'base_url="https://xxx.openai.azure.com/openai/deployments/my-deployment"'
692
+ )
693
+ raise ValueError(msg)
694
+
695
+ if not any(
696
+ (
697
+ self.openai_api_key,
698
+ self.azure_ad_token,
699
+ self.azure_ad_token_provider,
700
+ self.azure_ad_async_token_provider,
701
+ )
702
+ ):
703
+ sync_provider, async_provider = (
704
+ _get_azure_workload_identity_token_providers()
705
+ )
706
+ self.azure_ad_token_provider = sync_provider
707
+ self.azure_ad_async_token_provider = async_provider
708
+
709
+ client_params: dict = {
710
+ "api_version": self.openai_api_version,
711
+ "azure_endpoint": self.azure_endpoint,
712
+ "azure_deployment": self.deployment_name,
713
+ "api_key": (
714
+ self.openai_api_key.get_secret_value() if self.openai_api_key else None
715
+ ),
716
+ "azure_ad_token": (
717
+ self.azure_ad_token.get_secret_value() if self.azure_ad_token else None
718
+ ),
719
+ "azure_ad_token_provider": self.azure_ad_token_provider,
720
+ "organization": self.openai_organization,
721
+ "base_url": self.openai_api_base,
722
+ "timeout": self.request_timeout,
723
+ "default_headers": {
724
+ "User-Agent": "trustcoco-partner-python-azure-openai",
725
+ **(self.default_headers or {}),
726
+ },
727
+ "default_query": self.default_query,
728
+ }
729
+ if (
730
+ self.azure_ad_token
731
+ or self.azure_ad_token_provider
732
+ or self.azure_ad_async_token_provider
733
+ ):
734
+ client_params["api_key"] = None
735
+ if self.max_retries is not None:
736
+ client_params["max_retries"] = self.max_retries
737
+
738
+ if not self.client and (
739
+ not self.azure_ad_async_token_provider
740
+ or self.azure_ad_token
741
+ or self.azure_ad_token_provider
742
+ ):
743
+ sync_specific = {"http_client": self.http_client}
744
+ self.root_client = openai.AzureOpenAI(**client_params, **sync_specific) # type: ignore[arg-type]
745
+ self.client = self.root_client.chat.completions
746
+ if not self.async_client:
747
+ async_specific = {"http_client": self.http_async_client}
748
+
749
+ if self.azure_ad_async_token_provider:
750
+ client_params["azure_ad_token_provider"] = (
751
+ self.azure_ad_async_token_provider
752
+ )
753
+
754
+ self.root_async_client = openai.AsyncAzureOpenAI(
755
+ **client_params,
756
+ **async_specific, # type: ignore[arg-type]
757
+ )
758
+ self.async_client = self.root_async_client.chat.completions
759
+ return self
760
+
761
+ def _resolve_model_profile(self) -> ModelProfile | None:
762
+ if (self.model_name is not None) and (
763
+ profile := _get_default_model_profile(self.model_name) or None
764
+ ):
765
+ return profile
766
+ if self.deployment_name is not None:
767
+ return _get_default_model_profile(self.deployment_name) or None
768
+ return None
769
+
770
+ @property
771
+ def _identifying_params(self) -> dict[str, Any]:
772
+ """Get the identifying parameters."""
773
+ return {
774
+ "azure_deployment": self.deployment_name,
775
+ **super()._identifying_params,
776
+ }
777
+
778
+ @property
779
+ def _llm_type(self) -> str:
780
+ return "azure-openai-chat"
781
+
782
+ @property
783
+ def lc_attributes(self) -> dict[str, Any]:
784
+ """Get the attributes relevant to tracing."""
785
+ return {
786
+ "openai_api_type": self.openai_api_type,
787
+ "openai_api_version": self.openai_api_version,
788
+ }
789
+
790
+ @property
791
+ def _default_params(self) -> dict[str, Any]:
792
+ """Get the default parameters for calling Azure OpenAI API."""
793
+ params = super()._default_params
794
+ if "max_tokens" in params:
795
+ params["max_completion_tokens"] = params.pop("max_tokens")
796
+
797
+ return params
798
+
799
+ def _get_ls_params(
800
+ self, stop: list[str] | None = None, **kwargs: Any
801
+ ) -> LangSmithParams:
802
+ """Get the parameters used to invoke the model."""
803
+ params = super()._get_ls_params(stop=stop, **kwargs)
804
+ params["ls_provider"] = "azure"
805
+ if "model" in kwargs:
806
+ # Honor explicit per-call override resolved by super().
807
+ pass
808
+ elif self.model_name:
809
+ if self.model_version and self.model_version not in self.model_name:
810
+ params["ls_model_name"] = (
811
+ self.model_name + "-" + self.model_version.lstrip("-")
812
+ )
813
+ else:
814
+ params["ls_model_name"] = self.model_name
815
+ elif self.deployment_name:
816
+ params["ls_model_name"] = self.deployment_name
817
+ return params
818
+
819
+ def _create_chat_result(
820
+ self,
821
+ response: dict | openai.BaseModel,
822
+ generation_info: dict | None = None,
823
+ ) -> ChatResult:
824
+ chat_result = super()._create_chat_result(response, generation_info)
825
+
826
+ if not isinstance(response, dict):
827
+ # warnings=False due to https://github.com/openai/openai-python/issues/2872
828
+ response = response.model_dump(warnings=False)
829
+ for res in response["choices"]:
830
+ if res.get("finish_reason", None) == "content_filter":
831
+ msg = (
832
+ "Azure has not provided the response due to a content filter "
833
+ "being triggered"
834
+ )
835
+ raise ValueError(msg)
836
+
837
+ if "model" in response:
838
+ model = response["model"]
839
+ if self.model_version:
840
+ model = f"{model}-{self.model_version}"
841
+
842
+ chat_result.llm_output = chat_result.llm_output or {}
843
+ chat_result.llm_output["model_name"] = model
844
+ if "prompt_filter_results" in response:
845
+ chat_result.llm_output = chat_result.llm_output or {}
846
+ chat_result.llm_output["prompt_filter_results"] = response[
847
+ "prompt_filter_results"
848
+ ]
849
+ for chat_gen, response_choice in zip(
850
+ chat_result.generations, response["choices"], strict=False
851
+ ):
852
+ chat_gen.generation_info = chat_gen.generation_info or {}
853
+ chat_gen.generation_info["content_filter_results"] = response_choice.get(
854
+ "content_filter_results", {}
855
+ )
856
+
857
+ return chat_result
858
+
859
+ def _get_request_payload(
860
+ self,
861
+ input_: LanguageModelInput,
862
+ *,
863
+ stop: list[str] | None = None,
864
+ **kwargs: Any,
865
+ ) -> dict:
866
+ """Get the request payload, using deployment name for Azure Responses API."""
867
+ payload = super()._get_request_payload(input_, stop=stop, **kwargs)
868
+
869
+ # For Azure Responses API, use deployment name instead of model name
870
+ if (
871
+ self._use_responses_api(payload)
872
+ and not payload.get("model")
873
+ and self.deployment_name
874
+ ):
875
+ payload["model"] = self.deployment_name
876
+
877
+ return payload
878
+
879
+ def _stream(self, *args: Any, **kwargs: Any) -> Iterator[ChatGenerationChunk]:
880
+ """Route to Chat Completions or Responses API."""
881
+ if self._use_responses_api({**kwargs, **self.model_kwargs}):
882
+ return super()._stream_responses(*args, **kwargs)
883
+ return super()._stream(*args, **kwargs)
884
+
885
+ async def _astream(
886
+ self, *args: Any, **kwargs: Any
887
+ ) -> AsyncIterator[ChatGenerationChunk]:
888
+ """Route to Chat Completions or Responses API."""
889
+ if self._use_responses_api({**kwargs, **self.model_kwargs}):
890
+ async for chunk in super()._astream_responses(*args, **kwargs):
891
+ yield chunk
892
+ else:
893
+ async for chunk in super()._astream(*args, **kwargs):
894
+ yield chunk
895
+
896
+ def with_structured_output(
897
+ self,
898
+ schema: _DictOrPydanticClass | None = None,
899
+ *,
900
+ method: Literal["function_calling", "json_mode", "json_schema"] = "json_schema",
901
+ include_raw: bool = False,
902
+ strict: bool | None = None,
903
+ **kwargs: Any,
904
+ ) -> Runnable[LanguageModelInput, _DictOrPydantic]:
905
+ r"""Model wrapper that returns outputs formatted to match the given schema.
906
+
907
+ Args:
908
+ schema: The output schema. Can be passed in as:
909
+
910
+ - A JSON Schema,
911
+ - A `TypedDict` class,
912
+ - A Pydantic class,
913
+ - Or an OpenAI function/tool schema.
914
+
915
+ If `schema` is a Pydantic class then the model output will be a
916
+ Pydantic instance of that class, and the model-generated fields will be
917
+ validated by the Pydantic class. Otherwise the model output will be a
918
+ dict and will not be validated.
919
+
920
+ See `coco_core.utils.function_calling.convert_to_openai_tool` for
921
+ more on how to properly specify types and descriptions of schema fields
922
+ when specifying a Pydantic or `TypedDict` class.
923
+
924
+ method: The method for steering model generation, one of:
925
+
926
+ - `'json_schema'`:
927
+ Uses OpenAI's [Structured Output API](https://platform.openai.com/docs/guides/structured-outputs).
928
+ Supported only by models listed in OpenAI's Structured Output API
929
+ documentation.
930
+ - `'function_calling'`:
931
+ Uses OpenAI's tool-calling (formerly called function calling)
932
+ [API](https://platform.openai.com/docs/guides/function-calling)
933
+ - `'json_mode'`:
934
+ Uses OpenAI's [JSON mode](https://platform.openai.com/docs/guides/structured-outputs/json-mode).
935
+ Note that if using JSON mode then you must include instructions for
936
+ formatting the output into the desired schema into the model call
937
+
938
+ Learn more about the differences between the methods and which models
939
+ support which methods [here](https://platform.openai.com/docs/guides/structured-outputs/function-calling-vs-response-format).
940
+
941
+ include_raw:
942
+ If `False` then only the parsed structured output is returned.
943
+
944
+ If an error occurs during model output parsing it will be raised.
945
+
946
+ If `True` then both the raw model response (a `BaseMessage`) and the
947
+ parsed model response will be returned.
948
+
949
+ If an error occurs during output parsing it will be caught and returned
950
+ as well.
951
+
952
+ The final output is always a `dict` with keys `'raw'`, `'parsed'`, and
953
+ `'parsing_error'`.
954
+ strict:
955
+
956
+ - True:
957
+ Model output is guaranteed to exactly match the schema.
958
+ The input schema will also be validated according to the [supported schemas](https://platform.openai.com/docs/guides/structured-outputs/supported-schemas?api-mode=responses#supported-schemas).
959
+ - False:
960
+ Input schema will not be validated and model output will not be
961
+ validated.
962
+ - None:
963
+ `strict` argument will not be passed to the model.
964
+
965
+ If schema is specified via TypedDict or JSON schema, `strict` is not
966
+ enabled by default. Pass `strict=True` to enable it.
967
+
968
+ !!! note
969
+ `strict` can only be non-null if `method` is `'json_schema'`
970
+ or `'function_calling'`.
971
+ kwargs: Additional keyword args are passed through to the model.
972
+
973
+ Returns:
974
+ A `Runnable` that takes same inputs as a
975
+ `coco_core.language_models.chat.BaseChatModel`. If `include_raw` is
976
+ `False` and `schema` is a Pydantic class, `Runnable` outputs an instance
977
+ of `schema` (i.e., a Pydantic object). Otherwise, if `include_raw` is
978
+ `False` then `Runnable` outputs a `dict`.
979
+
980
+ If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:
981
+
982
+ - `'raw'`: `BaseMessage`
983
+ - `'parsed'`: `None` if there was a parsing error, otherwise the type
984
+ depends on the `schema` as described above.
985
+ - `'parsing_error'`: `BaseException | None`
986
+
987
+ !!! warning "Behavior changed in `trustcoco-openai` 0.3.0"
988
+
989
+ `method` default changed from "function_calling" to "json_schema".
990
+
991
+ !!! warning "Behavior changed in `trustcoco-openai` 0.3.12"
992
+
993
+ Support for `tools` added.
994
+
995
+ !!! warning "Behavior changed in `trustcoco-openai` 0.3.21"
996
+
997
+ Pass `kwargs` through to the model.
998
+
999
+ ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=False`, `strict=True`"
1000
+
1001
+ Note, OpenAI has a number of restrictions on what types of schemas can be
1002
+ provided if `strict` = True. When using Pydantic, our model cannot
1003
+ specify any Field metadata (like min/max constraints) and fields cannot
1004
+ have default values.
1005
+
1006
+ See all constraints [here](https://platform.openai.com/docs/guides/structured-outputs/supported-schemas).
1007
+
1008
+ ```python
1009
+ from typing import Optional
1010
+
1011
+ from coco_openai import AzureChatOpenAI
1012
+ from pydantic import BaseModel, Field
1013
+
1014
+
1015
+ class AnswerWithJustification(BaseModel):
1016
+ '''An answer to the user question along with justification for the answer.'''
1017
+
1018
+ answer: str
1019
+ justification: str | None = Field(
1020
+ default=..., description="A justification for the answer."
1021
+ )
1022
+
1023
+
1024
+ model = AzureChatOpenAI(
1025
+ azure_deployment="...", model="gpt-5.5", temperature=0
1026
+ )
1027
+ structured_model = model.with_structured_output(AnswerWithJustification)
1028
+
1029
+ structured_model.invoke(
1030
+ "What weighs more a pound of bricks or a pound of feathers"
1031
+ )
1032
+
1033
+ # -> AnswerWithJustification(
1034
+ # answer='They weigh the same',
1035
+ # 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.'
1036
+ # )
1037
+ ```
1038
+
1039
+ ??? note "Example: `schema=Pydantic` class, `method='function_calling'`, `include_raw=False`, `strict=False`"
1040
+
1041
+ ```python
1042
+ from typing import Optional
1043
+
1044
+ from coco_openai import AzureChatOpenAI
1045
+ from pydantic import BaseModel, Field
1046
+
1047
+
1048
+ class AnswerWithJustification(BaseModel):
1049
+ '''An answer to the user question along with justification for the answer.'''
1050
+
1051
+ answer: str
1052
+ justification: str | None = Field(
1053
+ default=..., description="A justification for the answer."
1054
+ )
1055
+
1056
+
1057
+ model = AzureChatOpenAI(
1058
+ azure_deployment="...", model="gpt-5.5", temperature=0
1059
+ )
1060
+ structured_model = model.with_structured_output(
1061
+ AnswerWithJustification, method="function_calling"
1062
+ )
1063
+
1064
+ structured_model.invoke(
1065
+ "What weighs more a pound of bricks or a pound of feathers"
1066
+ )
1067
+
1068
+ # -> AnswerWithJustification(
1069
+ # answer='They weigh the same',
1070
+ # 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.'
1071
+ # )
1072
+ ```
1073
+
1074
+ ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=True`"
1075
+
1076
+ ```python
1077
+ from coco_openai import AzureChatOpenAI
1078
+ from pydantic import BaseModel
1079
+
1080
+
1081
+ class AnswerWithJustification(BaseModel):
1082
+ '''An answer to the user question along with justification for the answer.'''
1083
+
1084
+ answer: str
1085
+ justification: str
1086
+
1087
+
1088
+ model = AzureChatOpenAI(
1089
+ azure_deployment="...", model="gpt-5.5", temperature=0
1090
+ )
1091
+ structured_model = model.with_structured_output(
1092
+ AnswerWithJustification, include_raw=True
1093
+ )
1094
+
1095
+ structured_model.invoke(
1096
+ "What weighs more a pound of bricks or a pound of feathers"
1097
+ )
1098
+ # -> {
1099
+ # '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'}]}),
1100
+ # '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.'),
1101
+ # 'parsing_error': None
1102
+ # }
1103
+ ```
1104
+
1105
+ ??? note "Example: `schema=TypedDict` class, `method='json_schema'`, `include_raw=False`, `strict=False`"
1106
+
1107
+ ```python
1108
+ from typing_extensions import Annotated, TypedDict
1109
+
1110
+ from coco_openai import AzureChatOpenAI
1111
+
1112
+
1113
+ class AnswerWithJustification(TypedDict):
1114
+ '''An answer to the user question along with justification for the answer.'''
1115
+
1116
+ answer: str
1117
+ justification: Annotated[
1118
+ str | None, None, "A justification for the answer."
1119
+ ]
1120
+
1121
+
1122
+ model = AzureChatOpenAI(
1123
+ azure_deployment="...", model="gpt-5.5", temperature=0
1124
+ )
1125
+ structured_model = model.with_structured_output(AnswerWithJustification)
1126
+
1127
+ structured_model.invoke(
1128
+ "What weighs more a pound of bricks or a pound of feathers"
1129
+ )
1130
+ # -> {
1131
+ # 'answer': 'They weigh the same',
1132
+ # '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.'
1133
+ # }
1134
+ ```
1135
+
1136
+ ??? note "Example: `schema=OpenAI` function schema, `method='json_schema'`, `include_raw=False`"
1137
+
1138
+ ```python
1139
+ from coco_openai import AzureChatOpenAI
1140
+
1141
+ oai_schema = {
1142
+ 'name': 'AnswerWithJustification',
1143
+ 'description': 'An answer to the user question along with justification for the answer.',
1144
+ 'parameters': {
1145
+ 'type': 'object',
1146
+ 'properties': {
1147
+ 'answer': {'type': 'string'},
1148
+ 'justification': {'description': 'A justification for the answer.', 'type': 'string'}
1149
+ },
1150
+ 'required': ['answer']
1151
+ }
1152
+
1153
+ model = AzureChatOpenAI(
1154
+ azure_deployment="...",
1155
+ model="gpt-5.5",
1156
+ temperature=0,
1157
+ )
1158
+ structured_model = model.with_structured_output(oai_schema)
1159
+
1160
+ structured_model.invoke(
1161
+ "What weighs more a pound of bricks or a pound of feathers"
1162
+ )
1163
+ # -> {
1164
+ # 'answer': 'They weigh the same',
1165
+ # '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.'
1166
+ # }
1167
+ ```
1168
+
1169
+ ??? note "Example: `schema=Pydantic` class, `method='json_mode'`, `include_raw=True`"
1170
+
1171
+ ```python
1172
+ from coco_openai import AzureChatOpenAI
1173
+ from pydantic import BaseModel
1174
+
1175
+
1176
+ class AnswerWithJustification(BaseModel):
1177
+ answer: str
1178
+ justification: str
1179
+
1180
+
1181
+ model = AzureChatOpenAI(
1182
+ azure_deployment="...",
1183
+ model="gpt-5.5",
1184
+ temperature=0,
1185
+ )
1186
+ structured_model = model.with_structured_output(
1187
+ AnswerWithJustification, method="json_mode", include_raw=True
1188
+ )
1189
+
1190
+ structured_model.invoke(
1191
+ "Answer the following question. "
1192
+ "Make sure to return a JSON blob with keys 'answer' and 'justification'.\\n\\n"
1193
+ "What's heavier a pound of bricks or a pound of feathers?"
1194
+ )
1195
+ # -> {
1196
+ # 'raw': AIMessage(content='{\\n "answer": "They are both the same weight.",\\n "justification": "Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight." \\n}'),
1197
+ # 'parsed': AnswerWithJustification(answer='They are both the same weight.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight.'),
1198
+ # 'parsing_error': None
1199
+ # }
1200
+ ```
1201
+
1202
+ ??? note "Example: `schema=None`, `method='json_mode'`, `include_raw=True`"
1203
+
1204
+ ```python
1205
+ structured_model = model.with_structured_output(
1206
+ method="json_mode", include_raw=True
1207
+ )
1208
+
1209
+ structured_model.invoke(
1210
+ "Answer the following question. "
1211
+ "Make sure to return a JSON blob with keys 'answer' and 'justification'.\\n\\n"
1212
+ "What's heavier a pound of bricks or a pound of feathers?"
1213
+ )
1214
+ # -> {
1215
+ # 'raw': AIMessage(content='{\\n "answer": "They are both the same weight.",\\n "justification": "Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight." \\n}'),
1216
+ # 'parsed': {
1217
+ # 'answer': 'They are both the same weight.',
1218
+ # 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight.'
1219
+ # },
1220
+ # 'parsing_error': None
1221
+ # }
1222
+ ```
1223
+
1224
+ """ # noqa: E501
1225
+ return super().with_structured_output(
1226
+ schema, method=method, include_raw=include_raw, strict=strict, **kwargs
1227
+ )