@a1hvdy/cc-openclaw 0.3.2

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 (491) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +207 -0
  3. package/configs/.gitkeep +0 -0
  4. package/configs/council-reviewer-prompt.md +82 -0
  5. package/configs/council-system-prompt.md +141 -0
  6. package/dist/scripts/bench/ab-harness.d.ts +58 -0
  7. package/dist/scripts/bench/ab-harness.d.ts.map +1 -0
  8. package/dist/scripts/bench/ab-harness.js +78 -0
  9. package/dist/scripts/bench/ab-harness.js.map +1 -0
  10. package/dist/src/channels/adapter.d.ts +103 -0
  11. package/dist/src/channels/adapter.d.ts.map +1 -0
  12. package/dist/src/channels/adapter.js +38 -0
  13. package/dist/src/channels/adapter.js.map +1 -0
  14. package/dist/src/channels/telegram/completion-summary.d.ts +22 -0
  15. package/dist/src/channels/telegram/completion-summary.d.ts.map +1 -0
  16. package/dist/src/channels/telegram/completion-summary.js +186 -0
  17. package/dist/src/channels/telegram/completion-summary.js.map +1 -0
  18. package/dist/src/channels/telegram/error-renderer.d.ts +30 -0
  19. package/dist/src/channels/telegram/error-renderer.d.ts.map +1 -0
  20. package/dist/src/channels/telegram/error-renderer.js +133 -0
  21. package/dist/src/channels/telegram/error-renderer.js.map +1 -0
  22. package/dist/src/channels/telegram/event-reducer.d.ts +34 -0
  23. package/dist/src/channels/telegram/event-reducer.d.ts.map +1 -0
  24. package/dist/src/channels/telegram/event-reducer.js +579 -0
  25. package/dist/src/channels/telegram/event-reducer.js.map +1 -0
  26. package/dist/src/channels/telegram/index.d.ts +14 -0
  27. package/dist/src/channels/telegram/index.d.ts.map +1 -0
  28. package/dist/src/channels/telegram/index.js +14 -0
  29. package/dist/src/channels/telegram/index.js.map +1 -0
  30. package/dist/src/channels/telegram/injector.d.ts +54 -0
  31. package/dist/src/channels/telegram/injector.d.ts.map +1 -0
  32. package/dist/src/channels/telegram/injector.js +200 -0
  33. package/dist/src/channels/telegram/injector.js.map +1 -0
  34. package/dist/src/channels/telegram/live-card.d.ts +230 -0
  35. package/dist/src/channels/telegram/live-card.d.ts.map +1 -0
  36. package/dist/src/channels/telegram/live-card.js +916 -0
  37. package/dist/src/channels/telegram/live-card.js.map +1 -0
  38. package/dist/src/channels/telegram/state-machine.d.ts +23 -0
  39. package/dist/src/channels/telegram/state-machine.d.ts.map +1 -0
  40. package/dist/src/channels/telegram/state-machine.js +72 -0
  41. package/dist/src/channels/telegram/state-machine.js.map +1 -0
  42. package/dist/src/channels/telegram/tool-tracker.d.ts +147 -0
  43. package/dist/src/channels/telegram/tool-tracker.d.ts.map +1 -0
  44. package/dist/src/channels/telegram/tool-tracker.js +520 -0
  45. package/dist/src/channels/telegram/tool-tracker.js.map +1 -0
  46. package/dist/src/circuit-breaker.d.ts +22 -0
  47. package/dist/src/circuit-breaker.d.ts.map +1 -0
  48. package/dist/src/circuit-breaker.js +47 -0
  49. package/dist/src/circuit-breaker.js.map +1 -0
  50. package/dist/src/command-router/cc-handler.d.ts +67 -0
  51. package/dist/src/command-router/cc-handler.d.ts.map +1 -0
  52. package/dist/src/command-router/cc-handler.js +980 -0
  53. package/dist/src/command-router/cc-handler.js.map +1 -0
  54. package/dist/src/command-router/index.d.ts +3 -0
  55. package/dist/src/command-router/index.d.ts.map +1 -0
  56. package/dist/src/command-router/index.js +2 -0
  57. package/dist/src/command-router/index.js.map +1 -0
  58. package/dist/src/constants.d.ts +132 -0
  59. package/dist/src/constants.d.ts.map +1 -0
  60. package/dist/src/constants.js +140 -0
  61. package/dist/src/constants.js.map +1 -0
  62. package/dist/src/council/consensus.d.ts +21 -0
  63. package/dist/src/council/consensus.d.ts.map +1 -0
  64. package/dist/src/council/consensus.js +52 -0
  65. package/dist/src/council/consensus.js.map +1 -0
  66. package/dist/src/council/council.d.ts +68 -0
  67. package/dist/src/council/council.d.ts.map +1 -0
  68. package/dist/src/council/council.js +914 -0
  69. package/dist/src/council/council.js.map +1 -0
  70. package/dist/src/council/index.d.ts +3 -0
  71. package/dist/src/council/index.d.ts.map +1 -0
  72. package/dist/src/council/index.js +3 -0
  73. package/dist/src/council/index.js.map +1 -0
  74. package/dist/src/engines/base-oneshot-session.d.ts +88 -0
  75. package/dist/src/engines/base-oneshot-session.d.ts.map +1 -0
  76. package/dist/src/engines/base-oneshot-session.js +228 -0
  77. package/dist/src/engines/base-oneshot-session.js.map +1 -0
  78. package/dist/src/engines/index.d.ts +7 -0
  79. package/dist/src/engines/index.d.ts.map +1 -0
  80. package/dist/src/engines/index.js +7 -0
  81. package/dist/src/engines/index.js.map +1 -0
  82. package/dist/src/engines/persistent-codex-session.d.ts +17 -0
  83. package/dist/src/engines/persistent-codex-session.d.ts.map +1 -0
  84. package/dist/src/engines/persistent-codex-session.js +106 -0
  85. package/dist/src/engines/persistent-codex-session.js.map +1 -0
  86. package/dist/src/engines/persistent-cursor-session.d.ts +22 -0
  87. package/dist/src/engines/persistent-cursor-session.d.ts.map +1 -0
  88. package/dist/src/engines/persistent-cursor-session.js +242 -0
  89. package/dist/src/engines/persistent-cursor-session.js.map +1 -0
  90. package/dist/src/engines/persistent-custom-session.d.ts +79 -0
  91. package/dist/src/engines/persistent-custom-session.d.ts.map +1 -0
  92. package/dist/src/engines/persistent-custom-session.js +939 -0
  93. package/dist/src/engines/persistent-custom-session.js.map +1 -0
  94. package/dist/src/engines/persistent-gemini-session.d.ts +22 -0
  95. package/dist/src/engines/persistent-gemini-session.d.ts.map +1 -0
  96. package/dist/src/engines/persistent-gemini-session.js +217 -0
  97. package/dist/src/engines/persistent-gemini-session.js.map +1 -0
  98. package/dist/src/engines/persistent-session.d.ts +77 -0
  99. package/dist/src/engines/persistent-session.d.ts.map +1 -0
  100. package/dist/src/engines/persistent-session.js +730 -0
  101. package/dist/src/engines/persistent-session.js.map +1 -0
  102. package/dist/src/health/handler.d.ts +40 -0
  103. package/dist/src/health/handler.d.ts.map +1 -0
  104. package/dist/src/health/handler.js +70 -0
  105. package/dist/src/health/handler.js.map +1 -0
  106. package/dist/src/health/index.d.ts +2 -0
  107. package/dist/src/health/index.d.ts.map +1 -0
  108. package/dist/src/health/index.js +2 -0
  109. package/dist/src/health/index.js.map +1 -0
  110. package/dist/src/index.d.ts +49 -0
  111. package/dist/src/index.d.ts.map +1 -0
  112. package/dist/src/index.js +84 -0
  113. package/dist/src/index.js.map +1 -0
  114. package/dist/src/lib/auto-recovery.d.ts +45 -0
  115. package/dist/src/lib/auto-recovery.d.ts.map +1 -0
  116. package/dist/src/lib/auto-recovery.js +217 -0
  117. package/dist/src/lib/auto-recovery.js.map +1 -0
  118. package/dist/src/lib/cache-parity.d.ts +39 -0
  119. package/dist/src/lib/cache-parity.d.ts.map +1 -0
  120. package/dist/src/lib/cache-parity.js +92 -0
  121. package/dist/src/lib/cache-parity.js.map +1 -0
  122. package/dist/src/lib/circuit-breaker.d.ts +22 -0
  123. package/dist/src/lib/circuit-breaker.d.ts.map +1 -0
  124. package/dist/src/lib/circuit-breaker.js +47 -0
  125. package/dist/src/lib/circuit-breaker.js.map +1 -0
  126. package/dist/src/lib/config.d.ts +74 -0
  127. package/dist/src/lib/config.d.ts.map +1 -0
  128. package/dist/src/lib/config.js +244 -0
  129. package/dist/src/lib/config.js.map +1 -0
  130. package/dist/src/lib/drift-detector.d.ts +47 -0
  131. package/dist/src/lib/drift-detector.d.ts.map +1 -0
  132. package/dist/src/lib/drift-detector.js +192 -0
  133. package/dist/src/lib/drift-detector.js.map +1 -0
  134. package/dist/src/lib/error-formatter.d.ts +78 -0
  135. package/dist/src/lib/error-formatter.d.ts.map +1 -0
  136. package/dist/src/lib/error-formatter.js +149 -0
  137. package/dist/src/lib/error-formatter.js.map +1 -0
  138. package/dist/src/lib/heartbeat-workaround.d.ts +45 -0
  139. package/dist/src/lib/heartbeat-workaround.d.ts.map +1 -0
  140. package/dist/src/lib/heartbeat-workaround.js +61 -0
  141. package/dist/src/lib/heartbeat-workaround.js.map +1 -0
  142. package/dist/src/lib/index.d.ts +8 -0
  143. package/dist/src/lib/index.d.ts.map +1 -0
  144. package/dist/src/lib/index.js +8 -0
  145. package/dist/src/lib/index.js.map +1 -0
  146. package/dist/src/lib/register-guard.d.ts +49 -0
  147. package/dist/src/lib/register-guard.d.ts.map +1 -0
  148. package/dist/src/lib/register-guard.js +73 -0
  149. package/dist/src/lib/register-guard.js.map +1 -0
  150. package/dist/src/lib/route-flag.d.ts +50 -0
  151. package/dist/src/lib/route-flag.d.ts.map +1 -0
  152. package/dist/src/lib/route-flag.js +52 -0
  153. package/dist/src/lib/route-flag.js.map +1 -0
  154. package/dist/src/lib/sysprompt-strip.d.ts +54 -0
  155. package/dist/src/lib/sysprompt-strip.d.ts.map +1 -0
  156. package/dist/src/lib/sysprompt-strip.js +75 -0
  157. package/dist/src/lib/sysprompt-strip.js.map +1 -0
  158. package/dist/src/lib/telemetry.d.ts +39 -0
  159. package/dist/src/lib/telemetry.d.ts.map +1 -0
  160. package/dist/src/lib/telemetry.js +73 -0
  161. package/dist/src/lib/telemetry.js.map +1 -0
  162. package/dist/src/lib/test-mode.d.ts +27 -0
  163. package/dist/src/lib/test-mode.d.ts.map +1 -0
  164. package/dist/src/lib/test-mode.js +38 -0
  165. package/dist/src/lib/test-mode.js.map +1 -0
  166. package/dist/src/lib/vendor-paths.d.ts +15 -0
  167. package/dist/src/lib/vendor-paths.d.ts.map +1 -0
  168. package/dist/src/lib/vendor-paths.js +32 -0
  169. package/dist/src/lib/vendor-paths.js.map +1 -0
  170. package/dist/src/logger.d.ts +17 -0
  171. package/dist/src/logger.d.ts.map +1 -0
  172. package/dist/src/logger.js +46 -0
  173. package/dist/src/logger.js.map +1 -0
  174. package/dist/src/mcp/bridge.d.ts +22 -0
  175. package/dist/src/mcp/bridge.d.ts.map +1 -0
  176. package/dist/src/mcp/bridge.js +78 -0
  177. package/dist/src/mcp/bridge.js.map +1 -0
  178. package/dist/src/mcp/index.d.ts +3 -0
  179. package/dist/src/mcp/index.d.ts.map +1 -0
  180. package/dist/src/mcp/index.js +2 -0
  181. package/dist/src/mcp/index.js.map +1 -0
  182. package/dist/src/models.d.ts +70 -0
  183. package/dist/src/models.d.ts.map +1 -0
  184. package/dist/src/models.js +289 -0
  185. package/dist/src/models.js.map +1 -0
  186. package/dist/src/openai-compat/cli-stream-parser.d.ts +135 -0
  187. package/dist/src/openai-compat/cli-stream-parser.d.ts.map +1 -0
  188. package/dist/src/openai-compat/cli-stream-parser.js +195 -0
  189. package/dist/src/openai-compat/cli-stream-parser.js.map +1 -0
  190. package/dist/src/openai-compat/index.d.ts +2 -0
  191. package/dist/src/openai-compat/index.d.ts.map +1 -0
  192. package/dist/src/openai-compat/index.js +2 -0
  193. package/dist/src/openai-compat/index.js.map +1 -0
  194. package/dist/src/openai-compat/openai-compat.d.ts +281 -0
  195. package/dist/src/openai-compat/openai-compat.d.ts.map +1 -0
  196. package/dist/src/openai-compat/openai-compat.js +939 -0
  197. package/dist/src/openai-compat/openai-compat.js.map +1 -0
  198. package/dist/src/openai-compat/skill-resolver.d.ts +36 -0
  199. package/dist/src/openai-compat/skill-resolver.d.ts.map +1 -0
  200. package/dist/src/openai-compat/skill-resolver.js +134 -0
  201. package/dist/src/openai-compat/skill-resolver.js.map +1 -0
  202. package/dist/src/openai-compat/sse-translator.d.ts +32 -0
  203. package/dist/src/openai-compat/sse-translator.d.ts.map +1 -0
  204. package/dist/src/openai-compat/sse-translator.js +155 -0
  205. package/dist/src/openai-compat/sse-translator.js.map +1 -0
  206. package/dist/src/proxy/anthropic-adapter.d.ts +137 -0
  207. package/dist/src/proxy/anthropic-adapter.d.ts.map +1 -0
  208. package/dist/src/proxy/anthropic-adapter.js +392 -0
  209. package/dist/src/proxy/anthropic-adapter.js.map +1 -0
  210. package/dist/src/proxy/handler.d.ts +40 -0
  211. package/dist/src/proxy/handler.d.ts.map +1 -0
  212. package/dist/src/proxy/handler.js +378 -0
  213. package/dist/src/proxy/handler.js.map +1 -0
  214. package/dist/src/proxy/index.d.ts +5 -0
  215. package/dist/src/proxy/index.d.ts.map +1 -0
  216. package/dist/src/proxy/index.js +5 -0
  217. package/dist/src/proxy/index.js.map +1 -0
  218. package/dist/src/proxy/schema-cleaner.d.ts +12 -0
  219. package/dist/src/proxy/schema-cleaner.d.ts.map +1 -0
  220. package/dist/src/proxy/schema-cleaner.js +34 -0
  221. package/dist/src/proxy/schema-cleaner.js.map +1 -0
  222. package/dist/src/proxy/thought-cache.d.ts +20 -0
  223. package/dist/src/proxy/thought-cache.d.ts.map +1 -0
  224. package/dist/src/proxy/thought-cache.js +53 -0
  225. package/dist/src/proxy/thought-cache.js.map +1 -0
  226. package/dist/src/session/embedded-server.d.ts +26 -0
  227. package/dist/src/session/embedded-server.d.ts.map +1 -0
  228. package/dist/src/session/embedded-server.js +367 -0
  229. package/dist/src/session/embedded-server.js.map +1 -0
  230. package/dist/src/session/inbox-manager.d.ts +39 -0
  231. package/dist/src/session/inbox-manager.d.ts.map +1 -0
  232. package/dist/src/session/inbox-manager.js +111 -0
  233. package/dist/src/session/inbox-manager.js.map +1 -0
  234. package/dist/src/session/index.d.ts +4 -0
  235. package/dist/src/session/index.d.ts.map +1 -0
  236. package/dist/src/session/index.js +4 -0
  237. package/dist/src/session/index.js.map +1 -0
  238. package/dist/src/session/session-manager.d.ts +212 -0
  239. package/dist/src/session/session-manager.d.ts.map +1 -0
  240. package/dist/src/session/session-manager.js +1351 -0
  241. package/dist/src/session/session-manager.js.map +1 -0
  242. package/dist/src/session-bootstrap/cwd-patch.d.ts +51 -0
  243. package/dist/src/session-bootstrap/cwd-patch.d.ts.map +1 -0
  244. package/dist/src/session-bootstrap/cwd-patch.js +955 -0
  245. package/dist/src/session-bootstrap/cwd-patch.js.map +1 -0
  246. package/dist/src/session-bootstrap/index.d.ts +4 -0
  247. package/dist/src/session-bootstrap/index.d.ts.map +1 -0
  248. package/dist/src/session-bootstrap/index.js +4 -0
  249. package/dist/src/session-bootstrap/index.js.map +1 -0
  250. package/dist/src/session-bootstrap/sysprompt-strip.d.ts +26 -0
  251. package/dist/src/session-bootstrap/sysprompt-strip.d.ts.map +1 -0
  252. package/dist/src/session-bootstrap/sysprompt-strip.js +57 -0
  253. package/dist/src/session-bootstrap/sysprompt-strip.js.map +1 -0
  254. package/dist/src/session-bootstrap/think-conflict-resolver.d.ts +33 -0
  255. package/dist/src/session-bootstrap/think-conflict-resolver.d.ts.map +1 -0
  256. package/dist/src/session-bootstrap/think-conflict-resolver.js +234 -0
  257. package/dist/src/session-bootstrap/think-conflict-resolver.js.map +1 -0
  258. package/dist/src/types.d.ts +489 -0
  259. package/dist/src/types.d.ts.map +1 -0
  260. package/dist/src/types.js +8 -0
  261. package/dist/src/types.js.map +1 -0
  262. package/dist/src/validation.d.ts +32 -0
  263. package/dist/src/validation.d.ts.map +1 -0
  264. package/dist/src/validation.js +104 -0
  265. package/dist/src/validation.js.map +1 -0
  266. package/dist/tests/_helpers/subprocess-mock.d.ts +35 -0
  267. package/dist/tests/_helpers/subprocess-mock.d.ts.map +1 -0
  268. package/dist/tests/_helpers/subprocess-mock.js +136 -0
  269. package/dist/tests/_helpers/subprocess-mock.js.map +1 -0
  270. package/dist/tests/auto-recovery.test.d.ts +2 -0
  271. package/dist/tests/auto-recovery.test.d.ts.map +1 -0
  272. package/dist/tests/auto-recovery.test.js +189 -0
  273. package/dist/tests/auto-recovery.test.js.map +1 -0
  274. package/dist/tests/bench-harness.test.d.ts +2 -0
  275. package/dist/tests/bench-harness.test.d.ts.map +1 -0
  276. package/dist/tests/bench-harness.test.js +21 -0
  277. package/dist/tests/bench-harness.test.js.map +1 -0
  278. package/dist/tests/cache-parity.test.d.ts +2 -0
  279. package/dist/tests/cache-parity.test.d.ts.map +1 -0
  280. package/dist/tests/cache-parity.test.js +401 -0
  281. package/dist/tests/cache-parity.test.js.map +1 -0
  282. package/dist/tests/command-router.test.d.ts +2 -0
  283. package/dist/tests/command-router.test.d.ts.map +1 -0
  284. package/dist/tests/command-router.test.js +60 -0
  285. package/dist/tests/command-router.test.js.map +1 -0
  286. package/dist/tests/council.test.d.ts +2 -0
  287. package/dist/tests/council.test.d.ts.map +1 -0
  288. package/dist/tests/council.test.js +20 -0
  289. package/dist/tests/council.test.js.map +1 -0
  290. package/dist/tests/drift-detector.test.d.ts +2 -0
  291. package/dist/tests/drift-detector.test.d.ts.map +1 -0
  292. package/dist/tests/drift-detector.test.js +268 -0
  293. package/dist/tests/drift-detector.test.js.map +1 -0
  294. package/dist/tests/eager-bootstrap-gating.test.d.ts +9 -0
  295. package/dist/tests/eager-bootstrap-gating.test.d.ts.map +1 -0
  296. package/dist/tests/eager-bootstrap-gating.test.js +97 -0
  297. package/dist/tests/eager-bootstrap-gating.test.js.map +1 -0
  298. package/dist/tests/engines.test.d.ts +2 -0
  299. package/dist/tests/engines.test.d.ts.map +1 -0
  300. package/dist/tests/engines.test.js +8 -0
  301. package/dist/tests/engines.test.js.map +1 -0
  302. package/dist/tests/error-formatter.test.d.ts +2 -0
  303. package/dist/tests/error-formatter.test.d.ts.map +1 -0
  304. package/dist/tests/error-formatter.test.js +220 -0
  305. package/dist/tests/error-formatter.test.js.map +1 -0
  306. package/dist/tests/health.test.d.ts +2 -0
  307. package/dist/tests/health.test.d.ts.map +1 -0
  308. package/dist/tests/health.test.js +110 -0
  309. package/dist/tests/health.test.js.map +1 -0
  310. package/dist/tests/heartbeat-workaround.test.d.ts +2 -0
  311. package/dist/tests/heartbeat-workaround.test.d.ts.map +1 -0
  312. package/dist/tests/heartbeat-workaround.test.js +90 -0
  313. package/dist/tests/heartbeat-workaround.test.js.map +1 -0
  314. package/dist/tests/index.test.d.ts +2 -0
  315. package/dist/tests/index.test.d.ts.map +1 -0
  316. package/dist/tests/index.test.js +7 -0
  317. package/dist/tests/index.test.js.map +1 -0
  318. package/dist/tests/lib-sysprompt-strip.test.d.ts +2 -0
  319. package/dist/tests/lib-sysprompt-strip.test.d.ts.map +1 -0
  320. package/dist/tests/lib-sysprompt-strip.test.js +145 -0
  321. package/dist/tests/lib-sysprompt-strip.test.js.map +1 -0
  322. package/dist/tests/listener-activation.test.d.ts +2 -0
  323. package/dist/tests/listener-activation.test.d.ts.map +1 -0
  324. package/dist/tests/listener-activation.test.js +87 -0
  325. package/dist/tests/listener-activation.test.js.map +1 -0
  326. package/dist/tests/mcp-bridge.test.d.ts +2 -0
  327. package/dist/tests/mcp-bridge.test.d.ts.map +1 -0
  328. package/dist/tests/mcp-bridge.test.js +137 -0
  329. package/dist/tests/mcp-bridge.test.js.map +1 -0
  330. package/dist/tests/openai-compat.test.d.ts +2 -0
  331. package/dist/tests/openai-compat.test.d.ts.map +1 -0
  332. package/dist/tests/openai-compat.test.js +8 -0
  333. package/dist/tests/openai-compat.test.js.map +1 -0
  334. package/dist/tests/proxy-heartbeat-integration.test.d.ts +15 -0
  335. package/dist/tests/proxy-heartbeat-integration.test.d.ts.map +1 -0
  336. package/dist/tests/proxy-heartbeat-integration.test.js +122 -0
  337. package/dist/tests/proxy-heartbeat-integration.test.js.map +1 -0
  338. package/dist/tests/proxy.test.d.ts +2 -0
  339. package/dist/tests/proxy.test.d.ts.map +1 -0
  340. package/dist/tests/proxy.test.js +8 -0
  341. package/dist/tests/proxy.test.js.map +1 -0
  342. package/dist/tests/register-guard-stacking.test.d.ts +2 -0
  343. package/dist/tests/register-guard-stacking.test.d.ts.map +1 -0
  344. package/dist/tests/register-guard-stacking.test.js +61 -0
  345. package/dist/tests/register-guard-stacking.test.js.map +1 -0
  346. package/dist/tests/register-guard.test.d.ts +2 -0
  347. package/dist/tests/register-guard.test.d.ts.map +1 -0
  348. package/dist/tests/register-guard.test.js +129 -0
  349. package/dist/tests/register-guard.test.js.map +1 -0
  350. package/dist/tests/route-flag-rollback.test.d.ts +2 -0
  351. package/dist/tests/route-flag-rollback.test.d.ts.map +1 -0
  352. package/dist/tests/route-flag-rollback.test.js +70 -0
  353. package/dist/tests/route-flag-rollback.test.js.map +1 -0
  354. package/dist/tests/route-flag.test.d.ts +2 -0
  355. package/dist/tests/route-flag.test.d.ts.map +1 -0
  356. package/dist/tests/route-flag.test.js +101 -0
  357. package/dist/tests/route-flag.test.js.map +1 -0
  358. package/dist/tests/session-bootstrap.test.d.ts +2 -0
  359. package/dist/tests/session-bootstrap.test.d.ts.map +1 -0
  360. package/dist/tests/session-bootstrap.test.js +183 -0
  361. package/dist/tests/session-bootstrap.test.js.map +1 -0
  362. package/dist/tests/session.test.d.ts +2 -0
  363. package/dist/tests/session.test.d.ts.map +1 -0
  364. package/dist/tests/session.test.js +17 -0
  365. package/dist/tests/session.test.js.map +1 -0
  366. package/dist/tests/state-machine.test.d.ts +2 -0
  367. package/dist/tests/state-machine.test.d.ts.map +1 -0
  368. package/dist/tests/state-machine.test.js +133 -0
  369. package/dist/tests/state-machine.test.js.map +1 -0
  370. package/dist/tests/streaming/cli-stream-parser.test.d.ts +2 -0
  371. package/dist/tests/streaming/cli-stream-parser.test.d.ts.map +1 -0
  372. package/dist/tests/streaming/cli-stream-parser.test.js +233 -0
  373. package/dist/tests/streaming/cli-stream-parser.test.js.map +1 -0
  374. package/dist/tests/streaming/feature-flag.test.d.ts +14 -0
  375. package/dist/tests/streaming/feature-flag.test.d.ts.map +1 -0
  376. package/dist/tests/streaming/feature-flag.test.js +163 -0
  377. package/dist/tests/streaming/feature-flag.test.js.map +1 -0
  378. package/dist/tests/streaming/no-tools-prompt.test.d.ts +17 -0
  379. package/dist/tests/streaming/no-tools-prompt.test.d.ts.map +1 -0
  380. package/dist/tests/streaming/no-tools-prompt.test.js +229 -0
  381. package/dist/tests/streaming/no-tools-prompt.test.js.map +1 -0
  382. package/dist/tests/streaming/skill-plus-tools.test.d.ts +14 -0
  383. package/dist/tests/streaming/skill-plus-tools.test.d.ts.map +1 -0
  384. package/dist/tests/streaming/skill-plus-tools.test.js +234 -0
  385. package/dist/tests/streaming/skill-plus-tools.test.js.map +1 -0
  386. package/dist/tests/streaming/sse-translator.test.d.ts +2 -0
  387. package/dist/tests/streaming/sse-translator.test.d.ts.map +1 -0
  388. package/dist/tests/streaming/sse-translator.test.js +227 -0
  389. package/dist/tests/streaming/sse-translator.test.js.map +1 -0
  390. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts +11 -0
  391. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts.map +1 -0
  392. package/dist/tests/streaming/tool-result-roundtrip.test.js +215 -0
  393. package/dist/tests/streaming/tool-result-roundtrip.test.js.map +1 -0
  394. package/dist/tests/streaming/tool-use-translation.test.d.ts +10 -0
  395. package/dist/tests/streaming/tool-use-translation.test.d.ts.map +1 -0
  396. package/dist/tests/streaming/tool-use-translation.test.js +251 -0
  397. package/dist/tests/streaming/tool-use-translation.test.js.map +1 -0
  398. package/dist/tests/telegram-bridge.test.d.ts +2 -0
  399. package/dist/tests/telegram-bridge.test.d.ts.map +1 -0
  400. package/dist/tests/telegram-bridge.test.js +17 -0
  401. package/dist/tests/telegram-bridge.test.js.map +1 -0
  402. package/dist/tests/telegram-injector.test.d.ts +2 -0
  403. package/dist/tests/telegram-injector.test.d.ts.map +1 -0
  404. package/dist/tests/telegram-injector.test.js +74 -0
  405. package/dist/tests/telegram-injector.test.js.map +1 -0
  406. package/dist/tests/telemetry.test.d.ts +2 -0
  407. package/dist/tests/telemetry.test.d.ts.map +1 -0
  408. package/dist/tests/telemetry.test.js +405 -0
  409. package/dist/tests/telemetry.test.js.map +1 -0
  410. package/dist/tests/test-mode.test.d.ts +2 -0
  411. package/dist/tests/test-mode.test.d.ts.map +1 -0
  412. package/dist/tests/test-mode.test.js +39 -0
  413. package/dist/tests/test-mode.test.js.map +1 -0
  414. package/mcp-config.template.json +13 -0
  415. package/mcp-tools.json +1 -0
  416. package/openclaw-mcp-bridge.cjs +152 -0
  417. package/openclaw.plugin.json +30 -0
  418. package/package.json +45 -0
  419. package/skills/.gitkeep +0 -0
  420. package/stubs/commands-status-deps.runtime.js +10 -0
  421. package/stubs/status.runtime.js +149 -0
  422. package/vendor/base-oneshot-session.d.ts +87 -0
  423. package/vendor/base-oneshot-session.js +227 -0
  424. package/vendor/base-oneshot-session.js.map +1 -0
  425. package/vendor/circuit-breaker.d.ts +21 -0
  426. package/vendor/circuit-breaker.js +47 -0
  427. package/vendor/circuit-breaker.js.map +1 -0
  428. package/vendor/consensus.d.ts +20 -0
  429. package/vendor/consensus.js +52 -0
  430. package/vendor/consensus.js.map +1 -0
  431. package/vendor/constants.d.ts +130 -0
  432. package/vendor/constants.js +139 -0
  433. package/vendor/constants.js.map +1 -0
  434. package/vendor/council.d.ts +67 -0
  435. package/vendor/council.js +913 -0
  436. package/vendor/council.js.map +1 -0
  437. package/vendor/embedded-server.d.ts +25 -0
  438. package/vendor/embedded-server.js +360 -0
  439. package/vendor/embedded-server.js.map +1 -0
  440. package/vendor/inbox-manager.d.ts +38 -0
  441. package/vendor/inbox-manager.js +111 -0
  442. package/vendor/inbox-manager.js.map +1 -0
  443. package/vendor/index.d.ts +63 -0
  444. package/vendor/index.js +705 -0
  445. package/vendor/index.js.map +1 -0
  446. package/vendor/logger.d.ts +16 -0
  447. package/vendor/logger.js +44 -0
  448. package/vendor/logger.js.map +1 -0
  449. package/vendor/models.d.ts +69 -0
  450. package/vendor/models.js +289 -0
  451. package/vendor/models.js.map +1 -0
  452. package/vendor/openai-compat.d.ts +197 -0
  453. package/vendor/openai-compat.js +721 -0
  454. package/vendor/openai-compat.js.map +1 -0
  455. package/vendor/persistent-codex-session.d.ts +16 -0
  456. package/vendor/persistent-codex-session.js +105 -0
  457. package/vendor/persistent-codex-session.js.map +1 -0
  458. package/vendor/persistent-cursor-session.d.ts +21 -0
  459. package/vendor/persistent-cursor-session.js +241 -0
  460. package/vendor/persistent-cursor-session.js.map +1 -0
  461. package/vendor/persistent-custom-session.d.ts +78 -0
  462. package/vendor/persistent-custom-session.js +937 -0
  463. package/vendor/persistent-custom-session.js.map +1 -0
  464. package/vendor/persistent-gemini-session.d.ts +21 -0
  465. package/vendor/persistent-gemini-session.js +216 -0
  466. package/vendor/persistent-gemini-session.js.map +1 -0
  467. package/vendor/persistent-session.d.ts +74 -0
  468. package/vendor/persistent-session.js +684 -0
  469. package/vendor/persistent-session.js.map +1 -0
  470. package/vendor/proxy/anthropic-adapter.d.ts +136 -0
  471. package/vendor/proxy/anthropic-adapter.js +392 -0
  472. package/vendor/proxy/anthropic-adapter.js.map +1 -0
  473. package/vendor/proxy/handler.d.ts +39 -0
  474. package/vendor/proxy/handler.js +323 -0
  475. package/vendor/proxy/handler.js.map +1 -0
  476. package/vendor/proxy/schema-cleaner.d.ts +11 -0
  477. package/vendor/proxy/schema-cleaner.js +34 -0
  478. package/vendor/proxy/schema-cleaner.js.map +1 -0
  479. package/vendor/proxy/thought-cache.d.ts +19 -0
  480. package/vendor/proxy/thought-cache.js +53 -0
  481. package/vendor/proxy/thought-cache.js.map +1 -0
  482. package/vendor/session-manager.d.ts +211 -0
  483. package/vendor/session-manager.js +1345 -0
  484. package/vendor/session-manager.js.map +1 -0
  485. package/vendor/skill-resolver.js +107 -0
  486. package/vendor/types.d.ts +466 -0
  487. package/vendor/types.js +8 -0
  488. package/vendor/types.js.map +1 -0
  489. package/vendor/validation.d.ts +31 -0
  490. package/vendor/validation.js +104 -0
  491. package/vendor/validation.js.map +1 -0
@@ -0,0 +1,939 @@
1
+ /**
2
+ * OpenAI-compatible /v1/chat/completions endpoint.
3
+ *
4
+ * Bridges OpenAI API format to persistent Claude Code sessions, enabling
5
+ * webchat frontends (ChatGPT-Next-Web, Open WebUI, etc.) to use the plugin
6
+ * as a drop-in backend. Stateful sessions maximize Anthropic prompt caching.
7
+ */
8
+ import * as http from 'node:http';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import * as os from 'node:os';
12
+ import { randomUUID, createHash } from 'node:crypto';
13
+ import { resolveEngineAndModel } from '../models.js';
14
+ import { OPENAI_COMPAT_DEFAULT_MODEL, OPENAI_COMPAT_AUTO_COMPACT_THRESHOLD, OPENAI_COMPAT_SESSION_PREFIX, } from '../constants.js';
15
+ import { getOpenaiCompatToolsPerMessage, isOpenaiCompatNewConvoHeuristic, getOpenaiCompatStatusUrl, } from '../lib/config.js';
16
+ import { maybeInlineSkill } from './skill-resolver.js';
17
+ // ─── Session Key Resolution ──────────────────────────────────────────────────
18
+ /**
19
+ * Derive a session key from the request.
20
+ * Priority: X-Session-Id header > user field > sha1(model + systemPrompt) > "default"
21
+ *
22
+ * The system-prompt-hash fallback prevents the bug where every caller without
23
+ * X-Session-Id or `user` collapses onto a single shared "openai-default"
24
+ * plugin session. In multi-caller setups (OpenClaw routing the main agent,
25
+ * cron jobs, and subagents through the same gateway) that previously meant
26
+ * every request serialized against every other and frequently picked up the
27
+ * wrong session's appendSystemPrompt — also a privacy leak across callers.
28
+ *
29
+ * The model is mixed into the hash so that two callers with the same system
30
+ * prompt but different requested models don't collide and silently get
31
+ * responses from the wrong model. Originally diagnosed in PR #40 by
32
+ * @megayounus786.
33
+ */
34
+ /**
35
+ * When set (to '1', 'true', 'yes'), the proxy preserves the pre-fix behavior:
36
+ * - tools injected into every user message
37
+ * - session key NOT fingerprinted by tools (same session across tool changes)
38
+ * Default (unset) is the new behavior: tools embedded in session system prompt
39
+ * at create time + session key fingerprinted by tools. The new behavior
40
+ * eliminates periodic latency spikes but does not support mutating the tool
41
+ * list within a single session (a new session is created when tools change).
42
+ */
43
+ export function isToolsPerMessageModeEnabled() {
44
+ const v = getOpenaiCompatToolsPerMessage();
45
+ if (!v)
46
+ return false;
47
+ const t = v.trim().toLowerCase();
48
+ return t === '1' || t === 'true' || t === 'yes';
49
+ }
50
+ /**
51
+ * Phase 2 R5: tool-stream mode flag. When `CC_OPENCLAW_TOOL_STREAM=1` AND the
52
+ * caller provides `tools[]`, cc-openclaw skips the defensive "no tools"
53
+ * system prompt and does NOT clear `sessionConfig.tools`, allowing Claude
54
+ * CLI's native tool_use events to flow through the new parser+translator
55
+ * pipeline (Phase 4 Pillar 0.5). Default off; opt-in for the new path.
56
+ */
57
+ export function isToolStreamMode() {
58
+ return process.env.CC_OPENCLAW_TOOL_STREAM === '1';
59
+ }
60
+ /**
61
+ * Generate the "no built-in tools" system prompt preamble.
62
+ * The `toolLocation` parameter controls how the model is told where to find
63
+ * tool definitions — 'system' means "in the <available_tools> block below"
64
+ * (tools baked into system prompt), 'user' means "in <available_tools> tags
65
+ * in the user message" (legacy per-turn injection).
66
+ */
67
+ export function noToolsSystemPrompt(toolLocation) {
68
+ const allowBuiltins = process.env.CC_OPENCLAW_ALLOW_BUILTINS === '1';
69
+ const locationHint = toolLocation === 'system'
70
+ ? 'in the <available_tools> block below'
71
+ : 'in <available_tools> tags in the user message';
72
+ if (allowBuiltins) {
73
+ // Phase 2.1 dual-source mode: Claude CLI's built-in tools (Bash, Read,
74
+ // Write, Edit, Glob, Grep, WebFetch, WebSearch, NotebookEdit, +any
75
+ // MCP-bridged tools) coexist with the proxy-defined <available_tools>.
76
+ // The model picks whichever fits.
77
+ return ('You are an AI assistant with TWO complementary tool sources available:\n' +
78
+ '1. Claude Code CLI built-ins: Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, NotebookEdit, plus any MCP-bridged tools. Invoke these directly via your normal tool_use mechanism.\n' +
79
+ `2. Proxy-defined tools: ${locationHint}. Use these by emitting <tool_calls> XML as instructed there.\n` +
80
+ 'When a request needs action, you MUST use whichever tool source fits — do not refuse on grounds of "no tools".\n' +
81
+ 'For research / web fetching / file ops / command execution: prefer the built-ins (WebFetch, WebSearch, Bash, Read).\n' +
82
+ 'For tasks where the proxy ships a specific custom tool: prefer the proxy tool.\n' +
83
+ 'If a tool you would normally use is not available in either source, attempt the task with what IS available and explain concretely which tool would have helped.');
84
+ }
85
+ return ('You are an AI assistant operating through a proxy that provides a specific set of tools.\n' +
86
+ `Your tools are defined ${locationHint}. Use them by emitting <tool_calls> XML as instructed there.\n` +
87
+ 'When a request needs action, you MUST use the tools that are defined — do not refuse on the grounds of "no tools".\n' +
88
+ 'If a specific tool you would normally use is not in <available_tools>, do the best you can with what IS provided, or report concretely which tool would be needed.\n' +
89
+ 'You do NOT have direct access to Claude Code CLI built-ins (Bash, Read, Write, Edit, Glob, Grep) outside of <available_tools>; do not invoke them directly.\n' +
90
+ 'If no <available_tools> are provided at all, respond with text only.');
91
+ }
92
+ /**
93
+ * Build the full session system prompt for a Claude Code session with tools.
94
+ * Exported for testability — called from `handleChatCompletion`.
95
+ *
96
+ * - Default mode: tools are embedded in the system prompt (cacheable by Anthropic).
97
+ * - Legacy mode (OPENAI_COMPAT_TOOLS_PER_MESSAGE=1): tools are NOT embedded;
98
+ * they'll be injected per-turn in the user message instead.
99
+ */
100
+ export function buildSessionSystemPrompt(tools, callerSystemPrompt) {
101
+ // Phase 2 R5: in tool-stream mode with tools provided, skip the defensive
102
+ // "no tools" preamble and the <available_tools> block entirely. Claude CLI
103
+ // gets the tools natively via sessionConfig.tools (not cleared) and emits
104
+ // tool_use events that the new parser+translator translate to OpenAI SSE.
105
+ if (isToolStreamMode() && tools && tools.length > 0) {
106
+ return callerSystemPrompt ?? '';
107
+ }
108
+ if (isToolsPerMessageModeEnabled()) {
109
+ const preamble = noToolsSystemPrompt('user');
110
+ return callerSystemPrompt ? `${preamble}\n\n${callerSystemPrompt}` : preamble;
111
+ }
112
+ const preamble = noToolsSystemPrompt('system');
113
+ const toolBlock = buildToolPromptBlock(tools);
114
+ const systemWithTools = `${preamble}\n\n${toolBlock}`;
115
+ return callerSystemPrompt ? `${systemWithTools}\n\n${callerSystemPrompt}` : systemWithTools;
116
+ }
117
+ export function resolveSessionKey(body, headers) {
118
+ const headerKey = headers['x-session-id'];
119
+ if (typeof headerKey === 'string' && headerKey.trim())
120
+ return headerKey.trim();
121
+ if (body.user && body.user.trim())
122
+ return body.user.trim();
123
+ const sys = (body.messages || [])
124
+ .filter((m) => m && m.role === 'system')
125
+ .map((m) => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)))
126
+ .join('\n');
127
+ const modelTag = (body.model || '').toString();
128
+ // Include a fingerprint of the tool list so that two requests with the same
129
+ // system prompt but different tool definitions land in different sessions.
130
+ // The tool schemas are baked into the session system prompt on create; if
131
+ // tools change we need a new session rather than re-using a stale one.
132
+ // Hash only tool names + a short description prefix to keep the fingerprint
133
+ // small and stable against schema formatting differences.
134
+ //
135
+ // Opt-out: OPENAI_COMPAT_TOOLS_PER_MESSAGE=1 restores the pre-fix behavior
136
+ // of keying sessions only by system prompt + model. Enable this if you have
137
+ // callers that mutate their tool list within one conversation and rely on
138
+ // continuing history across tool changes.
139
+ const toolsFingerprint = isToolsPerMessageModeEnabled()
140
+ ? ''
141
+ : (body.tools || [])
142
+ .map((t) => {
143
+ const fn = t?.function;
144
+ if (!fn?.name)
145
+ return '';
146
+ const descPrefix = (typeof fn.description === 'string' ? fn.description : '').slice(0, 64);
147
+ return `${fn.name}:${descPrefix}`;
148
+ })
149
+ .filter(Boolean)
150
+ .join('|');
151
+ if (sys || modelTag || toolsFingerprint) {
152
+ return ('sys-' +
153
+ createHash('sha1')
154
+ .update(modelTag + '\n' + sys + '\n' + toolsFingerprint)
155
+ .digest('hex')
156
+ .slice(0, 12));
157
+ }
158
+ return 'default';
159
+ }
160
+ /** Build the full session name from a key */
161
+ export function sessionNameFromKey(key) {
162
+ return `${OPENAI_COMPAT_SESSION_PREFIX}${key}`;
163
+ }
164
+ // ─── Function Calling Support ────────────────────────────────────────────────
165
+ /**
166
+ * Convert OpenAI tool definitions into a structured prompt block.
167
+ * Injected into the user message so the CLI model sees tool definitions
168
+ * and responds with <tool_calls> tags when it wants to invoke a function.
169
+ */
170
+ export function buildToolPromptBlock(tools) {
171
+ if (!tools?.length)
172
+ return '';
173
+ const toolDefs = tools
174
+ .map((t) => {
175
+ const fn = t.function;
176
+ const params = JSON.stringify(fn.parameters, null, 2);
177
+ return `### ${fn.name}\n${fn.description}\n\nParameters:\n\`\`\`json\n${params}\n\`\`\``;
178
+ })
179
+ .join('\n\n');
180
+ return ('<available_tools>\n' +
181
+ 'You have access to the following tools. When you need to use a tool, respond with a JSON array wrapped in <tool_calls> tags.\n\n' +
182
+ 'FORMAT:\n' +
183
+ '<tool_calls>\n' +
184
+ '[{"name": "tool_name", "arguments": {"param1": "value1"}}]\n' +
185
+ '</tool_calls>\n\n' +
186
+ 'If you do NOT need any tools, respond normally with text only (no <tool_calls> tags).\n\n' +
187
+ '## Available Tools\n\n' +
188
+ toolDefs +
189
+ '\n</available_tools>');
190
+ }
191
+ /**
192
+ * Parse tool_calls from CLI text output.
193
+ *
194
+ * Looks for <tool_calls>[...]</tool_calls> tags in the response text.
195
+ * Returns both the extracted text content (before/after tags) and any tool calls found.
196
+ */
197
+ export function parseToolCallsFromText(text) {
198
+ // Match ALL <tool_calls> blocks (model may output multiple)
199
+ const tagRegex = /<tool_calls>\s*([\s\S]*?)\s*<\/tool_calls>/g;
200
+ const allCalls = [];
201
+ let lastIndex = 0;
202
+ const textParts = [];
203
+ let m;
204
+ while ((m = tagRegex.exec(text)) !== null) {
205
+ // Collect text before this block
206
+ const before = text.slice(lastIndex, m.index).trim();
207
+ if (before)
208
+ textParts.push(before);
209
+ lastIndex = m.index + m[0].length;
210
+ try {
211
+ const parsed = JSON.parse(m[1].trim());
212
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
213
+ for (const raw of arr) {
214
+ const call = raw;
215
+ if (!call || typeof call !== 'object' || typeof call.name !== 'string')
216
+ continue;
217
+ let args;
218
+ if (typeof call.arguments === 'string') {
219
+ try {
220
+ JSON.parse(call.arguments);
221
+ args = call.arguments;
222
+ }
223
+ catch {
224
+ args = JSON.stringify({ input: call.arguments });
225
+ }
226
+ }
227
+ else {
228
+ args = JSON.stringify(call.arguments ?? {});
229
+ }
230
+ allCalls.push({
231
+ id: `call_${randomUUID().replace(/-/g, '').slice(0, 24)}`,
232
+ type: 'function',
233
+ function: { name: call.name, arguments: args },
234
+ });
235
+ }
236
+ }
237
+ catch {
238
+ // One block failed — keep its text as content
239
+ textParts.push(m[0]);
240
+ }
241
+ }
242
+ // Collect text after last block
243
+ const after = text.slice(lastIndex).trim();
244
+ if (after)
245
+ textParts.push(after);
246
+ // Strip <tool_result> and <tool_results> tags that the model may echo back
247
+ // from the serialized tool results we injected earlier.
248
+ const stripToolResultTags = (s) => s
249
+ .replace(/<tool_results?>[\s\S]*?<\/tool_results?>/g, '')
250
+ .replace(/<tool_results?[^>]*>/g, '')
251
+ .trim();
252
+ if (allCalls.length > 0) {
253
+ const raw = textParts.join('\n').trim();
254
+ const cleaned = raw ? stripToolResultTags(raw) : null;
255
+ return { textContent: cleaned || null, toolCalls: allCalls };
256
+ }
257
+ const cleaned = text ? stripToolResultTags(text) : null;
258
+ return { textContent: cleaned || null, toolCalls: [] };
259
+ }
260
+ /**
261
+ * Serialize tool result messages into a text block for the CLI model.
262
+ * Converts OpenAI `tool` role messages into <tool_result> tags.
263
+ *
264
+ * Legacy path (CC_OPENCLAW_TOOL_STREAM=0). Used when the model receives
265
+ * tool definitions via the system prompt's <available_tools> XML block
266
+ * and emits <tool_calls> XML in response. Tool-stream mode (R4) uses
267
+ * `serializeToolResultsAsBlocks()` instead, returning native Anthropic
268
+ * `tool_result` content blocks that Claude CLI parses directly.
269
+ */
270
+ export function serializeToolResults(messages) {
271
+ const toolMessages = messages.filter((m) => m.role === 'tool');
272
+ if (!toolMessages.length)
273
+ return '';
274
+ const results = toolMessages
275
+ .map((m) => {
276
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
277
+ return `<tool_result tool_call_id="${m.tool_call_id || 'unknown'}">\n${content}\n</tool_result>`;
278
+ })
279
+ .join('\n\n');
280
+ return `<tool_results>\n${results}\n</tool_results>\n\nAbove are the results of the tool calls you requested. Continue your response based on these results.`;
281
+ }
282
+ export function serializeToolResultsAsBlocks(messages) {
283
+ return messages
284
+ .filter((m) => m.role === 'tool')
285
+ .map((m) => ({
286
+ type: 'tool_result',
287
+ tool_use_id: m.tool_call_id || 'unknown',
288
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
289
+ }));
290
+ }
291
+ /**
292
+ * Extract the relevant parts from an OpenAI messages array.
293
+ *
294
+ * Sessions are stateful — we only need the last user message. The tricky
295
+ * question is whether to start a fresh session or append to the existing one.
296
+ *
297
+ * Default mode (no env var): only honor an explicit `X-Session-Reset: 1`
298
+ * header. This is correct for clients that maintain their own conversation
299
+ * transcript and forward only the latest user turn (OpenClaw main agent
300
+ * loop, cron jobs, subagents). The previous heuristic
301
+ * (`nonSystemMessages.length <= 1`) fired on every such request, killing the
302
+ * persistent CLI every turn and preventing Anthropic prompt caching from
303
+ * ever warming. Originally diagnosed in PR #40 by @megayounus786.
304
+ *
305
+ * Legacy mode (`OPENAI_COMPAT_NEW_CONVO_HEURISTIC=1`): restore the old
306
+ * `system + single user ⇒ new conversation` rule, for clients that re-send
307
+ * the full transcript on every turn (ChatGPT-Next-Web, Open WebUI, data
308
+ * labeling tools, etc). They use the transcript shape itself as their only
309
+ * "start a new conversation" signal.
310
+ *
311
+ * The env var is read on every call so ops can flip it via launchctl setenv
312
+ * without restarting the server.
313
+ */
314
+ export function extractUserMessage(messages, headers) {
315
+ if (!messages || messages.length === 0) {
316
+ throw new Error('messages array is empty');
317
+ }
318
+ // Normalize content from any message: OpenAI API allows content as a string
319
+ // OR an array of content parts (e.g. multimodal messages with text + images).
320
+ // We need a string for the CLI, so arrays are joined.
321
+ const textOf = (m) => {
322
+ if (typeof m.content === 'string')
323
+ return m.content;
324
+ if (Array.isArray(m.content)) {
325
+ return m.content
326
+ .map((p) => p.text || '')
327
+ .filter(Boolean)
328
+ .join('');
329
+ }
330
+ return m.content != null ? String(m.content) : '';
331
+ };
332
+ // Extract system prompt if present
333
+ const systemMessages = messages.filter((m) => m.role === 'system');
334
+ const systemPrompt = systemMessages.length > 0 ? systemMessages.map(textOf).join('\n') : undefined;
335
+ // Handle tool result messages — only when the LAST non-system message is
336
+ // a tool role (meaning we're in an active tool-use cycle). If the last
337
+ // message is a user role, it's a follow-up in an existing conversation
338
+ // and the old tool results are already in the CLI's history.
339
+ const lastNonSystem = [...messages].reverse().find((m) => m.role !== 'system');
340
+ if (lastNonSystem?.role === 'tool') {
341
+ const userMessages = messages.filter((m) => m.role === 'user');
342
+ const lastUserText = userMessages.length > 0 ? textOf(userMessages[userMessages.length - 1]) : '';
343
+ // Phase 2 R4 wire-up: in tool-stream mode, emit native Anthropic
344
+ // tool_result blocks instead of XML-wrapped text. Claude CLI's
345
+ // stream-json input accepts content arrays directly.
346
+ if (isToolStreamMode()) {
347
+ const toolBlocks = serializeToolResultsAsBlocks(messages);
348
+ const userMessageBlocks = [...toolBlocks];
349
+ if (lastUserText) {
350
+ userMessageBlocks.push({ type: 'text', text: lastUserText });
351
+ }
352
+ // Keep userMessage populated as the legacy XML form for callers
353
+ // that don't yet handle the structured path. Both fields agree in
354
+ // intent; consumers should prefer userMessageBlocks when present.
355
+ const fallback = serializeToolResults(messages);
356
+ const userMessage = lastUserText ? `${fallback}\n\n${lastUserText}` : fallback;
357
+ return { systemPrompt, userMessage, userMessageBlocks, isNewConversation: false };
358
+ }
359
+ const toolResultBlock = serializeToolResults(messages);
360
+ const userMessage = lastUserText ? `${toolResultBlock}\n\n${lastUserText}` : toolResultBlock;
361
+ return { systemPrompt, userMessage, isNewConversation: false };
362
+ }
363
+ // Find last user message
364
+ const userMessages = messages.filter((m) => m.role === 'user');
365
+ if (userMessages.length === 0) {
366
+ throw new Error('No user message found in messages array');
367
+ }
368
+ const rawUserMessage = textOf(userMessages[userMessages.length - 1]);
369
+ // Workspace skill auto-inline: if the last user message is /<skill> [args]
370
+ // and ~/.openclaw/workspace/skills/*/SKILL.md has a matching `name:` in
371
+ // frontmatter, replace the user message with the SKILL.md body so the
372
+ // model has full skill context without needing the Read tool (cc-openclaw
373
+ // disables built-in tools by design — see the `sessionConfig.tools = ''`
374
+ // line below).
375
+ const userMessage = maybeInlineSkill(rawUserMessage) ?? rawUserMessage;
376
+ // 1. Explicit reset header — honored in both modes. Normalize trim+lowercase
377
+ // so callers using `TRUE`, ` 1 `, etc. don't silently fail.
378
+ const rawReset = headers?.['x-session-reset'];
379
+ const resetHeader = typeof rawReset === 'string' ? rawReset.trim().toLowerCase() : '';
380
+ if (resetHeader === 'true' || resetHeader === '1') {
381
+ return { systemPrompt, userMessage, isNewConversation: true };
382
+ }
383
+ // 2. Legacy heuristic — only when explicitly opted in via env var.
384
+ if (isOpenaiCompatNewConvoHeuristic()) {
385
+ const nonSystemMessages = messages.filter((m) => m.role !== 'system');
386
+ return { systemPrompt, userMessage, isNewConversation: nonSystemMessages.length <= 1 };
387
+ }
388
+ return { systemPrompt, userMessage, isNewConversation: false };
389
+ }
390
+ // ─── Response Formatting ─────────────────────────────────────────────────────
391
+ export function formatCompletionResponse(id, model, text, tokensIn, tokensOut, toolCalls) {
392
+ const hasToolCalls = toolCalls && toolCalls.length > 0;
393
+ return {
394
+ id,
395
+ object: 'chat.completion',
396
+ created: Math.floor(Date.now() / 1000),
397
+ model,
398
+ choices: [
399
+ {
400
+ index: 0,
401
+ message: {
402
+ role: 'assistant',
403
+ content: text || null,
404
+ ...(hasToolCalls ? { tool_calls: toolCalls } : {}),
405
+ },
406
+ finish_reason: hasToolCalls ? 'tool_calls' : 'stop',
407
+ },
408
+ ],
409
+ usage: {
410
+ prompt_tokens: tokensIn,
411
+ completion_tokens: tokensOut,
412
+ total_tokens: tokensIn + tokensOut,
413
+ },
414
+ };
415
+ }
416
+ export function formatCompletionChunk(id, model, delta, finishReason) {
417
+ return {
418
+ id,
419
+ object: 'chat.completion.chunk',
420
+ created: Math.floor(Date.now() / 1000),
421
+ model,
422
+ choices: [{ index: 0, delta, finish_reason: finishReason }],
423
+ };
424
+ }
425
+ export async function handleChatCompletion(manager, body, headers, res) {
426
+ // Validate before casting
427
+ if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
428
+ res.writeHead(400, { 'Content-Type': 'application/json' });
429
+ res.end(JSON.stringify({
430
+ error: { message: 'messages is required and must be a non-empty array', type: 'invalid_request_error' },
431
+ }));
432
+ return;
433
+ }
434
+ // Safe cast: messages validated above, other fields are optional
435
+ const request = {
436
+ messages: body.messages,
437
+ model: body.model,
438
+ stream: body.stream,
439
+ temperature: body.temperature,
440
+ max_tokens: body.max_tokens,
441
+ user: body.user,
442
+ tools: body.tools,
443
+ };
444
+ // Validate max_tokens if provided
445
+ if (request.max_tokens !== undefined && (typeof request.max_tokens !== 'number' || request.max_tokens <= 0)) {
446
+ res.writeHead(400, { 'Content-Type': 'application/json' });
447
+ res.end(JSON.stringify({
448
+ error: { message: 'max_tokens must be a positive number', type: 'invalid_request_error' },
449
+ }));
450
+ return;
451
+ }
452
+ const modelStr = request.model || OPENAI_COMPAT_DEFAULT_MODEL;
453
+ const { engine, model: resolvedModel } = resolveEngineAndModel(modelStr);
454
+ const sessionKey = resolveSessionKey(request, headers);
455
+ const sessionName = sessionNameFromKey(sessionKey);
456
+ const isStreaming = request.stream === true;
457
+ let extracted;
458
+ try {
459
+ extracted = extractUserMessage(request.messages, headers);
460
+ }
461
+ catch (err) {
462
+ res.writeHead(400, { 'Content-Type': 'application/json' });
463
+ res.end(JSON.stringify({ error: { message: err.message, type: 'invalid_request_error' } }));
464
+ return;
465
+ }
466
+ // Check if session exists
467
+ const existingSessions = manager.listSessions().map((s) => s.name);
468
+ const sessionExists = existingSessions.includes(sessionName);
469
+ // If new conversation detected and session exists, stop old one first
470
+ if (extracted.isNewConversation && sessionExists) {
471
+ try {
472
+ await manager.stopSession(sessionName);
473
+ }
474
+ catch {
475
+ /* session may have already been cleaned up */
476
+ }
477
+ }
478
+ // Create session if needed
479
+ const needsCreate = !sessionExists || extracted.isNewConversation;
480
+ if (needsCreate) {
481
+ // OpenAI-compat sessions are API proxies, not coding sessions.
482
+ // Use a neutral empty temp dir so the CLI doesn't load CLAUDE.md,
483
+ // git state, or project context from wherever `serve` was started.
484
+ const sessionCwd = path.join(os.tmpdir(), `openclaw-compat-${sessionName}`);
485
+ if (!fs.existsSync(sessionCwd))
486
+ fs.mkdirSync(sessionCwd, { recursive: true });
487
+ const sessionConfig = {
488
+ name: sessionName,
489
+ cwd: sessionCwd,
490
+ engine,
491
+ model: resolvedModel,
492
+ permissionMode: 'bypassPermissions',
493
+ // skipPersistence: tells SessionManager not to write this session to
494
+ // the disk registry, preventing auto-resume of stale sessions.
495
+ // Note: noSessionPersistence (--no-session-persistence) is NOT set
496
+ // because some CLI forks don't support this flag.
497
+ skipPersistence: true,
498
+ };
499
+ // Phase 2.1 (CC_OPENCLAW_ALLOW_BUILTINS=1): when the env flag is set,
500
+ // do NOT disable Claude CLI's built-in tools. Claude's WebFetch /
501
+ // WebSearch / Bash / Read coexist with OpenClaw's <available_tools>.
502
+ // The model picks whichever fits — useful when OpenClaw's forwarded
503
+ // tools list omits something the skill needs (e.g. /search needs web
504
+ // tools that createOpenClawCodingTools doesn't ship).
505
+ //
506
+ // Default behavior (flag unset): disable built-ins when caller provides
507
+ // tools, to avoid model confusion between two tool sources. Flag exists
508
+ // for a controlled rollback path.
509
+ const allowBuiltins = process.env.CC_OPENCLAW_ALLOW_BUILTINS === '1';
510
+ // Phase 2 R5+R3: tool-stream mode forwards the allowlist of tool names
511
+ // from request.tools[] so Claude CLI knows which tools the model is
512
+ // allowed to invoke. Legacy mode clears tools entirely (suppresses
513
+ // built-in tool_use events). allowBuiltins skips both paths.
514
+ if (request.tools?.length && engine === 'claude' && !allowBuiltins) {
515
+ if (isToolStreamMode()) {
516
+ // R3: pass tool names as a comma-separated allowlist. Claude CLI's
517
+ // `--tools` arg accepts this; built-ins not in the list are
518
+ // disallowed for the session. Tool schemas/definitions reach the
519
+ // model via the caller's system prompt (callers already include
520
+ // them in <available_tools> XML).
521
+ const toolNames = request.tools
522
+ .map((t) => t.function?.name)
523
+ .filter((n) => typeof n === 'string' && n.length > 0)
524
+ .join(',');
525
+ if (toolNames)
526
+ sessionConfig.tools = toolNames;
527
+ }
528
+ else {
529
+ sessionConfig.tools = '';
530
+ }
531
+ }
532
+ // Claude Code CLI supports --system-prompt (replace) and --append-system-prompt (append).
533
+ // When the caller provides tools, use --system-prompt to REPLACE the CLI's entire
534
+ // system prompt via buildSessionSystemPrompt(). See that function's doc for details
535
+ // on default vs legacy (OPENAI_COMPAT_TOOLS_PER_MESSAGE=1) behavior.
536
+ if (engine === 'claude') {
537
+ if (request.tools?.length) {
538
+ sessionConfig.systemPrompt = buildSessionSystemPrompt(request.tools, extracted.systemPrompt);
539
+ }
540
+ else if (extracted.systemPrompt) {
541
+ sessionConfig.appendSystemPrompt = extracted.systemPrompt;
542
+ }
543
+ }
544
+ try {
545
+ await manager.startSession(sessionConfig);
546
+ }
547
+ catch (err) {
548
+ res.writeHead(503, { 'Content-Type': 'application/json' });
549
+ res.end(JSON.stringify({
550
+ error: { message: `Failed to start session: ${err.message}`, type: 'server_error' },
551
+ }));
552
+ return;
553
+ }
554
+ }
555
+ // Auto-compact if context is getting full
556
+ if (sessionExists && !needsCreate) {
557
+ try {
558
+ const status = manager.getStatus(sessionName);
559
+ if (status.stats.contextPercent > OPENAI_COMPAT_AUTO_COMPACT_THRESHOLD) {
560
+ await manager.compactSession(sessionName);
561
+ }
562
+ }
563
+ catch {
564
+ /* best effort — session may not support compact */
565
+ }
566
+ }
567
+ // For non-claude engines (Cursor, Codex, Gemini), their CLIs don't support
568
+ // --append-system-prompt. Prepend the upstream system prompt to the user
569
+ // message on EVERY turn so the model sees the caller's identity, tool
570
+ // definitions, and workspace context. This is done here (not at session
571
+ // creation) because these engines spawn a fresh CLI process per turn —
572
+ // there's no persistent session to carry the system prompt forward.
573
+ let userMessage = extracted.userMessage;
574
+ if (extracted.systemPrompt && engine !== 'claude') {
575
+ userMessage = `<system>\n${extracted.systemPrompt}\n</system>\n\n${userMessage}`;
576
+ }
577
+ // Phase 2 R4 wire-up: prefer structured content blocks when populated
578
+ // (tool-stream mode + tool-role-last). vendor/persistent-session.js
579
+ // forwards arrays through to Claude CLI's stream-json input as native
580
+ // user-message content. Falls back to the legacy XML string for any
581
+ // request that doesn't carry blocks.
582
+ const sendInput = extracted.userMessageBlocks ?? userMessage;
583
+ // Inject tool definitions into the user message.
584
+ //
585
+ // Default path for Claude Code: tools are already embedded in the session
586
+ // system prompt (see session create block above) — do NOT re-inject them
587
+ // per turn. Repeatedly prepending a large <available_tools> block to every
588
+ // user message bloats each turn's input, defeats Anthropic prompt caching,
589
+ // and was the cause of periodic 30-50s latency spikes.
590
+ //
591
+ // Opt-out path for Claude Code (OPENAI_COMPAT_TOOLS_PER_MESSAGE=1): fall
592
+ // back to the legacy behavior of injecting the tool block into each user
593
+ // message. Enables dynamic tool list updates within a single session.
594
+ //
595
+ // Non-claude engines: the CLI is spawned fresh per turn with no persistent
596
+ // system prompt, so tools must always be injected per message.
597
+ const hasTools = !!request.tools?.length;
598
+ const injectToolsPerTurn = hasTools && (engine !== 'claude' || isToolsPerMessageModeEnabled());
599
+ if (injectToolsPerTurn) {
600
+ const toolBlock = buildToolPromptBlock(request.tools);
601
+ userMessage = `${toolBlock}\n\n${userMessage}`;
602
+ }
603
+ const completionId = `chatcmpl-${randomUUID().replace(/-/g, '').slice(0, 29)}`;
604
+ if (isStreaming) {
605
+ await handleStreaming(manager, sessionName, resolvedModel, sendInput, completionId, res, hasTools);
606
+ }
607
+ else {
608
+ await handleNonStreaming(manager, sessionName, resolvedModel, sendInput, completionId, res, hasTools);
609
+ }
610
+ // Clean up ephemeral sessions immediately after response.
611
+ // When X-Session-Reset is set, each request creates a fresh session that
612
+ // should not persist — leaving it alive leaks CLI subprocesses until TTL.
613
+ if (extracted.isNewConversation) {
614
+ manager.stopSession(sessionName).catch(() => { });
615
+ }
616
+ }
617
+ // ─── Status Reporting ───────────────────────────────────────────────────────
618
+ // Push tool/thinking status to an external webhook so a webchat status bar
619
+ // can show what the CLI agent is doing. Best-effort fire-and-forget.
620
+ /**
621
+ * Optional status webhook — set `OPENAI_COMPAT_STATUS_URL` to an HTTP endpoint
622
+ * that accepts `POST { state, activity, tool }`. The bridge will fire-and-forget
623
+ * status updates when the CLI agent uses tools, so an external dashboard (e.g.
624
+ * a webchat status bar) can show real-time progress.
625
+ *
626
+ * Example: `OPENAI_COMPAT_STATUS_URL=http://127.0.0.1:18795/my-app/agent-status`
627
+ */
628
+ function reportStatus(state, activity, tool) {
629
+ const url = getOpenaiCompatStatusUrl();
630
+ if (!url)
631
+ return;
632
+ const payload = JSON.stringify({ state, activity, tool: tool || null });
633
+ const req = http.request(url, {
634
+ method: 'POST',
635
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
636
+ timeout: 2000,
637
+ }, () => { });
638
+ req.on('error', () => { });
639
+ req.write(payload);
640
+ req.end();
641
+ }
642
+ function getToolDescription(toolName, toolInput) {
643
+ switch (toolName) {
644
+ case 'Bash':
645
+ case 'exec': {
646
+ const cmd = String(toolInput?.command || '');
647
+ return `Running: ${cmd.length > 50 ? cmd.slice(0, 50) + '...' : cmd}`;
648
+ }
649
+ case 'Read':
650
+ case 'read':
651
+ return `Reading: ${String(toolInput?.file_path || toolInput?.path || 'file')
652
+ .split('/')
653
+ .pop()}`;
654
+ case 'Write':
655
+ case 'write':
656
+ return `Writing: ${String(toolInput?.file_path || toolInput?.path || 'file')
657
+ .split('/')
658
+ .pop()}`;
659
+ case 'Edit':
660
+ case 'edit':
661
+ return `Editing: ${String(toolInput?.file_path || toolInput?.path || 'file')
662
+ .split('/')
663
+ .pop()}`;
664
+ case 'Glob':
665
+ case 'glob':
666
+ return `Searching files: ${String(toolInput?.pattern || '')}`;
667
+ case 'Grep':
668
+ case 'grep':
669
+ return `Searching content: ${String(toolInput?.pattern || '')}`;
670
+ case 'WebSearch':
671
+ return `Web search: ${String(toolInput?.query || '')}`;
672
+ case 'Agent':
673
+ return `Spawning sub-agent...`;
674
+ default:
675
+ return `Using tool: ${toolName}`;
676
+ }
677
+ }
678
+ // ─── Non-Streaming ───────────────────────────────────────────────────────────
679
+ async function handleNonStreaming(manager, sessionName, model,
680
+ // Phase 2 R4 wire-up: accepts native content-block arrays in tool-stream mode.
681
+ userMessage, completionId, res, hasTools) {
682
+ try {
683
+ reportStatus('thinking', 'Processing request...');
684
+ const result = await manager.sendMessage(sessionName, userMessage, {
685
+ onEvent: (event) => {
686
+ if (event.type === 'tool_use' && event.tool?.name) {
687
+ const desc = getToolDescription(event.tool.name, event.tool.input);
688
+ reportStatus('working', desc, event.tool.name);
689
+ }
690
+ },
691
+ });
692
+ reportStatus('idle', 'Ready');
693
+ let tokensIn = 0;
694
+ let tokensOut = 0;
695
+ try {
696
+ const status = manager.getStatus(sessionName);
697
+ tokensIn = status.stats.tokensIn;
698
+ tokensOut = status.stats.tokensOut;
699
+ }
700
+ catch {
701
+ /* stats unavailable */
702
+ }
703
+ // Parse tool_calls from response text when caller provided tools
704
+ if (hasTools) {
705
+ const parsed = parseToolCallsFromText(result.output);
706
+ const response = formatCompletionResponse(completionId, model, parsed.textContent ?? '', tokensIn, tokensOut, parsed.toolCalls.length > 0 ? parsed.toolCalls : undefined);
707
+ res.writeHead(200, { 'Content-Type': 'application/json' });
708
+ res.end(JSON.stringify(response));
709
+ }
710
+ else {
711
+ const response = formatCompletionResponse(completionId, model, result.output, tokensIn, tokensOut);
712
+ res.writeHead(200, { 'Content-Type': 'application/json' });
713
+ res.end(JSON.stringify(response));
714
+ }
715
+ }
716
+ catch (err) {
717
+ reportStatus('idle', 'Request failed');
718
+ res.writeHead(500, { 'Content-Type': 'application/json' });
719
+ res.end(JSON.stringify({ error: { message: err.message, type: 'server_error' } }));
720
+ }
721
+ }
722
+ // ─── Streaming ───────────────────────────────────────────────────────────────
723
+ async function handleStreaming(manager, sessionName, model,
724
+ // Phase 2 R4 wire-up: accepts native content-block arrays in tool-stream mode.
725
+ userMessage, completionId, res, hasTools) {
726
+ res.writeHead(200, {
727
+ 'Content-Type': 'text/event-stream',
728
+ 'Cache-Control': 'no-cache',
729
+ Connection: 'keep-alive',
730
+ 'X-Accel-Buffering': 'no',
731
+ });
732
+ let clientDisconnected = false;
733
+ res.on('close', () => {
734
+ clientDisconnected = true;
735
+ });
736
+ const writeSSE = (data) => {
737
+ if (!clientDisconnected) {
738
+ try {
739
+ res.write(`data: ${data}\n\n`);
740
+ }
741
+ catch {
742
+ clientDisconnected = true;
743
+ }
744
+ }
745
+ };
746
+ // Initial chunk with role
747
+ writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { role: 'assistant' }, null)));
748
+ // SSE keepalive heartbeat
749
+ const heartbeatTimer = setInterval(() => {
750
+ if (!clientDisconnected) {
751
+ try {
752
+ res.write(': keepalive\n\n');
753
+ }
754
+ catch {
755
+ clientDisconnected = true;
756
+ }
757
+ }
758
+ }, 30_000);
759
+ // Phase 2 R1+R2: in tool-stream mode, bridge session-manager's pre-parsed
760
+ // tool_use events directly to OpenAI tool_calls SSE deltas. Skips the
761
+ // legacy "buffer text + regex-parse <tool_calls> XML" path entirely.
762
+ // Per memory project_cc_openclaw_session_manager_preparses.md:
763
+ // session-manager has already stripped Claude CLI's NDJSON envelope, so
764
+ // we don't need cli-stream-parser here — onEvent is the parser output.
765
+ const useToolStream = isToolStreamMode() && hasTools;
766
+ // When tools are present (legacy mode), buffer the full response to parse
767
+ // for <tool_calls> XML. Without tools — or in tool-stream mode — stream
768
+ // text chunks directly for low latency.
769
+ let bufferedText = '';
770
+ let toolCallsEmitted = 0;
771
+ try {
772
+ reportStatus('thinking', 'Processing request...');
773
+ await manager.sendMessage(sessionName, userMessage, {
774
+ onChunk: (chunk) => {
775
+ if (useToolStream || !hasTools) {
776
+ // Stream text deltas immediately. Tool-stream mode interleaves
777
+ // text and tool_calls chunks naturally — Claude CLI emits text
778
+ // between tool_use blocks, OpenClaw client handles that fine.
779
+ writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: chunk }, null)));
780
+ }
781
+ else {
782
+ // Legacy hasTools mode: buffer for XML <tool_calls> parsing post-stream.
783
+ bufferedText += chunk;
784
+ }
785
+ },
786
+ onEvent: (event) => {
787
+ if (event.type === 'tool_use' && event.tool?.name) {
788
+ reportStatus('working', getToolDescription(event.tool.name, event.tool.input), event.tool.name);
789
+ if (useToolStream) {
790
+ // R1+R2 bridge: session-manager event → OpenAI tool_calls SSE.
791
+ // Emit two chunks per tool_use (per OpenAI streaming spec):
792
+ // 1. id + name + empty arguments
793
+ // 2. arguments (JSON-stringified input)
794
+ const toolUseId = event.tool.id ||
795
+ `toolu_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
796
+ const idx = toolCallsEmitted;
797
+ const argsJson = event.tool.input != null ? JSON.stringify(event.tool.input) : '{}';
798
+ const startChunk = {
799
+ id: completionId,
800
+ object: 'chat.completion.chunk',
801
+ created: Math.floor(Date.now() / 1000),
802
+ model,
803
+ choices: [
804
+ {
805
+ index: 0,
806
+ delta: {
807
+ tool_calls: [
808
+ {
809
+ index: idx,
810
+ id: toolUseId,
811
+ type: 'function',
812
+ function: { name: event.tool.name, arguments: '' },
813
+ },
814
+ ],
815
+ },
816
+ finish_reason: null,
817
+ },
818
+ ],
819
+ };
820
+ const argsChunk = {
821
+ id: completionId,
822
+ object: 'chat.completion.chunk',
823
+ created: Math.floor(Date.now() / 1000),
824
+ model,
825
+ choices: [
826
+ {
827
+ index: 0,
828
+ delta: {
829
+ tool_calls: [
830
+ {
831
+ index: idx,
832
+ function: { arguments: argsJson },
833
+ },
834
+ ],
835
+ },
836
+ finish_reason: null,
837
+ },
838
+ ],
839
+ };
840
+ writeSSE(JSON.stringify(startChunk));
841
+ writeSSE(JSON.stringify(argsChunk));
842
+ toolCallsEmitted += 1;
843
+ }
844
+ }
845
+ },
846
+ });
847
+ reportStatus('idle', 'Ready');
848
+ // Get token usage for final chunk
849
+ let usage;
850
+ try {
851
+ const status = manager.getStatus(sessionName);
852
+ usage = {
853
+ prompt_tokens: status.stats.tokensIn,
854
+ completion_tokens: status.stats.tokensOut,
855
+ total_tokens: status.stats.tokensIn + status.stats.tokensOut,
856
+ };
857
+ }
858
+ catch {
859
+ /* best effort */
860
+ }
861
+ if (useToolStream) {
862
+ // R1+R2: tool-stream mode — text + tool_calls already streamed inline.
863
+ // Just emit the final chunk with the right finish_reason.
864
+ const finishReason = toolCallsEmitted > 0 ? 'tool_calls' : 'stop';
865
+ const finalChunk = formatCompletionChunk(completionId, model, {}, finishReason);
866
+ if (usage)
867
+ finalChunk.usage = usage;
868
+ writeSSE(JSON.stringify(finalChunk));
869
+ }
870
+ else if (hasTools && bufferedText) {
871
+ const parsed = parseToolCallsFromText(bufferedText);
872
+ if (parsed.toolCalls.length > 0) {
873
+ // Emit text content if any
874
+ if (parsed.textContent) {
875
+ writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: parsed.textContent }, null)));
876
+ }
877
+ // Emit tool_call chunks
878
+ for (let i = 0; i < parsed.toolCalls.length; i++) {
879
+ const tc = parsed.toolCalls[i];
880
+ writeSSE(JSON.stringify({
881
+ id: completionId,
882
+ object: 'chat.completion.chunk',
883
+ created: Math.floor(Date.now() / 1000),
884
+ model,
885
+ choices: [
886
+ {
887
+ index: 0,
888
+ delta: {
889
+ tool_calls: [
890
+ {
891
+ index: i,
892
+ id: tc.id,
893
+ type: 'function',
894
+ function: { name: tc.function.name, arguments: tc.function.arguments },
895
+ },
896
+ ],
897
+ },
898
+ finish_reason: null,
899
+ },
900
+ ],
901
+ }));
902
+ }
903
+ // Final chunk with tool_calls finish reason
904
+ const finalChunk = formatCompletionChunk(completionId, model, {}, 'tool_calls');
905
+ if (usage)
906
+ finalChunk.usage = usage;
907
+ writeSSE(JSON.stringify(finalChunk));
908
+ }
909
+ else {
910
+ // No tool calls — emit buffered text as content
911
+ writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: bufferedText }, null)));
912
+ const finalChunk = formatCompletionChunk(completionId, model, {}, 'stop');
913
+ if (usage)
914
+ finalChunk.usage = usage;
915
+ writeSSE(JSON.stringify(finalChunk));
916
+ }
917
+ }
918
+ else {
919
+ // No tools — standard finish
920
+ const finalChunk = formatCompletionChunk(completionId, model, {}, 'stop');
921
+ if (usage)
922
+ finalChunk.usage = usage;
923
+ writeSSE(JSON.stringify(finalChunk));
924
+ }
925
+ writeSSE('[DONE]');
926
+ }
927
+ catch (err) {
928
+ reportStatus('idle', 'Request failed');
929
+ writeSSE(JSON.stringify({ error: { message: err.message, type: 'server_error' } }));
930
+ writeSSE('[DONE]');
931
+ }
932
+ finally {
933
+ clearInterval(heartbeatTimer);
934
+ }
935
+ if (!clientDisconnected) {
936
+ res.end();
937
+ }
938
+ }
939
+ //# sourceMappingURL=openai-compat.js.map