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,820 @@
1
+ """Base classes for OpenAI embeddings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import warnings
7
+ from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
8
+ from typing import Any, Literal, cast
9
+
10
+ import openai
11
+ import tiktoken
12
+ from coco_core.embeddings import Embeddings
13
+ from coco_core.runnables.config import run_in_executor
14
+ from coco_core.utils import from_env, get_pydantic_field_names, secret_from_env
15
+ from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
16
+ from typing_extensions import Self
17
+
18
+ from coco_openai.chat_models._client_utils import _resolve_sync_and_async_api_keys
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ MAX_TOKENS_PER_REQUEST = 300000
23
+ """API limit per request for embedding tokens."""
24
+
25
+
26
+ def _process_batched_chunked_embeddings(
27
+ num_texts: int,
28
+ tokens: list[list[int] | str],
29
+ batched_embeddings: list[list[float]],
30
+ indices: list[int],
31
+ skip_empty: bool,
32
+ ) -> list[list[float] | None]:
33
+ # for each text, this is the list of embeddings (list of list of floats)
34
+ # corresponding to the chunks of the text
35
+ results: list[list[list[float]]] = [[] for _ in range(num_texts)]
36
+
37
+ # for each text, this is the token length of each chunk
38
+ # for transformers tokenization, this is the string length
39
+ # for tiktoken, this is the number of tokens
40
+ num_tokens_in_batch: list[list[int]] = [[] for _ in range(num_texts)]
41
+
42
+ for i in range(len(indices)):
43
+ if skip_empty and len(batched_embeddings[i]) == 1:
44
+ continue
45
+ results[indices[i]].append(batched_embeddings[i])
46
+ num_tokens_in_batch[indices[i]].append(len(tokens[i]))
47
+
48
+ # for each text, this is the final embedding
49
+ embeddings: list[list[float] | None] = []
50
+ for i in range(num_texts):
51
+ # an embedding for each chunk
52
+ _result: list[list[float]] = results[i]
53
+
54
+ if len(_result) == 0:
55
+ # this will be populated with the embedding of an empty string
56
+ # in the sync or async code calling this
57
+ embeddings.append(None)
58
+ continue
59
+
60
+ if len(_result) == 1:
61
+ # if only one embedding was produced, use it
62
+ embeddings.append(_result[0])
63
+ continue
64
+
65
+ # else we need to weighted average
66
+ # should be same as
67
+ # average = np.average(_result, axis=0, weights=num_tokens_in_batch[i])
68
+ total_weight = sum(num_tokens_in_batch[i])
69
+ average = [
70
+ sum(
71
+ val * weight
72
+ for val, weight in zip(embedding, num_tokens_in_batch[i], strict=False)
73
+ )
74
+ / total_weight
75
+ for embedding in zip(*_result, strict=False)
76
+ ]
77
+
78
+ # should be same as
79
+ # embeddings.append((average / np.linalg.norm(average)).tolist())
80
+ magnitude = sum(val**2 for val in average) ** 0.5
81
+ embeddings.append([val / magnitude for val in average])
82
+
83
+ return embeddings
84
+
85
+
86
+ class OpenAIEmbeddings(BaseModel, Embeddings):
87
+ """OpenAI embedding model integration.
88
+
89
+ Setup:
90
+ Install `coco_openai` and set environment variable `OPENAI_API_KEY`.
91
+
92
+ ```bash
93
+ pip install -U coco_openai
94
+ export OPENAI_API_KEY="your-api-key"
95
+ ```
96
+
97
+ Key init args — embedding params:
98
+ model:
99
+ Name of OpenAI model to use.
100
+ dimensions:
101
+ The number of dimensions the resulting output embeddings should have.
102
+ Only supported in `'text-embedding-3'` and later models.
103
+
104
+ Key init args — client params:
105
+ api_key:
106
+ OpenAI API key.
107
+ organization:
108
+ OpenAI organization ID. If not passed in will be read
109
+ from env var `OPENAI_ORG_ID`.
110
+ max_retries:
111
+ Maximum number of retries to make when generating.
112
+ request_timeout:
113
+ Timeout for requests to OpenAI completion API
114
+
115
+ See full list of supported init args and their descriptions in the params section.
116
+
117
+ Instantiate:
118
+ ```python
119
+ from coco_openai import OpenAIEmbeddings
120
+
121
+ embed = OpenAIEmbeddings(
122
+ model="text-embedding-3-large"
123
+ # With the `text-embedding-3` class
124
+ # of models, you can specify the size
125
+ # of the embeddings you want returned.
126
+ # dimensions=1024
127
+ )
128
+ ```
129
+
130
+ Embed single text:
131
+ ```python
132
+ input_text = "The meaning of life is 42"
133
+ vector = embeddings.embed_query("hello")
134
+ print(vector[:3])
135
+ ```
136
+ ```python
137
+ [-0.024603435769677162, -0.007543657906353474, 0.0039630369283258915]
138
+ ```
139
+
140
+ Embed multiple texts:
141
+ ```python
142
+ vectors = embeddings.embed_documents(["hello", "goodbye"])
143
+ # Showing only the first 3 coordinates
144
+ print(len(vectors))
145
+ print(vectors[0][:3])
146
+ ```
147
+ ```python
148
+ 2
149
+ [-0.024603435769677162, -0.007543657906353474, 0.0039630369283258915]
150
+ ```
151
+
152
+ Async:
153
+ ```python
154
+ await embed.aembed_query(input_text)
155
+ print(vector[:3])
156
+
157
+ # multiple:
158
+ # await embed.aembed_documents(input_texts)
159
+ ```
160
+ ```python
161
+ [-0.009100092574954033, 0.005071679595857859, -0.0029193938244134188]
162
+ ```
163
+
164
+ !!! note "OpenAI-compatible APIs (e.g. OpenRouter, Ollama, vLLM)"
165
+
166
+ When using a non-OpenAI provider, set
167
+ `check_embedding_ctx_length=False` to send raw text instead of tokens
168
+ (which many providers don't support), and optionally set
169
+ `encoding_format` to `'float'` to avoid base64 encoding issues:
170
+
171
+ ```python
172
+ from coco_openai import OpenAIEmbeddings
173
+
174
+ embeddings = OpenAIEmbeddings(
175
+ model="...",
176
+ base_url="...",
177
+ check_embedding_ctx_length=False,
178
+ )
179
+ ```
180
+
181
+ """
182
+
183
+ client: Any = Field(default=None, exclude=True)
184
+
185
+ async_client: Any = Field(default=None, exclude=True)
186
+
187
+ model: str = "text-embedding-ada-002"
188
+
189
+ dimensions: int | None = None
190
+ """The number of dimensions the resulting output embeddings should have.
191
+
192
+ Only supported in `'text-embedding-3'` and later models.
193
+ """
194
+
195
+ # to support Azure OpenAI Service custom deployment names
196
+ deployment: str | None = model
197
+
198
+ # TODO: Move to AzureOpenAIEmbeddings.
199
+ openai_api_version: str | None = Field(
200
+ default_factory=from_env("OPENAI_API_VERSION", default=None),
201
+ alias="api_version",
202
+ )
203
+ """Version of the OpenAI API to use.
204
+
205
+ Automatically inferred from env var `OPENAI_API_VERSION` if not provided.
206
+ """
207
+
208
+ # to support Azure OpenAI Service custom endpoints
209
+ openai_api_base: str | None = Field(
210
+ alias="base_url", default_factory=from_env("OPENAI_API_BASE", default=None)
211
+ )
212
+ """Base URL path for API requests, leave blank if not using a proxy or
213
+ service emulator.
214
+
215
+ Resolution order (first match wins):
216
+
217
+ 1. Explicit `base_url` (or `openai_api_base`) kwarg.
218
+ 2. Env var `OPENAI_API_BASE` (read by Coco at init).
219
+ 3. Env var `OPENAI_BASE_URL` (read by the underlying `openai` SDK client).
220
+ """
221
+
222
+ # to support Azure OpenAI Service custom endpoints
223
+ openai_api_type: str | None = Field(
224
+ default_factory=from_env("OPENAI_API_TYPE", default=None)
225
+ )
226
+
227
+ # to support explicit proxy for OpenAI
228
+ openai_proxy: str | None = Field(
229
+ default_factory=from_env("OPENAI_PROXY", default=None)
230
+ )
231
+
232
+ embedding_ctx_length: int = 8191
233
+ """The maximum number of tokens to embed at once."""
234
+
235
+ openai_api_key: (
236
+ SecretStr | None | Callable[[], str] | Callable[[], Awaitable[str]]
237
+ ) = Field(
238
+ alias="api_key", default_factory=secret_from_env("OPENAI_API_KEY", default=None)
239
+ )
240
+ """API key to use for API calls.
241
+
242
+ Automatically inferred from env var `OPENAI_API_KEY` if not provided.
243
+ """
244
+
245
+ openai_organization: str | None = Field(
246
+ alias="organization",
247
+ default_factory=from_env(
248
+ ["OPENAI_ORG_ID", "OPENAI_ORGANIZATION"], default=None
249
+ ),
250
+ )
251
+ """OpenAI organization ID to use for API calls.
252
+
253
+ Automatically inferred from env var `OPENAI_ORG_ID` if not provided.
254
+ """
255
+
256
+ allowed_special: Literal["all"] | set[str] | None = None
257
+
258
+ disallowed_special: Literal["all"] | set[str] | Sequence[str] | None = None
259
+
260
+ chunk_size: int = 1000
261
+ """Maximum number of texts to embed in each batch"""
262
+
263
+ max_retries: int = 2
264
+ """Maximum number of retries to make when generating."""
265
+
266
+ request_timeout: float | tuple[float, float] | Any | None = Field(
267
+ default=None, alias="timeout"
268
+ )
269
+ """Timeout for requests to OpenAI completion API.
270
+
271
+ Can be float, `httpx.Timeout` or `None`.
272
+ """
273
+
274
+ headers: Any = None
275
+
276
+ tiktoken_enabled: bool = True
277
+ """Set this to False to use HuggingFace `transformers` tokenization.
278
+
279
+ For non-OpenAI providers (OpenRouter, Ollama, vLLM, etc.), consider setting
280
+ `check_embedding_ctx_length=False` instead, as it bypasses tokenization
281
+ entirely.
282
+ """
283
+
284
+ tiktoken_model_name: str | None = None
285
+ """The model name to pass to tiktoken when using this class.
286
+
287
+ Tiktoken is used to count the number of tokens in documents to constrain
288
+ them to be under a certain limit.
289
+
290
+ By default, when set to `None`, this will be the same as the embedding model
291
+ name. However, there are some cases where you may want to use this
292
+ `Embedding` class with a model name not supported by tiktoken. This can
293
+ include when using Azure embeddings or when using one of the many model
294
+ providers that expose an OpenAI-like API but with different models. In those
295
+ cases, in order to avoid erroring when tiktoken is called, you can specify a
296
+ model name to use here.
297
+ """
298
+
299
+ show_progress_bar: bool = False
300
+ """Whether to show a progress bar when embedding."""
301
+
302
+ model_kwargs: dict[str, Any] = Field(default_factory=dict)
303
+ """Holds any model parameters valid for `create` call not explicitly specified."""
304
+
305
+ skip_empty: bool = False
306
+ """Whether to skip empty strings when embedding or raise an error."""
307
+
308
+ default_headers: Mapping[str, str] | None = None
309
+
310
+ default_query: Mapping[str, object] | None = None
311
+
312
+ # Configure a custom httpx client. See the
313
+ # [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
314
+
315
+ retry_min_seconds: int = 4
316
+ """Min number of seconds to wait between retries"""
317
+
318
+ retry_max_seconds: int = 20
319
+ """Max number of seconds to wait between retries"""
320
+
321
+ http_client: Any | None = None
322
+ """Optional `httpx.Client`.
323
+
324
+ Only used for sync invocations. Must specify `http_async_client` as well if
325
+ you'd like a custom client for async invocations.
326
+ """
327
+
328
+ http_async_client: Any | None = None
329
+ """Optional `httpx.AsyncClient`.
330
+
331
+ Only used for async invocations. Must specify `http_client` as well if you'd
332
+ like a custom client for sync invocations.
333
+ """
334
+
335
+ check_embedding_ctx_length: bool = True
336
+ """Whether to check the token length of inputs and automatically split inputs
337
+ longer than `embedding_ctx_length`.
338
+
339
+ Set to `False` to send raw text strings directly to the API instead of
340
+ tokenizing. Useful for many non-OpenAI providers (e.g. OpenRouter, Ollama,
341
+ vLLM).
342
+ """
343
+
344
+ model_config = ConfigDict(
345
+ extra="forbid", populate_by_name=True, protected_namespaces=()
346
+ )
347
+
348
+ @model_validator(mode="before")
349
+ @classmethod
350
+ def build_extra(cls, values: dict[str, Any]) -> Any:
351
+ """Build extra kwargs from additional params that were passed in."""
352
+ all_required_field_names = get_pydantic_field_names(cls)
353
+ extra = values.get("model_kwargs", {})
354
+ for field_name in list(values):
355
+ if field_name in extra:
356
+ msg = f"Found {field_name} supplied twice."
357
+ raise ValueError(msg)
358
+ if field_name not in all_required_field_names:
359
+ warnings.warn(
360
+ f"""WARNING! {field_name} is not default parameter.
361
+ {field_name} was transferred to model_kwargs.
362
+ Please confirm that {field_name} is what you intended."""
363
+ )
364
+ extra[field_name] = values.pop(field_name)
365
+
366
+ invalid_model_kwargs = all_required_field_names.intersection(extra.keys())
367
+ if invalid_model_kwargs:
368
+ msg = (
369
+ f"Parameters {invalid_model_kwargs} should be specified explicitly. "
370
+ f"Instead they were passed in as part of `model_kwargs` parameter."
371
+ )
372
+ raise ValueError(msg)
373
+
374
+ values["model_kwargs"] = extra
375
+ return values
376
+
377
+ @model_validator(mode="after")
378
+ def validate_environment(self) -> Self:
379
+ """Validate that api key and python package exists in environment."""
380
+ if self.openai_api_type in ("azure", "azure_ad", "azuread"):
381
+ msg = (
382
+ "If you are using Azure, please use the `AzureOpenAIEmbeddings` class."
383
+ )
384
+ raise ValueError(msg)
385
+
386
+ # Resolve API key from SecretStr or Callable
387
+ sync_api_key_value: str | Callable[[], str] | None = None
388
+ async_api_key_value: str | Callable[[], Awaitable[str]] | None = None
389
+
390
+ if self.openai_api_key is not None:
391
+ # Because OpenAI and AsyncOpenAI clients support either sync or async
392
+ # callables for the API key, we need to resolve separate values here.
393
+ sync_api_key_value, async_api_key_value = _resolve_sync_and_async_api_keys(
394
+ self.openai_api_key
395
+ )
396
+
397
+ client_params: dict = {
398
+ "organization": self.openai_organization,
399
+ "base_url": self.openai_api_base,
400
+ "timeout": self.request_timeout,
401
+ "max_retries": self.max_retries,
402
+ "default_headers": self.default_headers,
403
+ "default_query": self.default_query,
404
+ }
405
+
406
+ if self.openai_proxy and (self.http_client or self.http_async_client):
407
+ openai_proxy = self.openai_proxy
408
+ http_client = self.http_client
409
+ http_async_client = self.http_async_client
410
+ msg = (
411
+ "Cannot specify 'openai_proxy' if one of "
412
+ "'http_client'/'http_async_client' is already specified. Received:\n"
413
+ f"{openai_proxy=}\n{http_client=}\n{http_async_client=}"
414
+ )
415
+ raise ValueError(msg)
416
+ if not self.client:
417
+ if sync_api_key_value is None:
418
+ # No valid sync API key, leave client as None and raise informative
419
+ # error on invocation.
420
+ self.client = None
421
+ else:
422
+ if self.openai_proxy and not self.http_client:
423
+ try:
424
+ from coco_openai._compat import httpx
425
+ except ImportError as e:
426
+ msg = (
427
+ "Could not import the httpx package used by the "
428
+ "OpenAI SDK. It is normally installed alongside "
429
+ "`openai`; reinstall with `pip install openai`."
430
+ )
431
+ raise ImportError(msg) from e
432
+ self.http_client = httpx.Client(proxy=self.openai_proxy)
433
+ sync_specific = {
434
+ "http_client": self.http_client,
435
+ "api_key": sync_api_key_value,
436
+ }
437
+ self.client = openai.OpenAI(**client_params, **sync_specific).embeddings # type: ignore[arg-type]
438
+ if not self.async_client:
439
+ if self.openai_proxy and not self.http_async_client:
440
+ try:
441
+ from coco_openai._compat import httpx
442
+ except ImportError as e:
443
+ msg = (
444
+ "Could not import the httpx package used by the "
445
+ "OpenAI SDK. It is normally installed alongside "
446
+ "`openai`; reinstall with `pip install openai`."
447
+ )
448
+ raise ImportError(msg) from e
449
+ self.http_async_client = httpx.AsyncClient(proxy=self.openai_proxy)
450
+ async_specific = {
451
+ "http_client": self.http_async_client,
452
+ "api_key": async_api_key_value,
453
+ }
454
+ self.async_client = openai.AsyncOpenAI(
455
+ **client_params,
456
+ **async_specific, # type: ignore[arg-type]
457
+ ).embeddings
458
+ return self
459
+
460
+ @property
461
+ def _invocation_params(self) -> dict[str, Any]:
462
+ params: dict = {"model": self.model, **self.model_kwargs}
463
+ if self.dimensions is not None:
464
+ params["dimensions"] = self.dimensions
465
+ return params
466
+
467
+ def _ensure_sync_client_available(self) -> None:
468
+ """Check that sync client is available, raise error if not."""
469
+ if self.client is None:
470
+ msg = (
471
+ "Sync client is not available. This happens when an async callable "
472
+ "was provided for the API key. Use async methods (ainvoke, astream) "
473
+ "instead, or provide a string or sync callable for the API key."
474
+ )
475
+ raise ValueError(msg)
476
+
477
+ def _tokenize(
478
+ self, texts: list[str], chunk_size: int
479
+ ) -> tuple[Iterable[int], list[list[int] | str], list[int], list[int]]:
480
+ """Tokenize and batch input texts.
481
+
482
+ Splits texts based on `embedding_ctx_length` and groups them into batches
483
+ of size `chunk_size`.
484
+
485
+ Args:
486
+ texts: The list of texts to tokenize.
487
+ chunk_size: The maximum number of texts to include in a single batch.
488
+
489
+ Returns:
490
+ A tuple containing:
491
+ 1. An iterable of starting indices in the token list for each batch.
492
+ 2. A list of tokenized texts (token arrays for tiktoken, strings for
493
+ HuggingFace).
494
+ 3. An iterable mapping each token array to the index of the original
495
+ text. Same length as the token list.
496
+ 4. A list of token counts for each tokenized text.
497
+ """
498
+ tokens: list[list[int] | str] = []
499
+ indices: list[int] = []
500
+ token_counts: list[int] = []
501
+ model_name = self.tiktoken_model_name or self.model
502
+
503
+ # If tiktoken flag set to False
504
+ if not self.tiktoken_enabled:
505
+ try:
506
+ from transformers import AutoTokenizer
507
+ except ImportError:
508
+ msg = (
509
+ "Could not import transformers python package. "
510
+ "This is needed for OpenAIEmbeddings to work without "
511
+ "`tiktoken`. Please install it with `pip install transformers`. "
512
+ )
513
+ raise ValueError(msg)
514
+
515
+ tokenizer = AutoTokenizer.from_pretrained(
516
+ pretrained_model_name_or_path=model_name
517
+ )
518
+ for i, text in enumerate(texts):
519
+ # Tokenize the text using HuggingFace transformers
520
+ tokenized: list[int] = tokenizer.encode(text, add_special_tokens=False)
521
+
522
+ # Split tokens into chunks respecting the embedding_ctx_length
523
+ for j in range(0, len(tokenized), self.embedding_ctx_length):
524
+ token_chunk: list[int] = tokenized[
525
+ j : j + self.embedding_ctx_length
526
+ ]
527
+
528
+ # Convert token IDs back to a string
529
+ chunk_text: str = tokenizer.decode(token_chunk)
530
+ tokens.append(chunk_text)
531
+ indices.append(i)
532
+ token_counts.append(len(token_chunk))
533
+ else:
534
+ try:
535
+ encoding = tiktoken.encoding_for_model(model_name)
536
+ except KeyError:
537
+ encoding = tiktoken.get_encoding("cl100k_base")
538
+ encoder_kwargs: dict[str, Any] = {
539
+ k: v
540
+ for k, v in {
541
+ "allowed_special": self.allowed_special,
542
+ "disallowed_special": self.disallowed_special,
543
+ }.items()
544
+ if v is not None
545
+ }
546
+ for i, text in enumerate(texts):
547
+ if self.model.endswith("001"):
548
+ # See: https://github.com/openai/openai-python/
549
+ # issues/418#issuecomment-1525939500
550
+ # replace newlines, which can negatively affect performance.
551
+ text = text.replace("\n", " ")
552
+
553
+ if encoder_kwargs:
554
+ token = encoding.encode(text, **encoder_kwargs)
555
+ else:
556
+ token = encoding.encode_ordinary(text)
557
+
558
+ # Split tokens into chunks respecting the embedding_ctx_length
559
+ for j in range(0, len(token), self.embedding_ctx_length):
560
+ tokens.append(token[j : j + self.embedding_ctx_length])
561
+ indices.append(i)
562
+ token_counts.append(len(token[j : j + self.embedding_ctx_length]))
563
+
564
+ if self.show_progress_bar:
565
+ try:
566
+ from tqdm.auto import tqdm
567
+
568
+ _iter: Iterable = tqdm(range(0, len(tokens), chunk_size))
569
+ except ImportError:
570
+ _iter = range(0, len(tokens), chunk_size)
571
+ else:
572
+ _iter = range(0, len(tokens), chunk_size)
573
+ return _iter, tokens, indices, token_counts
574
+
575
+ # please refer to
576
+ # https://github.com/openai/openai-cookbook/blob/main/examples/Embedding_long_inputs.ipynb
577
+ def _get_len_safe_embeddings(
578
+ self,
579
+ texts: list[str],
580
+ *,
581
+ engine: str,
582
+ chunk_size: int | None = None,
583
+ **kwargs: Any,
584
+ ) -> list[list[float]]:
585
+ """Generate length-safe embeddings for a list of texts.
586
+
587
+ This method handles tokenization and embedding generation, respecting the
588
+ `embedding_ctx_length` and `chunk_size`. Supports both `tiktoken` and
589
+ HuggingFace `transformers` based on the `tiktoken_enabled` flag.
590
+
591
+ Args:
592
+ texts: The list of texts to embed.
593
+ engine: The engine or model to use for embeddings.
594
+ chunk_size: The size of chunks for processing embeddings.
595
+
596
+ Returns:
597
+ A list of embeddings for each input text.
598
+ """
599
+ _chunk_size = chunk_size or self.chunk_size
600
+ client_kwargs = {**self._invocation_params, **kwargs}
601
+ _iter, tokens, indices, token_counts = self._tokenize(texts, _chunk_size)
602
+ batched_embeddings: list[list[float]] = []
603
+
604
+ # Process in batches respecting the token limit
605
+ i = 0
606
+ while i < len(tokens):
607
+ # Determine how many chunks we can include in this batch
608
+ batch_token_count = 0
609
+ batch_end = i
610
+
611
+ for j in range(i, min(i + _chunk_size, len(tokens))):
612
+ chunk_tokens = token_counts[j]
613
+ # Check if adding this chunk would exceed the limit
614
+ if batch_token_count + chunk_tokens > MAX_TOKENS_PER_REQUEST:
615
+ if batch_end == i:
616
+ # Single chunk exceeds limit - handle it anyway
617
+ batch_end = j + 1
618
+ break
619
+ batch_token_count += chunk_tokens
620
+ batch_end = j + 1
621
+
622
+ # Make API call with this batch
623
+ batch_tokens = tokens[i:batch_end]
624
+ response = self.client.create(input=batch_tokens, **client_kwargs)
625
+ if not isinstance(response, dict):
626
+ response = response.model_dump()
627
+ batched_embeddings.extend(r["embedding"] for r in response["data"])
628
+
629
+ i = batch_end
630
+
631
+ embeddings = _process_batched_chunked_embeddings(
632
+ len(texts), tokens, batched_embeddings, indices, self.skip_empty
633
+ )
634
+ _cached_empty_embedding: list[float] | None = None
635
+
636
+ def empty_embedding() -> list[float]:
637
+ nonlocal _cached_empty_embedding
638
+ if _cached_empty_embedding is None:
639
+ average_embedded = self.client.create(input="", **client_kwargs)
640
+ if not isinstance(average_embedded, dict):
641
+ average_embedded = average_embedded.model_dump()
642
+ _cached_empty_embedding = average_embedded["data"][0]["embedding"]
643
+ return _cached_empty_embedding
644
+
645
+ return [e if e is not None else empty_embedding() for e in embeddings]
646
+
647
+ # please refer to
648
+ # https://github.com/openai/openai-cookbook/blob/main/examples/Embedding_long_inputs.ipynb
649
+ async def _aget_len_safe_embeddings(
650
+ self,
651
+ texts: list[str],
652
+ *,
653
+ engine: str,
654
+ chunk_size: int | None = None,
655
+ **kwargs: Any,
656
+ ) -> list[list[float]]:
657
+ """Asynchronously generate length-safe embeddings for a list of texts.
658
+
659
+ This method handles tokenization and embedding generation, respecting the
660
+ `embedding_ctx_length` and `chunk_size`. Supports both `tiktoken` and
661
+ HuggingFace `transformers` based on the `tiktoken_enabled` flag.
662
+
663
+ Args:
664
+ texts: The list of texts to embed.
665
+ engine: The engine or model to use for embeddings.
666
+ chunk_size: The size of chunks for processing embeddings.
667
+
668
+ Returns:
669
+ A list of embeddings for each input text.
670
+ """
671
+ _chunk_size = chunk_size or self.chunk_size
672
+ client_kwargs = {**self._invocation_params, **kwargs}
673
+ _iter, tokens, indices, token_counts = await run_in_executor(
674
+ None, self._tokenize, texts, _chunk_size
675
+ )
676
+ batched_embeddings: list[list[float]] = []
677
+
678
+ # Process in batches respecting the token limit
679
+ i = 0
680
+ while i < len(tokens):
681
+ # Determine how many chunks we can include in this batch
682
+ batch_token_count = 0
683
+ batch_end = i
684
+
685
+ for j in range(i, min(i + _chunk_size, len(tokens))):
686
+ chunk_tokens = token_counts[j]
687
+ # Check if adding this chunk would exceed the limit
688
+ if batch_token_count + chunk_tokens > MAX_TOKENS_PER_REQUEST:
689
+ if batch_end == i:
690
+ # Single chunk exceeds limit - handle it anyway
691
+ batch_end = j + 1
692
+ break
693
+ batch_token_count += chunk_tokens
694
+ batch_end = j + 1
695
+
696
+ # Make API call with this batch
697
+ batch_tokens = tokens[i:batch_end]
698
+ response = await self.async_client.create(
699
+ input=batch_tokens, **client_kwargs
700
+ )
701
+ if not isinstance(response, dict):
702
+ response = response.model_dump()
703
+ batched_embeddings.extend(r["embedding"] for r in response["data"])
704
+
705
+ i = batch_end
706
+
707
+ embeddings = _process_batched_chunked_embeddings(
708
+ len(texts), tokens, batched_embeddings, indices, self.skip_empty
709
+ )
710
+ _cached_empty_embedding: list[float] | None = None
711
+
712
+ async def empty_embedding() -> list[float]:
713
+ nonlocal _cached_empty_embedding
714
+ if _cached_empty_embedding is None:
715
+ average_embedded = await self.async_client.create(
716
+ input="", **client_kwargs
717
+ )
718
+ if not isinstance(average_embedded, dict):
719
+ average_embedded = average_embedded.model_dump()
720
+ _cached_empty_embedding = average_embedded["data"][0]["embedding"]
721
+ return _cached_empty_embedding
722
+
723
+ return [e if e is not None else await empty_embedding() for e in embeddings]
724
+
725
+ def embed_documents(
726
+ self, texts: list[str], chunk_size: int | None = None, **kwargs: Any
727
+ ) -> list[list[float]]:
728
+ """Call OpenAI's embedding endpoint to embed search docs.
729
+
730
+ Args:
731
+ texts: The list of texts to embed.
732
+ chunk_size: The chunk size of embeddings.
733
+
734
+ If `None`, will use the chunk size specified by the class.
735
+ kwargs: Additional keyword arguments to pass to the embedding API.
736
+
737
+ Returns:
738
+ List of embeddings, one for each text.
739
+ """
740
+ self._ensure_sync_client_available()
741
+ chunk_size_ = chunk_size or self.chunk_size
742
+ client_kwargs = {**self._invocation_params, **kwargs}
743
+ if not self.check_embedding_ctx_length:
744
+ embeddings: list[list[float]] = []
745
+ for i in range(0, len(texts), chunk_size_):
746
+ response = self.client.create(
747
+ input=texts[i : i + chunk_size_], **client_kwargs
748
+ )
749
+ if not isinstance(response, dict):
750
+ response = response.model_dump()
751
+ embeddings.extend(r["embedding"] for r in response["data"])
752
+ return embeddings
753
+
754
+ # Unconditionally call _get_len_safe_embeddings to handle length safety.
755
+ # This could be optimized to avoid double work when all texts are short enough.
756
+ engine = cast(str, self.deployment)
757
+ return self._get_len_safe_embeddings(
758
+ texts, engine=engine, chunk_size=chunk_size, **kwargs
759
+ )
760
+
761
+ async def aembed_documents(
762
+ self, texts: list[str], chunk_size: int | None = None, **kwargs: Any
763
+ ) -> list[list[float]]:
764
+ """Asynchronously call OpenAI's embedding endpoint to embed search docs.
765
+
766
+ Args:
767
+ texts: The list of texts to embed.
768
+ chunk_size: The chunk size of embeddings.
769
+
770
+ If `None`, will use the chunk size specified by the class.
771
+ kwargs: Additional keyword arguments to pass to the embedding API.
772
+
773
+ Returns:
774
+ List of embeddings, one for each text.
775
+ """
776
+ chunk_size_ = chunk_size or self.chunk_size
777
+ client_kwargs = {**self._invocation_params, **kwargs}
778
+ if not self.check_embedding_ctx_length:
779
+ embeddings: list[list[float]] = []
780
+ for i in range(0, len(texts), chunk_size_):
781
+ response = await self.async_client.create(
782
+ input=texts[i : i + chunk_size_], **client_kwargs
783
+ )
784
+ if not isinstance(response, dict):
785
+ response = response.model_dump()
786
+ embeddings.extend(r["embedding"] for r in response["data"])
787
+ return embeddings
788
+
789
+ # Unconditionally call _get_len_safe_embeddings to handle length safety.
790
+ # This could be optimized to avoid double work when all texts are short enough.
791
+ engine = cast(str, self.deployment)
792
+ return await self._aget_len_safe_embeddings(
793
+ texts, engine=engine, chunk_size=chunk_size, **kwargs
794
+ )
795
+
796
+ def embed_query(self, text: str, **kwargs: Any) -> list[float]:
797
+ """Call out to OpenAI's embedding endpoint for embedding query text.
798
+
799
+ Args:
800
+ text: The text to embed.
801
+ kwargs: Additional keyword arguments to pass to the embedding API.
802
+
803
+ Returns:
804
+ Embedding for the text.
805
+ """
806
+ self._ensure_sync_client_available()
807
+ return self.embed_documents([text], **kwargs)[0]
808
+
809
+ async def aembed_query(self, text: str, **kwargs: Any) -> list[float]:
810
+ """Call out to OpenAI's embedding endpoint async for embedding query text.
811
+
812
+ Args:
813
+ text: The text to embed.
814
+ kwargs: Additional keyword arguments to pass to the embedding API.
815
+
816
+ Returns:
817
+ Embedding for the text.
818
+ """
819
+ embeddings = await self.aembed_documents([text], **kwargs)
820
+ return embeddings[0]