flowent 0.0.0 → 0.0.4

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 (494) hide show
  1. package/README.md +70 -10
  2. package/assets/flowent-banner.png +0 -0
  3. package/backend/.python-version +1 -0
  4. package/backend/pyproject.toml +57 -0
  5. package/backend/src/flowent/__init__.py +3 -0
  6. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/access.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/image_assets.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/__pycache__/mcp_service.cpython-313.pyc +0 -0
  20. package/backend/src/flowent/__pycache__/model_metadata.cpython-313.pyc +0 -0
  21. package/backend/src/flowent/__pycache__/network.cpython-313.pyc +0 -0
  22. package/backend/src/flowent/__pycache__/registry.cpython-313.pyc +0 -0
  23. package/backend/src/flowent/__pycache__/role_management.cpython-313.pyc +0 -0
  24. package/backend/src/flowent/__pycache__/runtime.cpython-313.pyc +0 -0
  25. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  26. package/backend/src/flowent/__pycache__/security.cpython-313.pyc +0 -0
  27. package/backend/src/flowent/__pycache__/settings.cpython-313.pyc +0 -0
  28. package/backend/src/flowent/__pycache__/settings_management.cpython-313.pyc +0 -0
  29. package/backend/src/flowent/__pycache__/state_db.cpython-313.pyc +0 -0
  30. package/backend/src/flowent/__pycache__/stats_service.cpython-313.pyc +0 -0
  31. package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
  32. package/backend/src/flowent/_version.py +7 -0
  33. package/backend/src/flowent/access.py +247 -0
  34. package/backend/src/flowent/agent.py +2808 -0
  35. package/backend/src/flowent/assistant_commands.py +106 -0
  36. package/backend/src/flowent/channels/__init__.py +3 -0
  37. package/backend/src/flowent/channels/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/backend/src/flowent/channels/__pycache__/telegram.cpython-313.pyc +0 -0
  39. package/backend/src/flowent/channels/telegram.py +615 -0
  40. package/backend/src/flowent/cli.py +85 -0
  41. package/backend/src/flowent/config.py +14 -0
  42. package/backend/src/flowent/dev.py +3 -0
  43. package/backend/src/flowent/events.py +157 -0
  44. package/backend/src/flowent/graph_runtime.py +60 -0
  45. package/backend/src/flowent/graph_service.py +1346 -0
  46. package/backend/src/flowent/image_assets.py +356 -0
  47. package/backend/src/flowent/logging.py +155 -0
  48. package/backend/src/flowent/main.py +124 -0
  49. package/backend/src/flowent/mcp_service.py +1904 -0
  50. package/backend/src/flowent/model_metadata.py +98 -0
  51. package/backend/src/flowent/models/__init__.py +121 -0
  52. package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
  54. package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
  55. package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
  56. package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
  57. package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
  58. package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
  59. package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
  60. package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
  61. package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
  62. package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
  63. package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
  64. package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
  65. package/backend/src/flowent/models/agent.py +33 -0
  66. package/backend/src/flowent/models/base.py +24 -0
  67. package/backend/src/flowent/models/blueprint.py +176 -0
  68. package/backend/src/flowent/models/content.py +164 -0
  69. package/backend/src/flowent/models/delta.py +44 -0
  70. package/backend/src/flowent/models/event.py +51 -0
  71. package/backend/src/flowent/models/graph.py +437 -0
  72. package/backend/src/flowent/models/history.py +214 -0
  73. package/backend/src/flowent/models/llm.py +61 -0
  74. package/backend/src/flowent/models/message.py +27 -0
  75. package/backend/src/flowent/models/tab.py +48 -0
  76. package/backend/src/flowent/models/todo.py +10 -0
  77. package/backend/src/flowent/network.py +146 -0
  78. package/backend/src/flowent/prompts/__init__.py +67 -0
  79. package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  80. package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
  81. package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
  82. package/backend/src/flowent/prompts/common.py +250 -0
  83. package/backend/src/flowent/prompts/steward.py +64 -0
  84. package/backend/src/flowent/providers/__init__.py +23 -0
  85. package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  86. package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
  87. package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
  88. package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
  89. package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
  90. package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
  91. package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
  92. package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
  93. package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
  94. package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
  95. package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
  96. package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
  97. package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
  98. package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
  99. package/backend/src/flowent/providers/anthropic.py +468 -0
  100. package/backend/src/flowent/providers/base_url.py +60 -0
  101. package/backend/src/flowent/providers/configuration.py +182 -0
  102. package/backend/src/flowent/providers/content.py +122 -0
  103. package/backend/src/flowent/providers/errors.py +223 -0
  104. package/backend/src/flowent/providers/gateway.py +169 -0
  105. package/backend/src/flowent/providers/gemini.py +447 -0
  106. package/backend/src/flowent/providers/headers.py +20 -0
  107. package/backend/src/flowent/providers/management.py +96 -0
  108. package/backend/src/flowent/providers/ollama.py +293 -0
  109. package/backend/src/flowent/providers/openai.py +422 -0
  110. package/backend/src/flowent/providers/openai_responses.py +655 -0
  111. package/backend/src/flowent/providers/registry.py +144 -0
  112. package/backend/src/flowent/providers/sse.py +31 -0
  113. package/backend/src/flowent/providers/thinking.py +79 -0
  114. package/backend/src/flowent/registry.py +73 -0
  115. package/backend/src/flowent/role_management.py +255 -0
  116. package/backend/src/flowent/routes/__init__.py +30 -0
  117. package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
  118. package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
  119. package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
  120. package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
  121. package/backend/src/flowent/routes/__pycache__/mcp.cpython-313.pyc +0 -0
  122. package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
  123. package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
  124. package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
  125. package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
  126. package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
  127. package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
  128. package/backend/src/flowent/routes/__pycache__/stats.cpython-313.pyc +0 -0
  129. package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
  130. package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
  131. package/backend/src/flowent/routes/access.py +48 -0
  132. package/backend/src/flowent/routes/assistant.py +155 -0
  133. package/backend/src/flowent/routes/image_assets.py +33 -0
  134. package/backend/src/flowent/routes/mcp.py +125 -0
  135. package/backend/src/flowent/routes/meta.py +28 -0
  136. package/backend/src/flowent/routes/nodes.py +365 -0
  137. package/backend/src/flowent/routes/prompts.py +46 -0
  138. package/backend/src/flowent/routes/providers_route.py +364 -0
  139. package/backend/src/flowent/routes/roles.py +207 -0
  140. package/backend/src/flowent/routes/settings.py +324 -0
  141. package/backend/src/flowent/routes/stats.py +229 -0
  142. package/backend/src/flowent/routes/tabs.py +292 -0
  143. package/backend/src/flowent/routes/ws.py +33 -0
  144. package/backend/src/flowent/runtime.py +188 -0
  145. package/backend/src/flowent/sandbox.py +45 -0
  146. package/backend/src/flowent/security.py +42 -0
  147. package/backend/src/flowent/settings.py +2467 -0
  148. package/backend/src/flowent/settings_management.py +286 -0
  149. package/backend/src/flowent/state_db.py +120 -0
  150. package/backend/src/flowent/static/assets/AssistantPage-B3Xc08AS.js +1 -0
  151. package/backend/src/flowent/static/assets/ChannelsPage-ByLd28xk.js +1 -0
  152. package/backend/src/flowent/static/assets/HomePage-C0hAx9_l.js +3 -0
  153. package/backend/src/flowent/static/assets/McpPage-DkrYLvBv.js +7 -0
  154. package/backend/src/flowent/static/assets/PageScaffold-D4jO9ooX.js +1 -0
  155. package/backend/src/flowent/static/assets/PromptsPage-DWA7rRJd.js +1 -0
  156. package/backend/src/flowent/static/assets/ProvidersPage-PUWT8seJ.js +3 -0
  157. package/backend/src/flowent/static/assets/RolesPage-CqcclGRw.js +1 -0
  158. package/backend/src/flowent/static/assets/SettingsPage-8tS2cJgX.js +3 -0
  159. package/backend/src/flowent/static/assets/StatsPage-BX9khYzu.js +1 -0
  160. package/backend/src/flowent/static/assets/ToolsPage-9Tl9FdeD.js +1 -0
  161. package/backend/src/flowent/static/assets/WorkspaceCommandDialog-CCXxjDL8.js +1 -0
  162. package/backend/src/flowent/static/assets/WorkspacePanels-aMdJ7ZH7.js +1 -0
  163. package/backend/src/flowent/static/assets/alert-dialog-kFYVQ7oX.js +1 -0
  164. package/backend/src/flowent/static/assets/badge-74-3jsCg.js +1 -0
  165. package/backend/src/flowent/static/assets/constants-XUzFf6i1.js +1 -0
  166. package/backend/src/flowent/static/assets/datetime-m6_O_Ci9.js +1 -0
  167. package/backend/src/flowent/static/assets/dialog-BeGSweF6.js +1 -0
  168. package/backend/src/flowent/static/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  169. package/backend/src/flowent/static/assets/graph-vendor-CHpVij2M.css +1 -0
  170. package/backend/src/flowent/static/assets/graph-vendor-DRq_-6fV.js +7 -0
  171. package/backend/src/flowent/static/assets/index-BHC1Vhy8.css +1 -0
  172. package/backend/src/flowent/static/assets/index-CL1ALZ3r.js +10 -0
  173. package/backend/src/flowent/static/assets/layout.worker-jMHqAFbP.js +24 -0
  174. package/backend/src/flowent/static/assets/markdown-vendor-DVdy_w12.js +29 -0
  175. package/backend/src/flowent/static/assets/modelParams-CaHd0903.js +1 -0
  176. package/backend/src/flowent/static/assets/react-vendor-mEs_JJxa.js +9 -0
  177. package/backend/src/flowent/static/assets/roles-2OLDeTc5.js +1 -0
  178. package/backend/src/flowent/static/assets/rolldown-runtime-BYbx6iT9.js +1 -0
  179. package/backend/src/flowent/static/assets/select-DL_LPeDj.js +1 -0
  180. package/backend/src/flowent/static/assets/shared-CMxbpLeQ.js +1 -0
  181. package/backend/src/flowent/static/assets/triState-DEr3NkXV.js +1 -0
  182. package/backend/src/flowent/static/assets/ui-vendor-Dg9NNnWX.js +51 -0
  183. package/backend/src/flowent/static/index.html +36 -0
  184. package/backend/src/flowent/stats_service.py +218 -0
  185. package/backend/src/flowent/tools/__init__.py +201 -0
  186. package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  187. package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
  188. package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
  189. package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
  190. package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
  191. package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
  192. package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
  193. package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
  194. package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
  195. package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
  196. package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
  197. package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
  198. package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
  199. package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
  200. package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
  201. package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
  202. package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
  203. package/backend/src/flowent/tools/__pycache__/mcp.cpython-313.pyc +0 -0
  204. package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
  205. package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
  206. package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
  207. package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
  208. package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
  209. package/backend/src/flowent/tools/connect.py +156 -0
  210. package/backend/src/flowent/tools/contacts.py +22 -0
  211. package/backend/src/flowent/tools/create_agent.py +270 -0
  212. package/backend/src/flowent/tools/create_tab.py +59 -0
  213. package/backend/src/flowent/tools/delete_tab.py +39 -0
  214. package/backend/src/flowent/tools/edit.py +142 -0
  215. package/backend/src/flowent/tools/exec.py +117 -0
  216. package/backend/src/flowent/tools/fetch.py +85 -0
  217. package/backend/src/flowent/tools/idle.py +27 -0
  218. package/backend/src/flowent/tools/list_roles.py +50 -0
  219. package/backend/src/flowent/tools/list_tabs.py +96 -0
  220. package/backend/src/flowent/tools/list_tools.py +24 -0
  221. package/backend/src/flowent/tools/manage_prompts.py +102 -0
  222. package/backend/src/flowent/tools/manage_providers.py +220 -0
  223. package/backend/src/flowent/tools/manage_roles.py +275 -0
  224. package/backend/src/flowent/tools/manage_settings.py +346 -0
  225. package/backend/src/flowent/tools/mcp.py +199 -0
  226. package/backend/src/flowent/tools/read.py +152 -0
  227. package/backend/src/flowent/tools/send.py +50 -0
  228. package/backend/src/flowent/tools/set_permissions.py +84 -0
  229. package/backend/src/flowent/tools/sleep.py +41 -0
  230. package/backend/src/flowent/tools/todo.py +51 -0
  231. package/backend/src/flowent/workspace_store.py +479 -0
  232. package/backend/tests/__init__.py +0 -0
  233. package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  234. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  235. package/backend/tests/conftest.py +6 -0
  236. package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  237. package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
  238. package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
  239. package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
  240. package/backend/tests/integration/api/__pycache__/test_mcp_api.cpython-313-pytest-9.0.3.pyc +0 -0
  241. package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
  242. package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
  243. package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
  244. package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
  245. package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
  246. package/backend/tests/integration/api/conftest.py +29 -0
  247. package/backend/tests/integration/api/test_access_api.py +182 -0
  248. package/backend/tests/integration/api/test_assistant_api.py +354 -0
  249. package/backend/tests/integration/api/test_frontend_mounting.py +61 -0
  250. package/backend/tests/integration/api/test_mcp_api.py +116 -0
  251. package/backend/tests/integration/api/test_meta_api.py +33 -0
  252. package/backend/tests/integration/api/test_nodes_api.py +486 -0
  253. package/backend/tests/integration/api/test_prompts_api.py +47 -0
  254. package/backend/tests/integration/api/test_roles_api.py +227 -0
  255. package/backend/tests/integration/api/test_tabs_api.py +501 -0
  256. package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
  257. package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  258. package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  259. package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
  260. package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
  261. package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
  262. package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
  263. package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  264. package/backend/tests/unit/agent/test_agent_public_api.py +746 -0
  265. package/backend/tests/unit/agent/test_agent_runtime.py +2726 -0
  266. package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
  267. package/backend/tests/unit/channels/test_telegram_channel.py +552 -0
  268. package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  269. package/backend/tests/unit/logging/test_logging.py +132 -0
  270. package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
  271. package/backend/tests/unit/prompts/test_prompts.py +569 -0
  272. package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  273. package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
  274. package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
  275. package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  276. package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
  277. package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
  278. package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
  279. package/backend/tests/unit/providers/test_anthropic_provider.py +185 -0
  280. package/backend/tests/unit/providers/test_errors.py +68 -0
  281. package/backend/tests/unit/providers/test_extract_delta_parts.py +22 -0
  282. package/backend/tests/unit/providers/test_openai_provider.py +139 -0
  283. package/backend/tests/unit/providers/test_openai_responses.py +402 -0
  284. package/backend/tests/unit/providers/test_provider_gateway.py +359 -0
  285. package/backend/tests/unit/providers/test_think_tag_parser.py +36 -0
  286. package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  287. package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
  288. package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  289. package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  290. package/backend/tests/unit/routes/__pycache__/test_stats_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  291. package/backend/tests/unit/routes/test_prompts_routes.py +104 -0
  292. package/backend/tests/unit/routes/test_providers_route.py +368 -0
  293. package/backend/tests/unit/routes/test_roles_routes.py +426 -0
  294. package/backend/tests/unit/routes/test_settings_routes.py +1138 -0
  295. package/backend/tests/unit/routes/test_stats_routes.py +149 -0
  296. package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  297. package/backend/tests/unit/runtime/test_bootstrap_runtime.py +1012 -0
  298. package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  299. package/backend/tests/unit/sandbox/test_sandbox_tools.py +78 -0
  300. package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
  301. package/backend/tests/unit/security/test_security.py +110 -0
  302. package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
  303. package/backend/tests/unit/settings/test_settings_roles.py +711 -0
  304. package/backend/tests/unit/test_access.py +45 -0
  305. package/backend/tests/unit/test_cli.py +124 -0
  306. package/backend/tests/unit/test_graph_runtime.py +72 -0
  307. package/backend/tests/unit/test_network.py +51 -0
  308. package/backend/tests/unit/test_state_sqlite_storage.py +93 -0
  309. package/backend/tests/unit/test_workspace_store.py +231 -0
  310. package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  311. package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  312. package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  313. package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  314. package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  315. package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  316. package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  317. package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  318. package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  319. package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  320. package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  321. package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  322. package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  323. package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
  324. package/backend/tests/unit/tools/test_connect_tool.py +229 -0
  325. package/backend/tests/unit/tools/test_create_agent_tool.py +524 -0
  326. package/backend/tests/unit/tools/test_delete_tab_tool.py +83 -0
  327. package/backend/tests/unit/tools/test_edit_tool.py +115 -0
  328. package/backend/tests/unit/tools/test_exec_tool.py +81 -0
  329. package/backend/tests/unit/tools/test_fetch_tool.py +65 -0
  330. package/backend/tests/unit/tools/test_manage_prompts_tool.py +117 -0
  331. package/backend/tests/unit/tools/test_manage_providers_tool.py +458 -0
  332. package/backend/tests/unit/tools/test_manage_roles_tool.py +411 -0
  333. package/backend/tests/unit/tools/test_manage_settings_tool.py +608 -0
  334. package/backend/tests/unit/tools/test_read_tool.py +33 -0
  335. package/backend/tests/unit/tools/test_set_permissions_tool.py +391 -0
  336. package/backend/tests/unit/tools/test_todo_tool.py +37 -0
  337. package/backend/tests/unit/tools/test_tool_registry.py +91 -0
  338. package/backend/uv.lock +1144 -0
  339. package/bin/flowent.mjs +62 -35
  340. package/dist/frontend/assets/AssistantPage-B3Xc08AS.js +1 -0
  341. package/dist/frontend/assets/ChannelsPage-ByLd28xk.js +1 -0
  342. package/dist/frontend/assets/HomePage-C0hAx9_l.js +3 -0
  343. package/dist/frontend/assets/McpPage-DkrYLvBv.js +7 -0
  344. package/dist/frontend/assets/PageScaffold-D4jO9ooX.js +1 -0
  345. package/dist/frontend/assets/PromptsPage-DWA7rRJd.js +1 -0
  346. package/dist/frontend/assets/ProvidersPage-PUWT8seJ.js +3 -0
  347. package/dist/frontend/assets/RolesPage-CqcclGRw.js +1 -0
  348. package/dist/frontend/assets/SettingsPage-8tS2cJgX.js +3 -0
  349. package/dist/frontend/assets/StatsPage-BX9khYzu.js +1 -0
  350. package/dist/frontend/assets/ToolsPage-9Tl9FdeD.js +1 -0
  351. package/dist/frontend/assets/WorkspaceCommandDialog-CCXxjDL8.js +1 -0
  352. package/dist/frontend/assets/WorkspacePanels-aMdJ7ZH7.js +1 -0
  353. package/dist/frontend/assets/alert-dialog-kFYVQ7oX.js +1 -0
  354. package/dist/frontend/assets/badge-74-3jsCg.js +1 -0
  355. package/dist/frontend/assets/constants-XUzFf6i1.js +1 -0
  356. package/dist/frontend/assets/datetime-m6_O_Ci9.js +1 -0
  357. package/dist/frontend/assets/dialog-BeGSweF6.js +1 -0
  358. package/dist/frontend/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  359. package/dist/frontend/assets/graph-vendor-CHpVij2M.css +1 -0
  360. package/dist/frontend/assets/graph-vendor-DRq_-6fV.js +7 -0
  361. package/dist/frontend/assets/index-BHC1Vhy8.css +1 -0
  362. package/dist/frontend/assets/index-CL1ALZ3r.js +10 -0
  363. package/dist/frontend/assets/layout.worker-jMHqAFbP.js +24 -0
  364. package/dist/frontend/assets/markdown-vendor-DVdy_w12.js +29 -0
  365. package/dist/frontend/assets/modelParams-CaHd0903.js +1 -0
  366. package/dist/frontend/assets/react-vendor-mEs_JJxa.js +9 -0
  367. package/dist/frontend/assets/roles-2OLDeTc5.js +1 -0
  368. package/dist/frontend/assets/rolldown-runtime-BYbx6iT9.js +1 -0
  369. package/dist/frontend/assets/select-DL_LPeDj.js +1 -0
  370. package/dist/frontend/assets/shared-CMxbpLeQ.js +1 -0
  371. package/dist/frontend/assets/triState-DEr3NkXV.js +1 -0
  372. package/dist/frontend/assets/ui-vendor-Dg9NNnWX.js +51 -0
  373. package/dist/frontend/index.html +36 -0
  374. package/package.json +28 -41
  375. package/dist/.next/BUILD_ID +0 -1
  376. package/dist/.next/app-path-routes-manifest.json +0 -6
  377. package/dist/.next/build-manifest.json +0 -20
  378. package/dist/.next/package.json +0 -1
  379. package/dist/.next/prerender-manifest.json +0 -114
  380. package/dist/.next/required-server-files.json +0 -333
  381. package/dist/.next/routes-manifest.json +0 -69
  382. package/dist/.next/server/app/_global-error/page/app-paths-manifest.json +0 -3
  383. package/dist/.next/server/app/_global-error/page/build-manifest.json +0 -16
  384. package/dist/.next/server/app/_global-error/page/next-font-manifest.json +0 -6
  385. package/dist/.next/server/app/_global-error/page/react-loadable-manifest.json +0 -1
  386. package/dist/.next/server/app/_global-error/page/server-reference-manifest.json +0 -4
  387. package/dist/.next/server/app/_global-error/page.js +0 -9
  388. package/dist/.next/server/app/_global-error/page.js.map +0 -5
  389. package/dist/.next/server/app/_global-error/page.js.nft.json +0 -1
  390. package/dist/.next/server/app/_global-error/page_client-reference-manifest.js +0 -3
  391. package/dist/.next/server/app/_global-error.html +0 -1
  392. package/dist/.next/server/app/_global-error.meta +0 -15
  393. package/dist/.next/server/app/_global-error.rsc +0 -14
  394. package/dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +0 -5
  395. package/dist/.next/server/app/_global-error.segments/_full.segment.rsc +0 -14
  396. package/dist/.next/server/app/_global-error.segments/_head.segment.rsc +0 -5
  397. package/dist/.next/server/app/_global-error.segments/_index.segment.rsc +0 -5
  398. package/dist/.next/server/app/_global-error.segments/_tree.segment.rsc +0 -1
  399. package/dist/.next/server/app/_not-found/page/app-paths-manifest.json +0 -3
  400. package/dist/.next/server/app/_not-found/page/build-manifest.json +0 -16
  401. package/dist/.next/server/app/_not-found/page/next-font-manifest.json +0 -10
  402. package/dist/.next/server/app/_not-found/page/react-loadable-manifest.json +0 -1
  403. package/dist/.next/server/app/_not-found/page/server-reference-manifest.json +0 -4
  404. package/dist/.next/server/app/_not-found/page.js +0 -13
  405. package/dist/.next/server/app/_not-found/page.js.map +0 -5
  406. package/dist/.next/server/app/_not-found/page.js.nft.json +0 -1
  407. package/dist/.next/server/app/_not-found/page_client-reference-manifest.js +0 -3
  408. package/dist/.next/server/app/_not-found.html +0 -1
  409. package/dist/.next/server/app/_not-found.meta +0 -16
  410. package/dist/.next/server/app/_not-found.rsc +0 -16
  411. package/dist/.next/server/app/_not-found.segments/_full.segment.rsc +0 -16
  412. package/dist/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
  413. package/dist/.next/server/app/_not-found.segments/_index.segment.rsc +0 -5
  414. package/dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
  415. package/dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -5
  416. package/dist/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -2
  417. package/dist/.next/server/app/icon.svg/route/app-paths-manifest.json +0 -3
  418. package/dist/.next/server/app/icon.svg/route/build-manifest.json +0 -9
  419. package/dist/.next/server/app/icon.svg/route.js +0 -6
  420. package/dist/.next/server/app/icon.svg/route.js.map +0 -5
  421. package/dist/.next/server/app/icon.svg/route.js.nft.json +0 -1
  422. package/dist/.next/server/app/icon.svg.meta +0 -1
  423. package/dist/.next/server/app/index.html +0 -1
  424. package/dist/.next/server/app/index.meta +0 -14
  425. package/dist/.next/server/app/index.rsc +0 -15
  426. package/dist/.next/server/app/index.segments/__PAGE__.segment.rsc +0 -5
  427. package/dist/.next/server/app/index.segments/_full.segment.rsc +0 -15
  428. package/dist/.next/server/app/index.segments/_head.segment.rsc +0 -6
  429. package/dist/.next/server/app/index.segments/_index.segment.rsc +0 -5
  430. package/dist/.next/server/app/index.segments/_tree.segment.rsc +0 -3
  431. package/dist/.next/server/app/page/app-paths-manifest.json +0 -3
  432. package/dist/.next/server/app/page/build-manifest.json +0 -16
  433. package/dist/.next/server/app/page/next-font-manifest.json +0 -10
  434. package/dist/.next/server/app/page/react-loadable-manifest.json +0 -1
  435. package/dist/.next/server/app/page/server-reference-manifest.json +0 -4
  436. package/dist/.next/server/app/page.js +0 -14
  437. package/dist/.next/server/app/page.js.map +0 -5
  438. package/dist/.next/server/app/page.js.nft.json +0 -1
  439. package/dist/.next/server/app/page_client-reference-manifest.js +0 -3
  440. package/dist/.next/server/app-paths-manifest.json +0 -6
  441. package/dist/.next/server/chunks/[externals]_next_dist_0arv.vj._.js +0 -3
  442. package/dist/.next/server/chunks/[root-of-the-server]__0vcj1q1._.js +0 -13
  443. package/dist/.next/server/chunks/[turbopack]_runtime.js +0 -903
  444. package/dist/.next/server/chunks/_next-internal_server_app_icon_svg_route_actions_0-0ehc~.js +0 -3
  445. package/dist/.next/server/chunks/ssr/05w9_next_dist_0ihu0u9._.js +0 -6
  446. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_12u3mib._.js +0 -3
  447. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_forbidden_04fbe_..js +0 -3
  448. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_global-error_0brpl_..js +0 -3
  449. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_unauthorized_0~2g66g.js +0 -3
  450. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_0~cyr1_.js +0 -4
  451. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_1105emf.js +0 -4
  452. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_11uhyqv.js +0 -4
  453. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0.t9_75._.js +0 -33
  454. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0c0ud_z._.js +0 -3
  455. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0f9_8d4._.js +0 -3
  456. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0l5ko41._.js +0 -19
  457. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0mn6z7i._.js +0 -3
  458. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0npxxst._.js +0 -33
  459. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0qjhaca._.js +0 -3
  460. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0rwgw3s._.js +0 -3
  461. package/dist/.next/server/chunks/ssr/[turbopack]_runtime.js +0 -903
  462. package/dist/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0k77kol.js +0 -3
  463. package/dist/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0eq97pa.js +0 -3
  464. package/dist/.next/server/chunks/ssr/_next-internal_server_app_page_actions_09-gtaw.js +0 -3
  465. package/dist/.next/server/chunks/ssr/node_modules__pnpm_056~6.6._.js +0 -3
  466. package/dist/.next/server/chunks/ssr/node_modules__pnpm_0~j0k.e._.js +0 -33
  467. package/dist/.next/server/functions-config-manifest.json +0 -4
  468. package/dist/.next/server/middleware-build-manifest.js +0 -20
  469. package/dist/.next/server/middleware-manifest.json +0 -6
  470. package/dist/.next/server/next-font-manifest.js +0 -1
  471. package/dist/.next/server/next-font-manifest.json +0 -13
  472. package/dist/.next/server/pages/404.html +0 -1
  473. package/dist/.next/server/pages/500.html +0 -1
  474. package/dist/.next/server/pages-manifest.json +0 -4
  475. package/dist/.next/server/prefetch-hints.json +0 -1
  476. package/dist/.next/server/server-reference-manifest.js +0 -1
  477. package/dist/.next/server/server-reference-manifest.json +0 -5
  478. package/dist/.next/static/7FFlzRe2eS-D0Lw5oEpmC/_buildManifest.js +0 -11
  479. package/dist/.next/static/7FFlzRe2eS-D0Lw5oEpmC/_clientMiddlewareManifest.js +0 -1
  480. package/dist/.next/static/7FFlzRe2eS-D0Lw5oEpmC/_ssgManifest.js +0 -1
  481. package/dist/.next/static/chunks/01qk2~bgf76vu.js +0 -1
  482. package/dist/.next/static/chunks/03~yq9q893hmn.js +0 -1
  483. package/dist/.next/static/chunks/080queev.r2uy.js +0 -31
  484. package/dist/.next/static/chunks/0v3lyuj75aq50.js +0 -1
  485. package/dist/.next/static/chunks/10b~xdx5c-i7s.js +0 -5
  486. package/dist/.next/static/chunks/15~9l5n.~r-.4.css +0 -2
  487. package/dist/.next/static/chunks/turbopack-0m-970~qvs7sc.js +0 -1
  488. package/dist/.next/static/media/7178b3e590c64307-s.11.cyxs5p-0z~.woff2 +0 -0
  489. package/dist/.next/static/media/8a480f0b521d4e75-s.06d3mdzz5bre_.woff2 +0 -0
  490. package/dist/.next/static/media/caa3a2e1cccd8315-s.p.16t1db8_9y2o~.woff2 +0 -0
  491. package/dist/package.json +0 -87
  492. package/dist/server.js +0 -38
  493. /package/{dist/.next/server/app/icon.svg.body → backend/src/flowent/static/favicon.svg} +0 -0
  494. /package/dist/{.next/static/media/icon.0.r~afrtrocz9.svg → frontend/favicon.svg} +0 -0
@@ -0,0 +1,655 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import time
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ from loguru import logger
10
+
11
+ from flowent.model_metadata import build_model_info
12
+ from flowent.models import (
13
+ LLMOutputImagePart,
14
+ LLMOutputTextPart,
15
+ LLMResponse,
16
+ LLMUsage,
17
+ ModelInfo,
18
+ )
19
+ from flowent.models import ToolCallResult as ToolCall
20
+ from flowent.network import (
21
+ RequestException,
22
+ create_http_session,
23
+ iter_response_lines,
24
+ read_response_text,
25
+ response_looks_like_html,
26
+ truncate_text,
27
+ )
28
+ from flowent.providers import LLMProvider
29
+ from flowent.providers.content import (
30
+ collapse_parts_to_text,
31
+ to_openai_responses_content,
32
+ )
33
+ from flowent.providers.errors import (
34
+ build_access_blocked_error,
35
+ build_network_error,
36
+ build_status_error,
37
+ )
38
+ from flowent.providers.headers import merge_headers
39
+ from flowent.settings import ModelParams
40
+
41
+ REASONING_MODEL_PREFIXES = ("gpt-5", "o1", "o3", "o4")
42
+ VERBOSITY_MODEL_PREFIXES = ("gpt-5",)
43
+ REASONING_DELTA_EVENT_TYPES = {
44
+ "response.reasoning.delta",
45
+ "response.reasoning_text.delta",
46
+ "response.reasoning_summary_text.delta",
47
+ }
48
+
49
+
50
+ def _supports_reasoning(model: str) -> bool:
51
+ return model.startswith(REASONING_MODEL_PREFIXES)
52
+
53
+
54
+ def _build_reasoning_config(
55
+ model: str,
56
+ model_params: ModelParams | None,
57
+ ) -> dict[str, str] | None:
58
+ if not _supports_reasoning(model) or model_params is None:
59
+ return None
60
+ effort = model_params.reasoning_effort
61
+ if effort is None or effort == "none":
62
+ return None
63
+
64
+ return {
65
+ "effort": effort,
66
+ "summary": "detailed",
67
+ }
68
+
69
+
70
+ def _build_text_config(
71
+ model: str,
72
+ model_params: ModelParams | None,
73
+ ) -> dict[str, object] | None:
74
+ if model_params is None or not model.startswith(VERBOSITY_MODEL_PREFIXES):
75
+ return None
76
+ if not model_params.verbosity:
77
+ return None
78
+
79
+ return {
80
+ "format": {"type": "text"},
81
+ "verbosity": model_params.verbosity,
82
+ }
83
+
84
+
85
+ def _extract_reasoning_text_from_item(item: dict[str, Any]) -> str | None:
86
+ parts: list[str] = []
87
+
88
+ for key in ("summary", "content"):
89
+ raw_value = item.get(key)
90
+
91
+ if isinstance(raw_value, str):
92
+ if raw_value.strip():
93
+ parts.append(raw_value)
94
+ continue
95
+
96
+ if not isinstance(raw_value, list):
97
+ continue
98
+
99
+ for entry in raw_value:
100
+ if not isinstance(entry, dict):
101
+ continue
102
+ text = entry.get("text")
103
+ if isinstance(text, str) and text.strip():
104
+ parts.append(text)
105
+
106
+ if not parts:
107
+ return None
108
+
109
+ return "".join(parts)
110
+
111
+
112
+ def _extract_reasoning_text_from_output(output: Any) -> str | None:
113
+ if not isinstance(output, list):
114
+ return None
115
+
116
+ parts: list[str] = []
117
+ for item in output:
118
+ if not isinstance(item, dict):
119
+ continue
120
+ if item.get("type") != "reasoning":
121
+ continue
122
+ text = _extract_reasoning_text_from_item(item)
123
+ if text:
124
+ parts.append(text)
125
+
126
+ if not parts:
127
+ return None
128
+
129
+ return "\n\n".join(parts)
130
+
131
+
132
+ def _extract_reasoning_tokens(response: dict[str, Any]) -> int:
133
+ usage = response.get("usage")
134
+ if not isinstance(usage, dict):
135
+ return 0
136
+
137
+ output_tokens_details = usage.get("output_tokens_details")
138
+ if not isinstance(output_tokens_details, dict):
139
+ return 0
140
+
141
+ tokens = output_tokens_details.get("reasoning_tokens")
142
+ return tokens if isinstance(tokens, int) and tokens > 0 else 0
143
+
144
+
145
+ def _collect_usage_details(
146
+ value: dict[str, Any],
147
+ *,
148
+ prefix: str = "",
149
+ ) -> dict[str, int]:
150
+ details: dict[str, int] = {}
151
+ for key, raw_item in value.items():
152
+ detail_key = f"{prefix}.{key}" if prefix else key
153
+ if isinstance(raw_item, bool):
154
+ continue
155
+ if isinstance(raw_item, int) and raw_item >= 0:
156
+ details[detail_key] = raw_item
157
+ continue
158
+ if isinstance(raw_item, dict):
159
+ details.update(_collect_usage_details(raw_item, prefix=detail_key))
160
+ return details
161
+
162
+
163
+ def _extract_usage(response: dict[str, Any]) -> LLMUsage | None:
164
+ usage = response.get("usage")
165
+ if not isinstance(usage, dict):
166
+ return None
167
+
168
+ total_tokens = usage.get("total_tokens")
169
+ if isinstance(total_tokens, bool) or not isinstance(total_tokens, int):
170
+ return None
171
+
172
+ input_tokens = usage.get("input_tokens")
173
+ if isinstance(input_tokens, bool) or not isinstance(input_tokens, int):
174
+ input_tokens = None
175
+
176
+ output_tokens = usage.get("output_tokens")
177
+ if isinstance(output_tokens, bool) or not isinstance(output_tokens, int):
178
+ output_tokens = None
179
+
180
+ cached_input_tokens = usage.get("cached_input_tokens")
181
+ if isinstance(cached_input_tokens, bool) or not isinstance(
182
+ cached_input_tokens, int
183
+ ):
184
+ cached_input_tokens = None
185
+ input_token_details = usage.get("input_tokens_details")
186
+ if (
187
+ cached_input_tokens is None
188
+ and isinstance(input_token_details, dict)
189
+ and isinstance(input_token_details.get("cached_tokens"), int)
190
+ ):
191
+ cached_input_tokens = input_token_details["cached_tokens"]
192
+
193
+ details = _collect_usage_details(usage)
194
+ for key in (
195
+ "total_tokens",
196
+ "input_tokens",
197
+ "output_tokens",
198
+ "cached_input_tokens",
199
+ "input_tokens_details.cached_tokens",
200
+ ):
201
+ details.pop(key, None)
202
+
203
+ return LLMUsage(
204
+ total_tokens=total_tokens,
205
+ input_tokens=input_tokens,
206
+ output_tokens=output_tokens,
207
+ cached_input_tokens=cached_input_tokens,
208
+ cache_read_tokens=cached_input_tokens,
209
+ details=details,
210
+ )
211
+
212
+
213
+ def _decode_output_image(item: dict[str, Any]) -> LLMOutputImagePart | None:
214
+ encoded_data = (
215
+ item.get("result") or item.get("image_base64") or item.get("b64_json")
216
+ )
217
+ if not isinstance(encoded_data, str) or not encoded_data:
218
+ return None
219
+ try:
220
+ image_data = base64.b64decode(encoded_data)
221
+ except ValueError:
222
+ return None
223
+ mime_type = item.get("mime_type")
224
+ return LLMOutputImagePart(
225
+ data=image_data,
226
+ mime_type=mime_type if isinstance(mime_type, str) else "image/png",
227
+ )
228
+
229
+
230
+ def _extract_output_parts(output: Any) -> list[LLMOutputTextPart | LLMOutputImagePart]:
231
+ if not isinstance(output, list):
232
+ return []
233
+
234
+ parts: list[LLMOutputTextPart | LLMOutputImagePart] = []
235
+ for item in output:
236
+ if not isinstance(item, dict):
237
+ continue
238
+ item_type = item.get("type")
239
+ if item_type == "message":
240
+ content = item.get("content")
241
+ if not isinstance(content, list):
242
+ continue
243
+ for entry in content:
244
+ if not isinstance(entry, dict):
245
+ continue
246
+ entry_type = entry.get("type")
247
+ text = entry.get("text")
248
+ if entry_type in {"output_text", "text"} and isinstance(text, str):
249
+ parts.append(LLMOutputTextPart(text=text))
250
+ elif item_type == "image_generation_call":
251
+ image_part = _decode_output_image(item)
252
+ if image_part is not None:
253
+ parts.append(image_part)
254
+ return parts
255
+
256
+
257
+ def _format_reasoning_fallback(reasoning_tokens: int) -> str:
258
+ if reasoning_tokens > 0:
259
+ return f"Internal reasoning · {reasoning_tokens}"
260
+
261
+ return "Internal reasoning"
262
+
263
+
264
+ class OpenAIResponsesProvider(LLMProvider):
265
+ def __init__(
266
+ self,
267
+ provider_name: str,
268
+ api_base_url: str,
269
+ api_key: str = "",
270
+ headers: dict[str, str] | None = None,
271
+ model: str = "",
272
+ request_timeout_seconds: float = 120.0,
273
+ ) -> None:
274
+ self._provider_name = provider_name
275
+ self._api_base_url = api_base_url.rstrip("/")
276
+ self._api_key = api_key
277
+ self._header_overrides = dict(headers or {})
278
+ self._model = model
279
+ self._request_timeout_seconds = request_timeout_seconds
280
+ self._client = create_http_session(timeout=self._request_timeout_seconds)
281
+
282
+ def _headers(self) -> dict[str, str]:
283
+ headers: dict[str, str] = {"Content-Type": "application/json"}
284
+ if self._api_key:
285
+ headers["Authorization"] = f"Bearer {self._api_key}"
286
+ return merge_headers(headers, self._header_overrides)
287
+
288
+ def _convert_tools(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
289
+ result = []
290
+ for t in tools:
291
+ fn = t.get("function", {})
292
+ result.append(
293
+ {
294
+ "type": "function",
295
+ "name": fn.get("name", ""),
296
+ "description": fn.get("description", ""),
297
+ "parameters": fn.get("parameters", {}),
298
+ }
299
+ )
300
+ return result
301
+
302
+ def _convert_messages(
303
+ self, messages: list[dict[str, Any]]
304
+ ) -> tuple[str | None, list[dict[str, Any]]]:
305
+ system_prompt: str | None = None
306
+ input_items: list[dict[str, Any]] = []
307
+
308
+ for msg in messages:
309
+ role = msg.get("role")
310
+ content = msg.get("content")
311
+
312
+ if role == "system":
313
+ system_prompt = content
314
+ continue
315
+
316
+ if role == "user":
317
+ input_items.append(
318
+ {
319
+ "role": "user",
320
+ "content": to_openai_responses_content(
321
+ content,
322
+ allow_images=True,
323
+ ),
324
+ }
325
+ )
326
+ elif role == "assistant":
327
+ tool_calls = msg.get("tool_calls")
328
+ if tool_calls:
329
+ for tc in tool_calls:
330
+ fn = tc.get("function", {})
331
+ input_items.append(
332
+ {
333
+ "type": "function_call",
334
+ "call_id": tc.get("id", ""),
335
+ "name": fn.get("name", ""),
336
+ "arguments": fn.get("arguments", "{}"),
337
+ }
338
+ )
339
+ else:
340
+ input_items.append(
341
+ {
342
+ "role": "assistant",
343
+ "content": collapse_parts_to_text(content),
344
+ }
345
+ )
346
+ elif role == "tool":
347
+ input_items.append(
348
+ {
349
+ "type": "function_call_output",
350
+ "call_id": msg.get("tool_call_id", ""),
351
+ "output": content or "",
352
+ }
353
+ )
354
+
355
+ return system_prompt, input_items
356
+
357
+ def _supports_output_image(self) -> bool:
358
+ model_info = build_model_info(
359
+ provider_type="openai_responses",
360
+ model_id=self._model,
361
+ )
362
+ return model_info.capabilities.output_image
363
+
364
+ def chat(
365
+ self,
366
+ messages: list[dict[str, Any]],
367
+ tools: list[dict[str, Any]] | None = None,
368
+ on_chunk: Callable[[str, str], None] | None = None,
369
+ register_interrupt: Callable[[Callable[[], None] | None], None] | None = None,
370
+ model_params: ModelParams | None = None,
371
+ ) -> LLMResponse:
372
+ url = f"{self._api_base_url}/responses"
373
+ system_prompt, input_items = self._convert_messages(messages)
374
+
375
+ payload: dict[str, Any] = {
376
+ "model": self._model,
377
+ "input": input_items,
378
+ "stream": True,
379
+ }
380
+ reasoning = _build_reasoning_config(self._model, model_params)
381
+ if reasoning:
382
+ payload["reasoning"] = reasoning
383
+ text_config = _build_text_config(self._model, model_params)
384
+ if text_config:
385
+ payload["text"] = text_config
386
+ if model_params is not None:
387
+ if model_params.max_output_tokens is not None:
388
+ payload["max_output_tokens"] = model_params.max_output_tokens
389
+ if reasoning is None:
390
+ if model_params.temperature is not None:
391
+ payload["temperature"] = model_params.temperature
392
+ if model_params.top_p is not None:
393
+ payload["top_p"] = model_params.top_p
394
+ if system_prompt:
395
+ payload["instructions"] = system_prompt
396
+ payload_tools = []
397
+ if tools:
398
+ payload_tools.extend(self._convert_tools(tools))
399
+ if self._supports_output_image():
400
+ payload_tools.append({"type": "image_generation"})
401
+ if payload_tools:
402
+ payload["tools"] = payload_tools
403
+ payload["tool_choice"] = "auto"
404
+
405
+ logger.debug(
406
+ "[{}] OpenAI Responses chat request: model={}, input_items={}, tools={}",
407
+ self._provider_name,
408
+ self._model,
409
+ len(input_items),
410
+ len(tools) if tools else 0,
411
+ )
412
+
413
+ t0 = time.perf_counter()
414
+ content_parts: list[str] = []
415
+ thinking_parts: list[str] = []
416
+ response_parts: list[LLMOutputTextPart | LLMOutputImagePart] | None = None
417
+ tool_calls: list[ToolCall] = []
418
+ chunk_count = 0
419
+ saw_reasoning_item = False
420
+ saw_reasoning_text = False
421
+ reasoning_tokens = 0
422
+ response_usage: LLMUsage | None = None
423
+ raw_usage: dict[str, Any] | None = None
424
+
425
+ current_tool: dict[str, Any] = {}
426
+ client = self._client
427
+ if register_interrupt is not None:
428
+ register_interrupt(client.close)
429
+
430
+ try:
431
+ with client.stream(
432
+ "POST",
433
+ url,
434
+ headers=self._headers(),
435
+ json=payload,
436
+ ) as response:
437
+ if register_interrupt is not None:
438
+ register_interrupt(response.close)
439
+ if response_looks_like_html(response):
440
+ raise build_access_blocked_error(
441
+ provider_name=self._provider_name,
442
+ provider_type="openai_responses",
443
+ model=self._model,
444
+ base_url=self._api_base_url,
445
+ status_code=response.status_code,
446
+ detail=truncate_text(read_response_text(response)),
447
+ )
448
+ if response.status_code != 200:
449
+ body = truncate_text(read_response_text(response))
450
+ elapsed = time.perf_counter() - t0
451
+ logger.error(
452
+ "LLM API error [provider={}, model={}, type=openai_responses]: {} - {} ({:.2f}s)",
453
+ self._provider_name,
454
+ self._model,
455
+ response.status_code,
456
+ body[:500],
457
+ elapsed,
458
+ )
459
+ raise build_status_error(
460
+ provider_name=self._provider_name,
461
+ provider_type="openai_responses",
462
+ model=self._model,
463
+ base_url=self._api_base_url,
464
+ status_code=response.status_code,
465
+ body=body,
466
+ )
467
+
468
+ for line in iter_response_lines(response):
469
+ if not line or line.startswith(":"):
470
+ continue
471
+ if not line.startswith("data: "):
472
+ continue
473
+ data_str = line[6:]
474
+ if data_str.strip() == "[DONE]":
475
+ break
476
+
477
+ try:
478
+ event = json.loads(data_str)
479
+ except json.JSONDecodeError:
480
+ continue
481
+
482
+ chunk_count += 1
483
+ event_type = event.get("type", "")
484
+
485
+ if event_type in REASONING_DELTA_EVENT_TYPES:
486
+ text = event.get("delta", "")
487
+ if text:
488
+ saw_reasoning_text = True
489
+ thinking_parts.append(text)
490
+ if on_chunk:
491
+ on_chunk("thinking", text)
492
+
493
+ elif event_type == "response.output_text.delta":
494
+ text = event.get("delta", "")
495
+ if text:
496
+ content_parts.append(text)
497
+ if on_chunk:
498
+ on_chunk("content", text)
499
+
500
+ elif event_type == "response.function_call_arguments.delta":
501
+ delta = event.get("delta", "")
502
+ current_tool.setdefault("arguments", "")
503
+ current_tool["arguments"] += delta
504
+
505
+ elif event_type == "response.output_item.added":
506
+ item = event.get("item", {})
507
+ if item.get("type") == "reasoning":
508
+ saw_reasoning_item = True
509
+ elif item.get("type") == "function_call":
510
+ current_tool = {
511
+ "id": item.get("call_id", ""),
512
+ "name": item.get("name", ""),
513
+ "arguments": "",
514
+ }
515
+
516
+ elif event_type == "response.output_item.done":
517
+ item = event.get("item", {})
518
+ if item.get("type") == "reasoning":
519
+ saw_reasoning_item = True
520
+ if not saw_reasoning_text:
521
+ reasoning_text = _extract_reasoning_text_from_item(item)
522
+ if reasoning_text:
523
+ saw_reasoning_text = True
524
+ thinking_parts.append(reasoning_text)
525
+ if on_chunk:
526
+ on_chunk("thinking", reasoning_text)
527
+ elif item.get("type") == "function_call":
528
+ call_id = item.get("call_id") or current_tool.get("id", "")
529
+ name = item.get("name") or current_tool.get("name", "")
530
+ args_str = item.get("arguments") or current_tool.get(
531
+ "arguments", "{}"
532
+ )
533
+ try:
534
+ args = json.loads(args_str)
535
+ except json.JSONDecodeError:
536
+ args = {}
537
+ tool_calls.append(
538
+ ToolCall(id=call_id, name=name, arguments=args)
539
+ )
540
+ current_tool = {}
541
+
542
+ elif event_type == "response.completed":
543
+ response_data = event.get("response", {})
544
+ if isinstance(response_data, dict):
545
+ reasoning_tokens = _extract_reasoning_tokens(response_data)
546
+ response_usage = _extract_usage(response_data)
547
+ usage_payload = response_data.get("usage")
548
+ if isinstance(usage_payload, dict):
549
+ raw_usage = dict(usage_payload)
550
+ if not saw_reasoning_text:
551
+ reasoning_text = _extract_reasoning_text_from_output(
552
+ response_data.get("output")
553
+ )
554
+ if reasoning_text:
555
+ saw_reasoning_text = True
556
+ thinking_parts.append(reasoning_text)
557
+ if on_chunk:
558
+ on_chunk("thinking", reasoning_text)
559
+ output_parts = _extract_output_parts(
560
+ response_data.get("output")
561
+ )
562
+ if output_parts:
563
+ response_parts = output_parts
564
+ except RequestException as exc:
565
+ elapsed = time.perf_counter() - t0
566
+ logger.warning(
567
+ "LLM API transport error [provider={}, model={}, type=openai_responses]: {} ({:.2f}s)",
568
+ self._provider_name,
569
+ self._model,
570
+ exc,
571
+ elapsed,
572
+ )
573
+ raise build_network_error(
574
+ provider_name=self._provider_name,
575
+ provider_type="openai_responses",
576
+ model=self._model,
577
+ base_url=self._api_base_url,
578
+ error=exc,
579
+ ) from exc
580
+ finally:
581
+ close_client = getattr(client, "close", None)
582
+ if callable(close_client):
583
+ close_client()
584
+ self._client = create_http_session(
585
+ timeout=self._request_timeout_seconds
586
+ )
587
+
588
+ elapsed = time.perf_counter() - t0
589
+ content = "".join(content_parts) or None
590
+ thinking = "".join(thinking_parts) or None
591
+ if thinking is None and (saw_reasoning_item or reasoning_tokens > 0):
592
+ thinking = _format_reasoning_fallback(reasoning_tokens)
593
+
594
+ logger.debug(
595
+ "[{}] OpenAI Responses chat done: {:.2f}s, chunks={}, content_len={}, thinking_len={}, tool_calls={}",
596
+ self._provider_name,
597
+ elapsed,
598
+ chunk_count,
599
+ len(content) if content else 0,
600
+ len(thinking) if thinking else 0,
601
+ len(tool_calls),
602
+ )
603
+
604
+ if tool_calls:
605
+ return LLMResponse(
606
+ content=content,
607
+ parts=response_parts,
608
+ tool_calls=tool_calls,
609
+ thinking=thinking,
610
+ usage=response_usage,
611
+ raw_usage=raw_usage,
612
+ )
613
+
614
+ return LLMResponse(
615
+ content=content or "",
616
+ parts=response_parts,
617
+ thinking=thinking,
618
+ usage=response_usage,
619
+ raw_usage=raw_usage,
620
+ )
621
+
622
+ def list_models(
623
+ self,
624
+ register_interrupt: Callable[[Callable[[], None] | None], None] | None = None,
625
+ ) -> list[ModelInfo]:
626
+ url = f"{self._api_base_url}/models"
627
+ client = self._client
628
+ close_client = getattr(client, "close", None)
629
+ if register_interrupt is not None and callable(close_client):
630
+ register_interrupt(close_client)
631
+ try:
632
+ resp = client.get(url, headers=self._headers())
633
+ resp.raise_for_status()
634
+ data = resp.json()
635
+ models = data.get("data", [])
636
+ return [
637
+ build_model_info(
638
+ provider_type="openai_responses",
639
+ model_id=m["id"],
640
+ )
641
+ for m in models
642
+ ]
643
+ except Exception as e:
644
+ logger.error(
645
+ "Failed to list models [provider={}, type=openai_responses]: {}",
646
+ self._provider_name,
647
+ e,
648
+ )
649
+ return []
650
+ finally:
651
+ if callable(close_client):
652
+ close_client()
653
+ self._client = create_http_session(
654
+ timeout=self._request_timeout_seconds
655
+ )