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,919 @@
1
+ """Base classes for OpenAI large language models. Chat models are in `chat_models/`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from collections.abc import AsyncIterator, Callable, Collection, Iterator, Mapping
8
+ from typing import Any, Literal
9
+
10
+ import openai
11
+ import tiktoken
12
+ from coco_core._api.deprecation import deprecated
13
+ from coco_core.callbacks import (
14
+ AsyncCallbackManagerForLLMRun,
15
+ CallbackManagerForLLMRun,
16
+ )
17
+ from coco_core.language_models.llms import BaseLLM
18
+ from coco_core.outputs import Generation, GenerationChunk, LLMResult
19
+ from coco_core.utils import get_pydantic_field_names
20
+ from coco_core.utils.utils import _build_model_kwargs, from_env, secret_from_env
21
+ from pydantic import ConfigDict, Field, SecretStr, model_validator
22
+ from typing_extensions import Self
23
+
24
+ from coco_openai._version import __version__
25
+ from coco_openai.data._profiles import _PROFILES
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def _update_token_usage(
31
+ keys: set[str], response: dict[str, Any], token_usage: dict[str, Any]
32
+ ) -> None:
33
+ """Update token usage."""
34
+ _keys_to_use = keys.intersection(response["usage"])
35
+ for _key in _keys_to_use:
36
+ if _key not in token_usage:
37
+ token_usage[_key] = response["usage"][_key]
38
+ else:
39
+ token_usage[_key] += response["usage"][_key]
40
+
41
+
42
+ def _stream_response_to_generation_chunk(
43
+ stream_response: dict[str, Any],
44
+ ) -> GenerationChunk:
45
+ """Convert a stream response to a generation chunk."""
46
+ if not stream_response["choices"]:
47
+ return GenerationChunk(text="")
48
+ return GenerationChunk(
49
+ text=stream_response["choices"][0]["text"] or "",
50
+ generation_info={
51
+ "finish_reason": stream_response["choices"][0].get("finish_reason", None),
52
+ "logprobs": stream_response["choices"][0].get("logprobs", None),
53
+ },
54
+ )
55
+
56
+
57
+ class BaseOpenAI(BaseLLM):
58
+ """Base OpenAI large language model class.
59
+
60
+ Setup:
61
+ Install `trustcoco-openai` and set environment variable `OPENAI_API_KEY`.
62
+
63
+ ```bash
64
+ pip install -U trustcoco-openai
65
+ export OPENAI_API_KEY="your-api-key"
66
+ ```
67
+
68
+ Key init args — completion params:
69
+ model_name:
70
+ Name of OpenAI model to use.
71
+ temperature:
72
+ Sampling temperature.
73
+ max_tokens:
74
+ Max number of tokens to generate.
75
+ top_p:
76
+ Total probability mass of tokens to consider at each step.
77
+ frequency_penalty:
78
+ Penalizes repeated tokens according to frequency.
79
+ presence_penalty:
80
+ Penalizes repeated tokens.
81
+ n:
82
+ How many completions to generate for each prompt.
83
+ best_of:
84
+ Generates best_of completions server-side and returns the "best".
85
+ logit_bias:
86
+ Adjust the probability of specific tokens being generated.
87
+ seed:
88
+ Seed for generation.
89
+ logprobs:
90
+ Include the log probabilities on the logprobs most likely output tokens.
91
+ streaming:
92
+ Whether to stream the results or not.
93
+
94
+ Key init args — client params:
95
+ openai_api_key:
96
+ OpenAI API key. If not passed in will be read from env var
97
+ `OPENAI_API_KEY`.
98
+ openai_api_base:
99
+ Base URL path for API requests, leave blank if not using a proxy or
100
+ service emulator. Falls back to env var `OPENAI_API_BASE`, then to
101
+ `OPENAI_BASE_URL` (read by the underlying SDK client).
102
+ openai_organization:
103
+ OpenAI organization ID. If not passed in will be read from env
104
+ var `OPENAI_ORG_ID`.
105
+ request_timeout:
106
+ Timeout for requests to OpenAI completion API.
107
+ max_retries:
108
+ Maximum number of retries to make when generating.
109
+ batch_size:
110
+ Batch size to use when passing multiple documents to generate.
111
+
112
+ See full list of supported init args and their descriptions in the params section.
113
+
114
+ Instantiate:
115
+ ```python
116
+ from coco_openai.llms.base import BaseOpenAI
117
+
118
+ model = BaseOpenAI(
119
+ model_name="gpt-3.5-turbo-instruct",
120
+ temperature=0.7,
121
+ max_tokens=256,
122
+ top_p=1,
123
+ frequency_penalty=0,
124
+ presence_penalty=0,
125
+ # openai_api_key="...",
126
+ # openai_api_base="...",
127
+ # openai_organization="...",
128
+ # other params...
129
+ )
130
+ ```
131
+
132
+ Invoke:
133
+ ```python
134
+ input_text = "The meaning of life is "
135
+ response = model.invoke(input_text)
136
+ print(response)
137
+ ```
138
+
139
+ ```txt
140
+ "a philosophical question that has been debated by thinkers and
141
+ scholars for centuries."
142
+ ```
143
+
144
+ Stream:
145
+ ```python
146
+ for chunk in model.stream(input_text):
147
+ print(chunk, end="")
148
+ ```
149
+ ```txt
150
+ a philosophical question that has been debated by thinkers and
151
+ scholars for centuries.
152
+ ```
153
+
154
+ Async:
155
+ ```python
156
+ response = await model.ainvoke(input_text)
157
+
158
+ # stream:
159
+ # async for chunk in model.astream(input_text):
160
+ # print(chunk, end="")
161
+
162
+ # batch:
163
+ # await model.abatch([input_text])
164
+ ```
165
+ ```
166
+ "a philosophical question that has been debated by thinkers and
167
+ scholars for centuries."
168
+ ```
169
+
170
+ """
171
+
172
+ client: Any = Field(default=None, exclude=True)
173
+
174
+ async_client: Any = Field(default=None, exclude=True)
175
+
176
+ model_name: str = Field(default="gpt-3.5-turbo-instruct", alias="model")
177
+ """Model name to use."""
178
+
179
+ temperature: float = 0.7
180
+ """What sampling temperature to use."""
181
+
182
+ max_tokens: int = 256
183
+ """The maximum number of tokens to generate in the completion.
184
+ -1 returns as many tokens as possible given the prompt and
185
+ the models maximal context size."""
186
+
187
+ top_p: float = 1
188
+ """Total probability mass of tokens to consider at each step."""
189
+
190
+ frequency_penalty: float = 0
191
+ """Penalizes repeated tokens according to frequency."""
192
+
193
+ presence_penalty: float = 0
194
+ """Penalizes repeated tokens."""
195
+
196
+ n: int = 1
197
+ """How many completions to generate for each prompt."""
198
+
199
+ best_of: int = 1
200
+ """Generates best_of completions server-side and returns the "best"."""
201
+
202
+ model_kwargs: dict[str, Any] = Field(default_factory=dict)
203
+ """Holds any model parameters valid for `create` call not explicitly specified."""
204
+
205
+ openai_api_key: SecretStr | None | Callable[[], str] = Field(
206
+ alias="api_key", default_factory=secret_from_env("OPENAI_API_KEY", default=None)
207
+ )
208
+ """Automatically inferred from env var `OPENAI_API_KEY` if not provided."""
209
+
210
+ openai_api_base: str | None = Field(
211
+ alias="base_url", default_factory=from_env("OPENAI_API_BASE", default=None)
212
+ )
213
+ """Base URL path for API requests, leave blank if not using a proxy or service
214
+ emulator.
215
+
216
+ Resolution order (first match wins):
217
+
218
+ 1. Explicit `base_url` (or `openai_api_base`) kwarg.
219
+ 2. Env var `OPENAI_API_BASE` (read by Coco at init).
220
+ 3. Env var `OPENAI_BASE_URL` (read by the underlying `openai` SDK client).
221
+ """
222
+
223
+ openai_organization: str | None = Field(
224
+ alias="organization",
225
+ default_factory=from_env(
226
+ ["OPENAI_ORG_ID", "OPENAI_ORGANIZATION"], default=None
227
+ ),
228
+ )
229
+ """Automatically inferred from env var `OPENAI_ORG_ID` if not provided."""
230
+
231
+ # to support explicit proxy for OpenAI
232
+ openai_proxy: str | None = Field(
233
+ default_factory=from_env("OPENAI_PROXY", default=None)
234
+ )
235
+
236
+ batch_size: int = 20
237
+ """Batch size to use when passing multiple documents to generate."""
238
+
239
+ request_timeout: float | tuple[float, float] | Any | None = Field(
240
+ default=None, alias="timeout"
241
+ )
242
+ """Timeout for requests to OpenAI completion API. Can be float, `httpx.Timeout` or
243
+ None."""
244
+
245
+ logit_bias: dict[str, float] | None = None
246
+ """Adjust the probability of specific tokens being generated."""
247
+
248
+ max_retries: int = 2
249
+ """Maximum number of retries to make when generating."""
250
+
251
+ seed: int | None = None
252
+ """Seed for generation"""
253
+
254
+ logprobs: int | None = None
255
+ """Include the log probabilities on the logprobs most likely output tokens,
256
+ as well the chosen tokens."""
257
+
258
+ streaming: bool = False
259
+ """Whether to stream the results or not."""
260
+
261
+ allowed_special: Literal["all"] | set[str] = set()
262
+ """Set of special tokens that are allowed。"""
263
+
264
+ disallowed_special: Literal["all"] | Collection[str] = "all"
265
+ """Set of special tokens that are not allowed。"""
266
+
267
+ tiktoken_model_name: str | None = None
268
+ """The model name to pass to tiktoken when using this class.
269
+
270
+ Tiktoken is used to count the number of tokens in documents to constrain
271
+ them to be under a certain limit.
272
+
273
+ By default, when set to `None`, this will be the same as the embedding model name.
274
+ However, there are some cases where you may want to use this `Embedding` class with
275
+ a model name not supported by tiktoken. This can include when using Azure embeddings
276
+ or when using one of the many model providers that expose an OpenAI-like
277
+ API but with different models. In those cases, in order to avoid erroring
278
+ when tiktoken is called, you can specify a model name to use here.
279
+ """
280
+
281
+ default_headers: Mapping[str, str] | None = None
282
+
283
+ default_query: Mapping[str, object] | None = None
284
+
285
+ # Configure a custom httpx client. See the
286
+ # [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
287
+ http_client: Any | None = None
288
+ """Optional `httpx.Client`.
289
+
290
+ Only used for sync invocations. Must specify `http_async_client` as well if you'd
291
+ like a custom client for async invocations.
292
+ """
293
+
294
+ http_async_client: Any | None = None
295
+ """Optional `httpx.AsyncClient`.
296
+
297
+ Only used for async invocations. Must specify `http_client` as well if you'd like a
298
+ custom client for sync invocations.
299
+ """
300
+
301
+ extra_body: Mapping[str, Any] | None = None
302
+ """Optional additional JSON properties to include in the request parameters when
303
+ making requests to OpenAI compatible APIs, such as vLLM."""
304
+
305
+ model_config = ConfigDict(populate_by_name=True)
306
+
307
+ @model_validator(mode="before")
308
+ @classmethod
309
+ def build_extra(cls, values: dict[str, Any]) -> Any:
310
+ """Build extra kwargs from additional params that were passed in."""
311
+ all_required_field_names = get_pydantic_field_names(cls)
312
+ return _build_model_kwargs(values, all_required_field_names)
313
+
314
+ @model_validator(mode="after")
315
+ def _set_openai_version(self) -> Self:
316
+ """Set package version in metadata."""
317
+ self._add_version("trustcoco-openai", __version__)
318
+ return self
319
+
320
+ @model_validator(mode="after")
321
+ def validate_environment(self) -> Self:
322
+ """Validate that api key and python package exists in environment."""
323
+ if self.n < 1:
324
+ msg = "n must be at least 1."
325
+ raise ValueError(msg)
326
+ if self.streaming and self.n > 1:
327
+ msg = "Cannot stream results when n > 1."
328
+ raise ValueError(msg)
329
+ if self.streaming and self.best_of > 1:
330
+ msg = "Cannot stream results when best_of > 1."
331
+ raise ValueError(msg)
332
+
333
+ # Resolve API key from SecretStr or Callable
334
+ api_key_value: str | Callable[[], str] | None = None
335
+ if self.openai_api_key is not None:
336
+ if isinstance(self.openai_api_key, SecretStr):
337
+ api_key_value = self.openai_api_key.get_secret_value()
338
+ elif callable(self.openai_api_key):
339
+ api_key_value = self.openai_api_key
340
+
341
+ client_params: dict = {
342
+ "api_key": api_key_value,
343
+ "organization": self.openai_organization,
344
+ "base_url": self.openai_api_base,
345
+ "timeout": self.request_timeout,
346
+ "max_retries": self.max_retries,
347
+ "default_headers": self.default_headers,
348
+ "default_query": self.default_query,
349
+ }
350
+ if not self.client:
351
+ sync_specific = {"http_client": self.http_client}
352
+ self.client = openai.OpenAI(**client_params, **sync_specific).completions # type: ignore[arg-type]
353
+ if not self.async_client:
354
+ async_specific = {"http_client": self.http_async_client}
355
+ self.async_client = openai.AsyncOpenAI(
356
+ **client_params,
357
+ **async_specific, # type: ignore[arg-type]
358
+ ).completions
359
+
360
+ return self
361
+
362
+ @property
363
+ def _default_params(self) -> dict[str, Any]:
364
+ """Get the default parameters for calling OpenAI API."""
365
+ normal_params: dict[str, Any] = {
366
+ "temperature": self.temperature,
367
+ "top_p": self.top_p,
368
+ "frequency_penalty": self.frequency_penalty,
369
+ "presence_penalty": self.presence_penalty,
370
+ "n": self.n,
371
+ "seed": self.seed,
372
+ "logprobs": self.logprobs,
373
+ }
374
+
375
+ if self.logit_bias is not None:
376
+ normal_params["logit_bias"] = self.logit_bias
377
+
378
+ if self.max_tokens is not None:
379
+ normal_params["max_tokens"] = self.max_tokens
380
+
381
+ if self.extra_body is not None:
382
+ normal_params["extra_body"] = self.extra_body
383
+
384
+ # Azure gpt-35-turbo doesn't support best_of
385
+ # don't specify best_of if it is 1
386
+ if self.best_of > 1:
387
+ normal_params["best_of"] = self.best_of
388
+
389
+ return {**normal_params, **self.model_kwargs}
390
+
391
+ def _stream(
392
+ self,
393
+ prompt: str,
394
+ stop: list[str] | None = None,
395
+ run_manager: CallbackManagerForLLMRun | None = None,
396
+ **kwargs: Any,
397
+ ) -> Iterator[GenerationChunk]:
398
+ params = {**self._invocation_params, **kwargs, "stream": True}
399
+ self.get_sub_prompts(params, [prompt], stop) # this mutates params
400
+ for stream_resp in self.client.create(prompt=prompt, **params):
401
+ if not isinstance(stream_resp, dict):
402
+ stream_resp = stream_resp.model_dump()
403
+ chunk = _stream_response_to_generation_chunk(stream_resp)
404
+
405
+ if run_manager:
406
+ run_manager.on_llm_new_token(
407
+ chunk.text,
408
+ chunk=chunk,
409
+ verbose=self.verbose,
410
+ logprobs=(
411
+ chunk.generation_info["logprobs"]
412
+ if chunk.generation_info
413
+ else None
414
+ ),
415
+ )
416
+ yield chunk
417
+
418
+ async def _astream(
419
+ self,
420
+ prompt: str,
421
+ stop: list[str] | None = None,
422
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
423
+ **kwargs: Any,
424
+ ) -> AsyncIterator[GenerationChunk]:
425
+ params = {**self._invocation_params, **kwargs, "stream": True}
426
+ self.get_sub_prompts(params, [prompt], stop) # this mutates params
427
+ async for stream_resp in await self.async_client.create(
428
+ prompt=prompt, **params
429
+ ):
430
+ if not isinstance(stream_resp, dict):
431
+ stream_resp = stream_resp.model_dump()
432
+ chunk = _stream_response_to_generation_chunk(stream_resp)
433
+
434
+ if run_manager:
435
+ await run_manager.on_llm_new_token(
436
+ chunk.text,
437
+ chunk=chunk,
438
+ verbose=self.verbose,
439
+ logprobs=(
440
+ chunk.generation_info["logprobs"]
441
+ if chunk.generation_info
442
+ else None
443
+ ),
444
+ )
445
+ yield chunk
446
+
447
+ def _generate(
448
+ self,
449
+ prompts: list[str],
450
+ stop: list[str] | None = None,
451
+ run_manager: CallbackManagerForLLMRun | None = None,
452
+ **kwargs: Any,
453
+ ) -> LLMResult:
454
+ """Call out to OpenAI's endpoint with k unique prompts.
455
+
456
+ Args:
457
+ prompts: The prompts to pass into the model.
458
+ stop: Optional list of stop words to use when generating.
459
+ run_manager: Optional callback manager to use for the call.
460
+
461
+ Returns:
462
+ The full LLM output.
463
+
464
+ Example:
465
+ ```python
466
+ response = openai.generate(["Tell me a joke."])
467
+ ```
468
+ """
469
+ # TODO: write a unit test for this
470
+ params = self._invocation_params
471
+ params = {**params, **kwargs}
472
+ sub_prompts = self.get_sub_prompts(params, prompts, stop)
473
+ choices = []
474
+ token_usage: dict[str, int] = {}
475
+ # Get the token usage from the response.
476
+ # Includes prompt, completion, and total tokens used.
477
+ _keys = {"completion_tokens", "prompt_tokens", "total_tokens"}
478
+ system_fingerprint: str | None = None
479
+ for _prompts in sub_prompts:
480
+ if self.streaming:
481
+ if len(_prompts) > 1:
482
+ msg = "Cannot stream results with multiple prompts."
483
+ raise ValueError(msg)
484
+
485
+ generation: GenerationChunk | None = None
486
+ for chunk in self._stream(_prompts[0], stop, run_manager, **kwargs):
487
+ if generation is None:
488
+ generation = chunk
489
+ else:
490
+ generation += chunk
491
+ if generation is None:
492
+ msg = "Generation is empty after streaming."
493
+ raise ValueError(msg)
494
+ choices.append(
495
+ {
496
+ "text": generation.text,
497
+ "finish_reason": (
498
+ generation.generation_info.get("finish_reason")
499
+ if generation.generation_info
500
+ else None
501
+ ),
502
+ "logprobs": (
503
+ generation.generation_info.get("logprobs")
504
+ if generation.generation_info
505
+ else None
506
+ ),
507
+ }
508
+ )
509
+ else:
510
+ response = self.client.create(prompt=_prompts, **params)
511
+ if not isinstance(response, dict):
512
+ # V1 client returns the response in an PyDantic object instead of
513
+ # dict. For the transition period, we deep convert it to dict.
514
+ response = response.model_dump()
515
+
516
+ # Sometimes the AI Model calling will get error, we should raise it.
517
+ # Otherwise, the next code 'choices.extend(response["choices"])'
518
+ # will throw a "TypeError: 'NoneType' object is not iterable" error
519
+ # to mask the true error. Because 'response["choices"]' is None.
520
+ if response.get("error"):
521
+ raise ValueError(response.get("error"))
522
+
523
+ choices.extend(response["choices"])
524
+ _update_token_usage(_keys, response, token_usage)
525
+ if not system_fingerprint:
526
+ system_fingerprint = response.get("system_fingerprint")
527
+ return self.create_llm_result(
528
+ choices, prompts, params, token_usage, system_fingerprint=system_fingerprint
529
+ )
530
+
531
+ async def _agenerate(
532
+ self,
533
+ prompts: list[str],
534
+ stop: list[str] | None = None,
535
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
536
+ **kwargs: Any,
537
+ ) -> LLMResult:
538
+ """Call out to OpenAI's endpoint async with k unique prompts."""
539
+ params = self._invocation_params
540
+ params = {**params, **kwargs}
541
+ sub_prompts = self.get_sub_prompts(params, prompts, stop)
542
+ choices = []
543
+ token_usage: dict[str, int] = {}
544
+ # Get the token usage from the response.
545
+ # Includes prompt, completion, and total tokens used.
546
+ _keys = {"completion_tokens", "prompt_tokens", "total_tokens"}
547
+ system_fingerprint: str | None = None
548
+ for _prompts in sub_prompts:
549
+ if self.streaming:
550
+ if len(_prompts) > 1:
551
+ msg = "Cannot stream results with multiple prompts."
552
+ raise ValueError(msg)
553
+
554
+ generation: GenerationChunk | None = None
555
+ async for chunk in self._astream(
556
+ _prompts[0], stop, run_manager, **kwargs
557
+ ):
558
+ if generation is None:
559
+ generation = chunk
560
+ else:
561
+ generation += chunk
562
+ if generation is None:
563
+ msg = "Generation is empty after streaming."
564
+ raise ValueError(msg)
565
+ choices.append(
566
+ {
567
+ "text": generation.text,
568
+ "finish_reason": (
569
+ generation.generation_info.get("finish_reason")
570
+ if generation.generation_info
571
+ else None
572
+ ),
573
+ "logprobs": (
574
+ generation.generation_info.get("logprobs")
575
+ if generation.generation_info
576
+ else None
577
+ ),
578
+ }
579
+ )
580
+ else:
581
+ response = await self.async_client.create(prompt=_prompts, **params)
582
+ if not isinstance(response, dict):
583
+ response = response.model_dump()
584
+ choices.extend(response["choices"])
585
+ _update_token_usage(_keys, response, token_usage)
586
+ return self.create_llm_result(
587
+ choices, prompts, params, token_usage, system_fingerprint=system_fingerprint
588
+ )
589
+
590
+ def get_sub_prompts(
591
+ self,
592
+ params: dict[str, Any],
593
+ prompts: list[str],
594
+ stop: list[str] | None = None,
595
+ ) -> list[list[str]]:
596
+ """Get the sub prompts for llm call."""
597
+ if stop is not None:
598
+ params["stop"] = stop
599
+ if params["max_tokens"] == -1:
600
+ if len(prompts) != 1:
601
+ msg = "max_tokens set to -1 not supported for multiple inputs."
602
+ raise ValueError(msg)
603
+ params["max_tokens"] = self.max_tokens_for_prompt(prompts[0])
604
+ return [
605
+ prompts[i : i + self.batch_size]
606
+ for i in range(0, len(prompts), self.batch_size)
607
+ ]
608
+
609
+ def create_llm_result(
610
+ self,
611
+ choices: Any,
612
+ prompts: list[str],
613
+ params: dict[str, Any],
614
+ token_usage: dict[str, int],
615
+ *,
616
+ system_fingerprint: str | None = None,
617
+ ) -> LLMResult:
618
+ """Create the LLMResult from the choices and prompts."""
619
+ generations = []
620
+ n = params.get("n", self.n)
621
+ for i, _ in enumerate(prompts):
622
+ sub_choices = choices[i * n : (i + 1) * n]
623
+ generations.append(
624
+ [
625
+ Generation(
626
+ text=choice["text"],
627
+ generation_info={
628
+ "finish_reason": choice.get("finish_reason"),
629
+ "logprobs": choice.get("logprobs"),
630
+ },
631
+ )
632
+ for choice in sub_choices
633
+ ]
634
+ )
635
+ llm_output = {"token_usage": token_usage, "model_name": self.model_name}
636
+ if system_fingerprint:
637
+ llm_output["system_fingerprint"] = system_fingerprint
638
+ return LLMResult(generations=generations, llm_output=llm_output)
639
+
640
+ @property
641
+ def _invocation_params(self) -> dict[str, Any]:
642
+ """Get the parameters used to invoke the model."""
643
+ return self._default_params
644
+
645
+ @property
646
+ def _identifying_params(self) -> Mapping[str, Any]:
647
+ """Get the identifying parameters."""
648
+ return {"model_name": self.model_name, **self._default_params}
649
+
650
+ @property
651
+ def _llm_type(self) -> str:
652
+ """Return type of llm."""
653
+ return "openai"
654
+
655
+ def get_token_ids(self, text: str) -> list[int]:
656
+ """Get the token IDs using the tiktoken package."""
657
+ if self.custom_get_token_ids is not None:
658
+ return self.custom_get_token_ids(text)
659
+ # tiktoken NOT supported for Python < 3.8
660
+ if sys.version_info[1] < 8:
661
+ return super().get_num_tokens(text)
662
+
663
+ model_name = self.tiktoken_model_name or self.model_name
664
+ try:
665
+ enc = tiktoken.encoding_for_model(model_name)
666
+ except KeyError:
667
+ enc = tiktoken.get_encoding("cl100k_base")
668
+
669
+ return enc.encode(
670
+ text,
671
+ allowed_special=self.allowed_special,
672
+ disallowed_special=self.disallowed_special,
673
+ )
674
+
675
+ @staticmethod
676
+ @deprecated(
677
+ since="1.2",
678
+ removal="2.0",
679
+ alternative=(
680
+ "the model profile's `max_input_tokens` field "
681
+ "(e.g. `ChatOpenAI(model=...).profile['max_input_tokens']`)"
682
+ ),
683
+ )
684
+ def modelname_to_contextsize(modelname: str) -> int:
685
+ """Return the maximum input context size for a model.
686
+
687
+ Prefers the model's profile (`max_input_tokens`) and falls back to a
688
+ mapping of legacy models that have no profile.
689
+
690
+ !!! warning "Changed in 1.2"
691
+
692
+ Now returns `max_input_tokens` from the model profile, which is the
693
+ input context window. Earlier releases returned a hand-maintained
694
+ number that for some newer models (e.g. `gpt-5`) reflected the
695
+ *total* context (input + output). Callers using the result as an
696
+ input-token budget are unaffected; callers using it as a combined
697
+ input+output budget should switch to the profile fields directly.
698
+
699
+ Args:
700
+ modelname: The modelname we want to know the context size for.
701
+
702
+ Returns:
703
+ The maximum input context size.
704
+
705
+ Example:
706
+ ```python
707
+ max_tokens = openai.modelname_to_contextsize("gpt-3.5-turbo-instruct")
708
+ ```
709
+ """
710
+ # Legacy models without a model profile.
711
+ legacy_token_mapping = {
712
+ "gpt-4-0314": 8192,
713
+ "gpt-4-0613": 8192,
714
+ "gpt-4-32k": 32768,
715
+ "gpt-4-32k-0314": 32768,
716
+ "gpt-4-32k-0613": 32768,
717
+ "gpt-4o-2024-05-13": 128_000,
718
+ "gpt-3.5-turbo-0301": 4096,
719
+ "gpt-3.5-turbo-0613": 4096,
720
+ "gpt-3.5-turbo-16k": 16385,
721
+ "gpt-3.5-turbo-16k-0613": 16385,
722
+ "gpt-3.5-turbo-instruct": 4096,
723
+ "text-ada-001": 2049,
724
+ "ada": 2049,
725
+ "text-babbage-001": 2040,
726
+ "babbage": 2049,
727
+ "text-curie-001": 2049,
728
+ "curie": 2049,
729
+ "davinci": 2049,
730
+ "text-davinci-003": 4097,
731
+ "text-davinci-002": 4097,
732
+ "code-davinci-002": 8001,
733
+ "code-davinci-001": 8001,
734
+ "code-cushman-002": 2048,
735
+ "code-cushman-001": 2048,
736
+ }
737
+
738
+ # handling finetuned models
739
+ if "ft-" in modelname:
740
+ modelname = modelname.split(":", maxsplit=1)[0]
741
+
742
+ profile = _PROFILES.get(modelname)
743
+ context_size = profile.get("max_input_tokens") if profile else None
744
+ if profile is not None and context_size is None:
745
+ logger.warning(
746
+ "Profile for model %s is missing `max_input_tokens`; "
747
+ "falling back to legacy mapping.",
748
+ modelname,
749
+ )
750
+ if context_size is None:
751
+ context_size = legacy_token_mapping.get(modelname)
752
+
753
+ if context_size is None:
754
+ known = sorted({*_PROFILES.keys(), *legacy_token_mapping.keys()})
755
+ msg = (
756
+ f"Unknown model: {modelname}. Please provide a valid OpenAI model "
757
+ "name, or read `max_input_tokens` from the model profile directly. "
758
+ "Known models are: " + ", ".join(known)
759
+ )
760
+ raise ValueError(msg)
761
+
762
+ return context_size
763
+
764
+ @property
765
+ def max_context_size(self) -> int:
766
+ """Get max context size for this model."""
767
+ return self.modelname_to_contextsize(self.model_name)
768
+
769
+ def max_tokens_for_prompt(self, prompt: str) -> int:
770
+ """Calculate the maximum number of tokens possible to generate for a prompt.
771
+
772
+ Args:
773
+ prompt: The prompt to pass into the model.
774
+
775
+ Returns:
776
+ The maximum number of tokens to generate for a prompt.
777
+
778
+ Example:
779
+ ```python
780
+ max_tokens = openai.max_tokens_for_prompt("Tell me a joke.")
781
+ ```
782
+ """
783
+ num_tokens = self.get_num_tokens(prompt)
784
+ return self.max_context_size - num_tokens
785
+
786
+
787
+ class OpenAI(BaseOpenAI):
788
+ """OpenAI completion model integration.
789
+
790
+ Setup:
791
+ Install `trustcoco-openai` and set environment variable `OPENAI_API_KEY`.
792
+
793
+ ```bash
794
+ pip install -U trustcoco-openai
795
+ export OPENAI_API_KEY="your-api-key"
796
+ ```
797
+
798
+ Key init args — completion params:
799
+ model:
800
+ Name of OpenAI model to use.
801
+ temperature:
802
+ Sampling temperature.
803
+ max_tokens:
804
+ Max number of tokens to generate.
805
+ logprobs:
806
+ Whether to return logprobs.
807
+ stream_options:
808
+ Configure streaming outputs, like whether to return token usage when
809
+ streaming (`{"include_usage": True}`).
810
+
811
+ Key init args — client params:
812
+ timeout:
813
+ Timeout for requests.
814
+ max_retries:
815
+ Max number of retries.
816
+ api_key:
817
+ OpenAI API key. If not passed in will be read from env var `OPENAI_API_KEY`.
818
+ base_url:
819
+ Base URL for API requests. Only specify if using a proxy or service
820
+ emulator.
821
+ organization:
822
+ OpenAI organization ID. If not passed in will be read from env
823
+ var `OPENAI_ORG_ID`.
824
+
825
+ See full list of supported init args and their descriptions in the params section.
826
+
827
+ Instantiate:
828
+ ```python
829
+ from coco_openai import OpenAI
830
+
831
+ model = OpenAI(
832
+ model="gpt-3.5-turbo-instruct",
833
+ temperature=0,
834
+ max_retries=2,
835
+ # api_key="...",
836
+ # base_url="...",
837
+ # organization="...",
838
+ # other params...
839
+ )
840
+ ```
841
+
842
+ Invoke:
843
+ ```python
844
+ input_text = "The meaning of life is "
845
+ model.invoke(input_text)
846
+ ```
847
+ ```txt
848
+ "a philosophical question that has been debated by thinkers and scholars for centuries."
849
+ ```
850
+
851
+ Stream:
852
+ ```python
853
+ for chunk in model.stream(input_text):
854
+ print(chunk, end="|")
855
+ ```
856
+ ```txt
857
+ a| philosophical| question| that| has| been| debated| by| thinkers| and| scholars| for| centuries|.
858
+ ```
859
+
860
+ ```python
861
+ "".join(model.stream(input_text))
862
+ ```
863
+ ```txt
864
+ "a philosophical question that has been debated by thinkers and scholars for centuries."
865
+ ```
866
+
867
+ Async:
868
+ ```python
869
+ await model.ainvoke(input_text)
870
+
871
+ # stream:
872
+ # async for chunk in (await model.astream(input_text)):
873
+ # print(chunk)
874
+
875
+ # batch:
876
+ # await model.abatch([input_text])
877
+ ```
878
+ ```txt
879
+ "a philosophical question that has been debated by thinkers and scholars for centuries."
880
+ ```
881
+ """ # noqa: E501
882
+
883
+ @classmethod
884
+ def get_lc_namespace(cls) -> list[str]:
885
+ """Get the namespace of the Coco object.
886
+
887
+ Returns:
888
+ `["coco", "llms", "openai"]`
889
+ """
890
+ return ["coco", "llms", "openai"]
891
+
892
+ @classmethod
893
+ def is_lc_serializable(cls) -> bool:
894
+ """Return whether this model can be serialized by Coco."""
895
+ return True
896
+
897
+ @property
898
+ def _invocation_params(self) -> dict[str, Any]:
899
+ return {"model": self.model_name, **super()._invocation_params}
900
+
901
+ @property
902
+ def lc_secrets(self) -> dict[str, str]:
903
+ """Mapping of secret keys to environment variables."""
904
+ return {"openai_api_key": "OPENAI_API_KEY"}
905
+
906
+ @property
907
+ def lc_attributes(self) -> dict[str, Any]:
908
+ """Coco attributes for this class."""
909
+ attributes: dict[str, Any] = {}
910
+ if self.openai_api_base:
911
+ attributes["openai_api_base"] = self.openai_api_base
912
+
913
+ if self.openai_organization:
914
+ attributes["openai_organization"] = self.openai_organization
915
+
916
+ if self.openai_proxy:
917
+ attributes["openai_proxy"] = self.openai_proxy
918
+
919
+ return attributes