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,557 @@
1
+ """`_ChatOpenAICodex`: experimental OAuth-backed chat model.
2
+
3
+ Wraps `ChatOpenAI` to target the ChatGPT codex backend
4
+ (`https://chatgpt.com/backend-api/codex`) and supplies refresh-aware
5
+ `Authorization` and `ChatGPT-Account-Id` headers from a
6
+ `_ChatGPTOAuthTokenProvider`.
7
+
8
+ The standard `ChatOpenAI` (API-key) flow is untouched.
9
+
10
+ !!! warning "Experimental and unofficial"
11
+
12
+ `_ChatOpenAICodex` is not an official OpenAI API integration. Use it only
13
+ where your OpenAI account, workspace, plan, and applicable OpenAI terms
14
+ permit ChatGPT-authenticated Codex access. You are responsible for ensuring
15
+ your implementation complies with OpenAI's terms, usage policies, account
16
+ restrictions, rate limits, and safeguards.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ import warnings
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ from coco_core.language_models.chat_models import LangSmithParams
27
+ from coco_core.messages import BaseMessage, ChatMessage, SystemMessage
28
+ from pydantic import Field, model_validator
29
+
30
+ from coco_openai.chat_models.base import ChatOpenAI
31
+ from coco_openai.chatgpt_oauth import (
32
+ _ChatGPTOAuthTokenProvider,
33
+ _FileChatGPTOAuthTokenProvider,
34
+ )
35
+
36
+ if TYPE_CHECKING:
37
+ from collections.abc import AsyncIterator
38
+
39
+ from coco_core.callbacks import AsyncCallbackManagerForLLMRun
40
+ from coco_core.language_models import LanguageModelInput
41
+ from coco_core.outputs import ChatGenerationChunk, ChatResult
42
+
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"
48
+ ORIGINATOR_HEADER = "originator"
49
+ ORIGINATOR_VALUE = "coco"
50
+ """Built-in default for the `originator` header value.
51
+
52
+ Identifies requests as coming from `trustcoco-openai`. Override per-instance
53
+ via the `originator` field or globally via the `COCO_CODEX_ORIGINATOR`
54
+ env var.
55
+ """
56
+ ORIGINATOR_ENV_VAR = "COCO_CODEX_ORIGINATOR"
57
+ ACCOUNT_ID_HEADER = "ChatGPT-Account-Id"
58
+ _CODEX_HEADERS_KWARG = "_codex_headers"
59
+ """Private kwarg used to hand pre-built Codex headers to `_get_request_payload`.
60
+
61
+ The async `_agenerate`/`_astream` paths build the headers from a token fetched
62
+ off the event loop (via `aget_token`) and pass them through this kwarg so the
63
+ sync payload builder doesn't fall back to `_codex_headers_sync` — which would
64
+ acquire a thread + cross-process file lock on the loop. Leading underscore keeps
65
+ it out of the public surface; it is popped before the payload reaches the SDK.
66
+ """
67
+ EXPERIMENTAL_UNOFFICIAL_WARNING = (
68
+ "`_ChatOpenAICodex` is experimental and unofficial. It uses ChatGPT "
69
+ "subscription OAuth against Codex endpoints and must only be used where "
70
+ "permitted by your OpenAI account, workspace, plan, and applicable OpenAI "
71
+ "terms and policies. You are responsible for implementing and operating "
72
+ "it responsibly, including respecting OpenAI's usage policies, rate "
73
+ "limits, and safeguards."
74
+ )
75
+ _experimental_warning_emitted = False
76
+ _INSTRUCTION_ROLES = frozenset({"system", "developer"})
77
+
78
+
79
+ def _default_originator() -> str:
80
+ """Resolve the `originator` header default, honoring the env-var override."""
81
+ return os.environ.get(ORIGINATOR_ENV_VAR) or ORIGINATOR_VALUE
82
+
83
+
84
+ def _warn_experimental_unofficial() -> None:
85
+ """Warn once that `_ChatOpenAICodex` is experimental and unofficial."""
86
+ global _experimental_warning_emitted
87
+ if _experimental_warning_emitted:
88
+ return
89
+ _experimental_warning_emitted = True
90
+ warnings.warn(EXPERIMENTAL_UNOFFICIAL_WARNING, UserWarning, stacklevel=5)
91
+
92
+
93
+ def _maybe_has_system_messages(input_: Any) -> bool:
94
+ """Return `True` if `input_` *could* contain a system-role message.
95
+
96
+ Cheap structural probe used to skip the full `_convert_input` pipeline
97
+ when there is no chance the lift logic will fire. False positives only
98
+ cost an extra conversion; false negatives would silently skip the lift,
99
+ so the probe is biased toward `True` for unknown shapes.
100
+ """
101
+ if isinstance(input_, str):
102
+ return False
103
+ if isinstance(input_, BaseMessage):
104
+ return _is_instruction_message(input_)
105
+ if isinstance(input_, (list, tuple)):
106
+ for item in input_:
107
+ if isinstance(item, BaseMessage) and _is_instruction_message(item):
108
+ return True
109
+ if isinstance(item, dict) and item.get("role") in _INSTRUCTION_ROLES:
110
+ return True
111
+ if (
112
+ isinstance(item, tuple)
113
+ and item
114
+ and isinstance(item[0], str)
115
+ and item[0] in _INSTRUCTION_ROLES
116
+ ):
117
+ return True
118
+ return False
119
+ # `PromptValue` or any future shape — be safe and run the slow path.
120
+ return True
121
+
122
+
123
+ def _is_instruction_message(message: BaseMessage) -> bool:
124
+ return isinstance(message, SystemMessage) or (
125
+ isinstance(message, ChatMessage) and message.role in _INSTRUCTION_ROLES
126
+ )
127
+
128
+
129
+ def _flatten_system_message_content(system_messages: list[BaseMessage]) -> str:
130
+ """Join system/developer message content into a single `instructions` string.
131
+
132
+ Codex rejects system-role entries in the input list, so their content
133
+ is lifted into the top-level `instructions` field. Content that uses
134
+ list-of-content-blocks form is accepted only when every block is
135
+ `{"type": "text", ...}`; anything else cannot be flattened into the
136
+ string-typed `instructions` field.
137
+
138
+ Raises:
139
+ ValueError: A system/developer message carries a non-text content block.
140
+ """
141
+ parts: list[str] = []
142
+ for index, message in enumerate(system_messages):
143
+ message_name = type(message).__name__
144
+ content = message.content
145
+ if isinstance(content, str):
146
+ parts.append(content)
147
+ continue
148
+ if not isinstance(content, list):
149
+ msg = (
150
+ f"`{message_name}` at index {index} has unsupported content "
151
+ f"type {type(content).__name__!r}; only `str` and "
152
+ "list-of-text-blocks are accepted by `_ChatOpenAICodex`."
153
+ )
154
+ raise ValueError(msg)
155
+ text_parts: list[str] = []
156
+ for block_index, block in enumerate(content):
157
+ if not isinstance(block, dict) or block.get("type") != "text":
158
+ msg = (
159
+ f"`{message_name}` at index {index} contains a "
160
+ f"non-text content block at position {block_index} "
161
+ "(Codex `instructions` is a string field — only "
162
+ '`{"type": "text", "text": "..."}` blocks can be '
163
+ "lifted into it). Move the non-text content to a "
164
+ "`HumanMessage`, or pass plain instructions via the "
165
+ "constructor or `instructions=` kwarg."
166
+ )
167
+ raise ValueError(msg)
168
+ text_value = block.get("text", "")
169
+ if not isinstance(text_value, str):
170
+ msg = (
171
+ f"`{message_name}` at index {index} has a text block "
172
+ f"at position {block_index} whose `text` is not a "
173
+ "string."
174
+ )
175
+ raise ValueError(msg)
176
+ text_parts.append(text_value)
177
+ parts.append("".join(text_parts))
178
+ return "\n\n".join(parts)
179
+
180
+
181
+ DEFAULT_INSTRUCTIONS = "You are ChatGPT, a large language model trained by OpenAI."
182
+ """Generic fallback for the Responses-API `instructions` field.
183
+
184
+ The Codex backend rejects any request missing a top-level `instructions`
185
+ value (400 `Instructions are required`), so this constant keeps zero-config
186
+ construction working. **Most callers should override it** with their own
187
+ prompt — see `_ChatOpenAICodex.instructions` for the resolution rules.
188
+ """
189
+ _FORCED_VALUES: dict[str, Any] = {
190
+ "use_responses_api": True,
191
+ "store": False,
192
+ "streaming": True,
193
+ }
194
+ """Values forced onto every `_ChatOpenAICodex` instance.
195
+
196
+ These are the wire-level constraints the Codex backend imposes:
197
+
198
+ - `use_responses_api=True`: Codex is only reachable through the Responses
199
+ API surface.
200
+ - `store=False`: the backend rejects `store=true`
201
+ (`400 'Store must be set to false'`).
202
+ - `streaming=True`: the backend rejects non-streaming requests
203
+ (`400 'Stream must be set to true'`). Pinning this routes `invoke`
204
+ through `_stream` so a streaming request is always sent and chunks
205
+ are aggregated back into a single message for the caller.
206
+
207
+ `output_version` is intentionally **not** forced — it is a client-side
208
+ `AIMessage` projection (see `ChatOpenAI.output_version`) that never
209
+ appears in the request payload, so callers can pick `"v0"`, `"v1"`, or
210
+ `"responses/v1"` freely.
211
+
212
+ `base_url` (and its `openai_api_base` alias) is also pinned — to
213
+ `CHATGPT_CODEX_BASE_URL` — under the same raise-don't-rewrite contract.
214
+ It is enforced separately in the validator rather than listed here
215
+ because a caller-controlled endpoint combined with the OAuth bearer
216
+ token would be a token-exfiltration vector; see the validator for the
217
+ rationale.
218
+ """
219
+
220
+
221
+ class _ChatOpenAICodex(ChatOpenAI):
222
+ """Experimental `ChatOpenAI` variant authed by ChatGPT OAuth.
223
+
224
+ This integration is unofficial and should only be used where your OpenAI
225
+ account, workspace, plan, and applicable OpenAI terms permit
226
+ ChatGPT-authenticated Codex access. Users are responsible for implementing
227
+ and operating it in compliance with OpenAI's terms, usage policies, account
228
+ restrictions, rate limits, and safeguards.
229
+
230
+ Routes requests to `https://chatgpt.com/backend-api/codex` and forces
231
+ the wire-level fields the Codex backend requires
232
+ (`use_responses_api=True`, `store=False`, `streaming=True`). These
233
+ values are forced — passing a conflicting value to the constructor
234
+ raises. `output_version` (a client-side `AIMessage` projection) is
235
+ not forced; pick whichever projection you want. Authorization and
236
+ `ChatGPT-Account-Id` headers are taken from `token_provider` on every
237
+ request so a freshly-refreshed access token is always used.
238
+
239
+ Example:
240
+ ```python
241
+ from coco_openai.chat_models.codex import _ChatOpenAICodex
242
+ from coco_openai.chatgpt_oauth import login_chatgpt
243
+
244
+ # One-time setup. The returned provider writes to the default store
245
+ # at `~/.coco/chatgpt-auth.json`, which `_ChatOpenAICodex` also
246
+ # reads from by default — so subsequent constructions need no
247
+ # explicit `token_provider`.
248
+ login_chatgpt()
249
+ model = _ChatOpenAICodex(
250
+ model="gpt-5.5",
251
+ instructions="You are a senior Python reviewer. Be terse.",
252
+ )
253
+ response = model.invoke("hello")
254
+ ```
255
+
256
+ !!! tip "Override `instructions`"
257
+
258
+ The Codex backend requires a top-level `instructions` value on every
259
+ request. A generic default keeps zero-config use working, but most
260
+ callers should override it via the constructor (above) or per call
261
+ (`model.invoke(..., instructions=...)`). See the field's docstring
262
+ for the full resolution rules.
263
+
264
+ !!! note
265
+
266
+ Token storage is handled by `_FileChatGPTOAuthTokenProvider`, which
267
+ defaults to `~/.coco/chatgpt-auth.json` so it does not collide
268
+ with the Codex CLI / VS Code session at `~/.codex/auth.json`.
269
+
270
+ !!! note "Always streams over the wire"
271
+
272
+ The Codex backend only accepts streaming requests, so `streaming=True`
273
+ is forced. `invoke` still returns a single aggregated `AIMessage` —
274
+ chunks are collected internally — but the underlying HTTP request is
275
+ a stream either way. Expect every call to show up as a streamed
276
+ request in network logs and LangSmith traces.
277
+ """
278
+
279
+ token_provider: Any = Field(default=None, exclude=True)
280
+ """Refresh-aware ChatGPT OAuth token provider.
281
+
282
+ Must implement the `_ChatGPTOAuthTokenProvider` protocol. If `None`, a
283
+ `_FileChatGPTOAuthTokenProvider` rooted at the default store path is
284
+ constructed.
285
+ """
286
+
287
+ originator: str | None = Field(default_factory=_default_originator)
288
+ """Value sent in the `originator` request header, or `None` to omit it.
289
+
290
+ Identifies the client making the request. Defaults to `"coco"` so
291
+ OpenAI telemetry attributes calls to this package. Downstream consumers
292
+ (e.g., a framework built on top of `_ChatOpenAICodex`) can override this
293
+ to identify themselves instead, or set `None` to suppress the header.
294
+
295
+ Resolution order (first match wins):
296
+
297
+ 1. Per-call `extra_headers={"originator": "..."}` (always trumps the
298
+ field; pass an explicit value to override on a single call).
299
+ 2. Constructor / kwarg value (`_ChatOpenAICodex(originator="my-app")`).
300
+ 3. The `COCO_CODEX_ORIGINATOR` env var, if set and non-empty.
301
+ 4. `ORIGINATOR_VALUE` (`"coco"`).
302
+
303
+ Setting `originator=None` disables the header entirely; the constructor
304
+ default never resolves to `None`.
305
+ """
306
+
307
+ instructions: str = Field(default=DEFAULT_INSTRUCTIONS)
308
+ """System prompt sent in the Responses-API `instructions` field.
309
+
310
+ `instructions` is a *top-level* field of the Responses API request — it
311
+ is not a chat message. The Codex backend rejects any request where this
312
+ field is missing or empty (400 `Instructions are required`) **and**
313
+ rejects any `SystemMessage` entry in the input list
314
+ (400 `System messages are not allowed`). To bridge those constraints
315
+ transparently, `_ChatOpenAICodex` resolves `instructions` per call with
316
+ this precedence (highest wins):
317
+
318
+ 1. Explicit `instructions=` kwarg on `invoke` / `stream`.
319
+ 2. Concatenated content of any `SystemMessage` entries in the input
320
+ list — joined with `"\\n\\n"` and stripped from the input before
321
+ sending. Set the explicit kwarg in (1) to override.
322
+ 3. This constructor field (defaults to a generic ChatGPT prompt).
323
+
324
+ The Codex backend is stateless for this client (`store=False` is
325
+ forced), so `instructions` is sent on every request and can be changed
326
+ between calls — useful for switching persona / tooling mid-conversation:
327
+
328
+ ```python
329
+ model = _ChatOpenAICodex(
330
+ model="gpt-5.5",
331
+ instructions="You are a senior Python reviewer. Be terse.",
332
+ )
333
+ model.invoke("review this diff…")
334
+ model.invoke(
335
+ "now translate the review to French",
336
+ instructions="You are a translator.",
337
+ )
338
+ ```
339
+
340
+ `SystemMessage` content that uses list-of-content-blocks form is
341
+ accepted only if every block is `{"type": "text", ...}`; any other
342
+ block type raises `ValueError` since it cannot be flattened into the
343
+ string-typed `instructions` field.
344
+ """
345
+
346
+ @model_validator(mode="before")
347
+ @classmethod
348
+ def _apply_codex_defaults(cls, values: dict[str, Any]) -> dict[str, Any]:
349
+ """Apply Codex-specific defaults before the parent validator runs."""
350
+ _warn_experimental_unofficial()
351
+ if not isinstance(values, dict):
352
+ return values
353
+ for key, forced in _FORCED_VALUES.items():
354
+ supplied = values.get(key)
355
+ if supplied is not None and supplied != forced:
356
+ msg = (
357
+ f"`_ChatOpenAICodex` requires `{key}={forced!r}`; "
358
+ f"got `{key}={supplied!r}`. Use `ChatOpenAI` if you "
359
+ "need to customize this."
360
+ )
361
+ raise ValueError(msg)
362
+ values[key] = forced
363
+ # Pin `base_url` (and its legacy `openai_api_base` alias) to the Codex
364
+ # endpoint. The OAuth bearer token is wired in as `api_key` below, so a
365
+ # caller-controlled `base_url` would otherwise exfiltrate the token to
366
+ # an attacker-chosen host. Reject any non-matching override rather than
367
+ # silently rewriting it, mirroring the `_FORCED_VALUES` contract.
368
+ for key in ("base_url", "openai_api_base"):
369
+ supplied = values.get(key)
370
+ if supplied is not None and supplied != CHATGPT_CODEX_BASE_URL:
371
+ msg = (
372
+ f"`_ChatOpenAICodex` requires `{key}={CHATGPT_CODEX_BASE_URL!r}`; "
373
+ f"got `{key}={supplied!r}`. Use `ChatOpenAI` if you need to "
374
+ "target a different endpoint."
375
+ )
376
+ raise ValueError(msg)
377
+ values[key] = CHATGPT_CODEX_BASE_URL
378
+
379
+ provider = values.get("token_provider")
380
+ if provider is None:
381
+ provider = _FileChatGPTOAuthTokenProvider.from_default_store()
382
+ values["token_provider"] = provider
383
+ if not isinstance(provider, _ChatGPTOAuthTokenProvider):
384
+ msg = (
385
+ "`token_provider` must implement the "
386
+ "`_ChatGPTOAuthTokenProvider` protocol."
387
+ )
388
+ raise TypeError(msg)
389
+
390
+ # The OAuth `token_provider` is the sole auth source: its access token
391
+ # is wired into the OpenAI SDK as `api_key` below. A caller-supplied
392
+ # `api_key` (or its `openai_api_key` alias) would silently win over the
393
+ # OAuth bearer, leaving the model in a conflicting state — so reject it
394
+ # (raise-don't-rewrite, mirroring the `base_url` handling above). An
395
+ # `OPENAI_API_KEY` env var is not consulted: the field's default
396
+ # factory never runs because `api_key` is always set here.
397
+ for key in ("api_key", "openai_api_key"):
398
+ if values.get(key) is not None:
399
+ msg = (
400
+ f"`_ChatOpenAICodex` manages authentication via "
401
+ f"`token_provider`; drop the explicit `{key}=`. Use "
402
+ "`ChatOpenAI` if you want API-key authentication."
403
+ )
404
+ raise ValueError(msg)
405
+ values["api_key"] = _SyncTokenCallable(provider)
406
+ return values
407
+
408
+ def _codex_headers_sync(self) -> dict[str, str]:
409
+ token = self.token_provider.get_token()
410
+ return self._build_headers(token.account_id)
411
+
412
+ def _build_headers(self, account_id: str | None) -> dict[str, str]:
413
+ headers: dict[str, str] = {}
414
+ if account_id:
415
+ headers[ACCOUNT_ID_HEADER] = account_id
416
+ if self.originator is not None:
417
+ headers[ORIGINATOR_HEADER] = self.originator
418
+ return headers
419
+
420
+ def _merge_codex_headers(
421
+ self, payload: dict[str, Any], headers: dict[str, str]
422
+ ) -> dict[str, Any]:
423
+ # Caller-supplied `extra_headers` win over our Codex defaults so
424
+ # users can override (e.g., to send a different `originator`).
425
+ if not headers:
426
+ return payload
427
+ merged = {**headers, **(payload.get("extra_headers") or {})}
428
+ payload["extra_headers"] = merged
429
+ return payload
430
+
431
+ def _get_request_payload(
432
+ self,
433
+ input_: LanguageModelInput,
434
+ *,
435
+ stop: list[str] | None = None,
436
+ **kwargs: Any,
437
+ ) -> dict:
438
+ """Build the request payload and attach Codex auth headers.
439
+
440
+ Lifts any `SystemMessage` content out of the input list into the
441
+ top-level `instructions` field, since Codex rejects `SystemMessage`
442
+ chat turns. See the `instructions` field docstring for the
443
+ precedence rules.
444
+
445
+ Fast path: when the input can't carry a `SystemMessage`, skip the
446
+ local conversion and delegate `input_` straight to super — that
447
+ way `_convert_input` only runs once (inside super) instead of once
448
+ here and again there.
449
+ """
450
+ codex_headers = kwargs.pop(_CODEX_HEADERS_KWARG, None)
451
+ payload_input: LanguageModelInput = input_
452
+ if _maybe_has_system_messages(input_):
453
+ messages = self._convert_input(input_).to_messages()
454
+ system_messages = [m for m in messages if _is_instruction_message(m)]
455
+ if system_messages:
456
+ non_system = [m for m in messages if not _is_instruction_message(m)]
457
+ lifted = _flatten_system_message_content(system_messages)
458
+ explicit = kwargs.get("instructions")
459
+ if explicit is not None:
460
+ logger.warning(
461
+ "Both `instructions=` and a `SystemMessage` were "
462
+ "provided; the explicit `instructions=` kwarg wins "
463
+ "and the `SystemMessage` content is discarded for "
464
+ "this call. Discarded length: %d.",
465
+ len(lifted),
466
+ )
467
+ else:
468
+ kwargs["instructions"] = lifted
469
+ payload_input = non_system
470
+
471
+ payload = super()._get_request_payload(payload_input, stop=stop, **kwargs)
472
+ # The Codex backend rejects requests without `instructions` — populate
473
+ # the field's value if the caller didn't supply one. An explicit empty
474
+ # string from the caller is preserved (the backend will reject it, but
475
+ # silently overwriting it would hide a programming error).
476
+ if payload.get("instructions") is None:
477
+ payload["instructions"] = self.instructions
478
+ # An async caller may have already built the headers off the event loop
479
+ # and passed them through `_codex_headers`. Honor them verbatim — the
480
+ # `is not None` check (not truthiness) is deliberate: an explicit empty
481
+ # dict means "no headers, already decided async" and must NOT trigger a
482
+ # sync `get_token()` (which blocks the loop on a file lock). Only the
483
+ # purely-sync path, where the kwarg is absent, reads the token here.
484
+ headers = (
485
+ codex_headers if codex_headers is not None else self._codex_headers_sync()
486
+ )
487
+ return self._merge_codex_headers(payload, headers)
488
+
489
+ async def _agenerate(
490
+ self,
491
+ messages: list[BaseMessage],
492
+ stop: list[str] | None = None,
493
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
494
+ **kwargs: Any,
495
+ ) -> ChatResult:
496
+ # Fetch the token off the event loop and build the headers here, then
497
+ # hand them to the sync payload builder via `_codex_headers`. This keeps
498
+ # `_get_request_payload` (run on the loop inside `super()._agenerate`)
499
+ # from falling back to the sync `get_token()`, which would acquire a
500
+ # thread + cross-process file lock on the loop.
501
+ token = await self.token_provider.aget_token()
502
+ kwargs[_CODEX_HEADERS_KWARG] = self._build_headers(token.account_id)
503
+ return await super()._agenerate(
504
+ messages, stop=stop, run_manager=run_manager, **kwargs
505
+ )
506
+
507
+ async def _astream(
508
+ self,
509
+ messages: list[BaseMessage],
510
+ stop: list[str] | None = None,
511
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
512
+ **kwargs: Any,
513
+ ) -> AsyncIterator[ChatGenerationChunk]:
514
+ # Build the headers from a token fetched off the event loop (see
515
+ # `_agenerate` for why) and pass them to the sync payload builder.
516
+ token = await self.token_provider.aget_token()
517
+ kwargs[_CODEX_HEADERS_KWARG] = self._build_headers(token.account_id)
518
+ async for chunk in super()._astream(
519
+ messages, stop=stop, run_manager=run_manager, **kwargs
520
+ ):
521
+ yield chunk
522
+
523
+ def _get_ls_params(
524
+ self, stop: list[str] | None = None, **kwargs: Any
525
+ ) -> LangSmithParams:
526
+ params = super()._get_ls_params(stop=stop, **kwargs)
527
+ params["ls_provider"] = "openai-codex"
528
+ return params
529
+
530
+ @property
531
+ def _llm_type(self) -> str:
532
+ return "openai-codex-chat"
533
+
534
+ @classmethod
535
+ def is_lc_serializable(cls) -> bool:
536
+ """`_ChatOpenAICodex` is not serializable (holds a live token provider)."""
537
+ return False
538
+
539
+
540
+ class _SyncTokenCallable:
541
+ """Sync callable wrapper around a token provider for the OpenAI SDK.
542
+
543
+ The OpenAI Python SDK accepts a callable returning a string for `api_key`.
544
+ Wrapping the provider lets the SDK fetch a freshly-refreshed access token
545
+ on every request without exposing the provider's other methods.
546
+ """
547
+
548
+ __slots__ = ("_provider",)
549
+
550
+ def __init__(self, provider: _ChatGPTOAuthTokenProvider) -> None:
551
+ self._provider = provider
552
+
553
+ def __call__(self) -> str:
554
+ return self._provider.get_access_token()
555
+
556
+
557
+ __all__: list[str] = []