@strands-agents/sdk 0.0.1

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 (423) hide show
  1. package/LICENSE +175 -0
  2. package/README.md +185 -0
  3. package/dist/__fixtures__/model-test-helpers.d.ts +56 -0
  4. package/dist/__fixtures__/model-test-helpers.d.ts.map +1 -0
  5. package/dist/__fixtures__/model-test-helpers.js +85 -0
  6. package/dist/__fixtures__/model-test-helpers.js.map +1 -0
  7. package/dist/__fixtures__/tool-helpers.d.ts +15 -0
  8. package/dist/__fixtures__/tool-helpers.d.ts.map +1 -0
  9. package/dist/__fixtures__/tool-helpers.js +22 -0
  10. package/dist/__fixtures__/tool-helpers.js.map +1 -0
  11. package/dist/__tests__/errors.test.d.ts +2 -0
  12. package/dist/__tests__/errors.test.d.ts.map +1 -0
  13. package/dist/__tests__/errors.test.js +20 -0
  14. package/dist/__tests__/errors.test.js.map +1 -0
  15. package/dist/__tests__/index.test.d.ts +2 -0
  16. package/dist/__tests__/index.test.d.ts.map +1 -0
  17. package/dist/__tests__/index.test.js +27 -0
  18. package/dist/__tests__/index.test.js.map +1 -0
  19. package/dist/errors.d.ts +22 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +25 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index.d.ts +19 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +17 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/models/__tests__/bedrock.test.d.ts +2 -0
  28. package/dist/models/__tests__/bedrock.test.d.ts.map +1 -0
  29. package/dist/models/__tests__/bedrock.test.js +1161 -0
  30. package/dist/models/__tests__/bedrock.test.js.map +1 -0
  31. package/dist/models/__tests__/model.test.d.ts +2 -0
  32. package/dist/models/__tests__/model.test.d.ts.map +1 -0
  33. package/dist/models/__tests__/model.test.js +297 -0
  34. package/dist/models/__tests__/model.test.js.map +1 -0
  35. package/dist/models/__tests__/openai.test.d.ts +2 -0
  36. package/dist/models/__tests__/openai.test.d.ts.map +1 -0
  37. package/dist/models/__tests__/openai.test.js +1016 -0
  38. package/dist/models/__tests__/openai.test.js.map +1 -0
  39. package/dist/models/__tests__/test-utils.d.ts +10 -0
  40. package/dist/models/__tests__/test-utils.d.ts.map +1 -0
  41. package/dist/models/__tests__/test-utils.js +17 -0
  42. package/dist/models/__tests__/test-utils.js.map +1 -0
  43. package/dist/models/bedrock.d.ts +272 -0
  44. package/dist/models/bedrock.d.ts.map +1 -0
  45. package/dist/models/bedrock.js +679 -0
  46. package/dist/models/bedrock.js.map +1 -0
  47. package/dist/models/model.d.ts +89 -0
  48. package/dist/models/model.d.ts.map +1 -0
  49. package/dist/models/model.js +122 -0
  50. package/dist/models/model.js.map +1 -0
  51. package/dist/models/openai.d.ts +262 -0
  52. package/dist/models/openai.d.ts.map +1 -0
  53. package/dist/models/openai.js +625 -0
  54. package/dist/models/openai.js.map +1 -0
  55. package/dist/models/streaming.d.ts +226 -0
  56. package/dist/models/streaming.d.ts.map +1 -0
  57. package/dist/models/streaming.js +2 -0
  58. package/dist/models/streaming.js.map +1 -0
  59. package/dist/src/__fixtures__/agent-helpers.d.ts +29 -0
  60. package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -0
  61. package/dist/src/__fixtures__/agent-helpers.js +19 -0
  62. package/dist/src/__fixtures__/agent-helpers.js.map +1 -0
  63. package/dist/src/__fixtures__/environment.d.ts +12 -0
  64. package/dist/src/__fixtures__/environment.d.ts.map +1 -0
  65. package/dist/src/__fixtures__/environment.js +12 -0
  66. package/dist/src/__fixtures__/environment.js.map +1 -0
  67. package/dist/src/__fixtures__/mock-hook-provider.d.ts +14 -0
  68. package/dist/src/__fixtures__/mock-hook-provider.d.ts.map +1 -0
  69. package/dist/src/__fixtures__/mock-hook-provider.js +33 -0
  70. package/dist/src/__fixtures__/mock-hook-provider.js.map +1 -0
  71. package/dist/src/__fixtures__/mock-message-model.d.ts +93 -0
  72. package/dist/src/__fixtures__/mock-message-model.d.ts.map +1 -0
  73. package/dist/src/__fixtures__/mock-message-model.js +226 -0
  74. package/dist/src/__fixtures__/mock-message-model.js.map +1 -0
  75. package/dist/src/__fixtures__/model-test-helpers.d.ts +56 -0
  76. package/dist/src/__fixtures__/model-test-helpers.d.ts.map +1 -0
  77. package/dist/src/__fixtures__/model-test-helpers.js +85 -0
  78. package/dist/src/__fixtures__/model-test-helpers.js.map +1 -0
  79. package/dist/src/__fixtures__/tool-helpers.d.ts +37 -0
  80. package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -0
  81. package/dist/src/__fixtures__/tool-helpers.js +78 -0
  82. package/dist/src/__fixtures__/tool-helpers.js.map +1 -0
  83. package/dist/src/__tests__/errors.test.d.ts +2 -0
  84. package/dist/src/__tests__/errors.test.d.ts.map +1 -0
  85. package/dist/src/__tests__/errors.test.js +64 -0
  86. package/dist/src/__tests__/errors.test.js.map +1 -0
  87. package/dist/src/__tests__/index.test.d.ts +2 -0
  88. package/dist/src/__tests__/index.test.d.ts.map +1 -0
  89. package/dist/src/__tests__/index.test.js +27 -0
  90. package/dist/src/__tests__/index.test.js.map +1 -0
  91. package/dist/src/__tests__/mcp.test.d.ts +2 -0
  92. package/dist/src/__tests__/mcp.test.d.ts.map +1 -0
  93. package/dist/src/__tests__/mcp.test.js +166 -0
  94. package/dist/src/__tests__/mcp.test.js.map +1 -0
  95. package/dist/src/agent/__tests__/agent.hook.test.d.ts +2 -0
  96. package/dist/src/agent/__tests__/agent.hook.test.d.ts.map +1 -0
  97. package/dist/src/agent/__tests__/agent.hook.test.js +250 -0
  98. package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -0
  99. package/dist/src/agent/__tests__/agent.test.d.ts +2 -0
  100. package/dist/src/agent/__tests__/agent.test.d.ts.map +1 -0
  101. package/dist/src/agent/__tests__/agent.test.js +414 -0
  102. package/dist/src/agent/__tests__/agent.test.js.map +1 -0
  103. package/dist/src/agent/__tests__/printer.test.d.ts +2 -0
  104. package/dist/src/agent/__tests__/printer.test.d.ts.map +1 -0
  105. package/dist/src/agent/__tests__/printer.test.js +152 -0
  106. package/dist/src/agent/__tests__/printer.test.js.map +1 -0
  107. package/dist/src/agent/__tests__/state.test.d.ts +2 -0
  108. package/dist/src/agent/__tests__/state.test.d.ts.map +1 -0
  109. package/dist/src/agent/__tests__/state.test.js +231 -0
  110. package/dist/src/agent/__tests__/state.test.js.map +1 -0
  111. package/dist/src/agent/agent.d.ts +207 -0
  112. package/dist/src/agent/agent.d.ts.map +1 -0
  113. package/dist/src/agent/agent.js +481 -0
  114. package/dist/src/agent/agent.js.map +1 -0
  115. package/dist/src/agent/printer.d.ts +73 -0
  116. package/dist/src/agent/printer.d.ts.map +1 -0
  117. package/dist/src/agent/printer.js +145 -0
  118. package/dist/src/agent/printer.js.map +1 -0
  119. package/dist/src/agent/state.d.ts +102 -0
  120. package/dist/src/agent/state.d.ts.map +1 -0
  121. package/dist/src/agent/state.js +73 -0
  122. package/dist/src/agent/state.js.map +1 -0
  123. package/dist/src/agent/streaming.d.ts +91 -0
  124. package/dist/src/agent/streaming.d.ts.map +1 -0
  125. package/dist/src/agent/streaming.js +2 -0
  126. package/dist/src/agent/streaming.js.map +1 -0
  127. package/dist/src/conversation-manager/__tests__/conversation-manager.test.d.ts +2 -0
  128. package/dist/src/conversation-manager/__tests__/conversation-manager.test.d.ts.map +1 -0
  129. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js +10 -0
  130. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js.map +1 -0
  131. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.d.ts +2 -0
  132. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.d.ts.map +1 -0
  133. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js +35 -0
  134. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js.map +1 -0
  135. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.d.ts +2 -0
  136. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.d.ts.map +1 -0
  137. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +553 -0
  138. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js.map +1 -0
  139. package/dist/src/conversation-manager/conversation-manager.d.ts +73 -0
  140. package/dist/src/conversation-manager/conversation-manager.d.ts.map +1 -0
  141. package/dist/src/conversation-manager/conversation-manager.js +24 -0
  142. package/dist/src/conversation-manager/conversation-manager.js.map +1 -0
  143. package/dist/src/conversation-manager/index.d.ts +8 -0
  144. package/dist/src/conversation-manager/index.d.ts.map +1 -0
  145. package/dist/src/conversation-manager/index.js +8 -0
  146. package/dist/src/conversation-manager/index.js.map +1 -0
  147. package/dist/src/conversation-manager/null-conversation-manager.d.ts +23 -0
  148. package/dist/src/conversation-manager/null-conversation-manager.d.ts.map +1 -0
  149. package/dist/src/conversation-manager/null-conversation-manager.js +23 -0
  150. package/dist/src/conversation-manager/null-conversation-manager.js.map +1 -0
  151. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +105 -0
  152. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts.map +1 -0
  153. package/dist/src/conversation-manager/sliding-window-conversation-manager.js +212 -0
  154. package/dist/src/conversation-manager/sliding-window-conversation-manager.js.map +1 -0
  155. package/dist/src/errors.d.ts +83 -0
  156. package/dist/src/errors.d.ts.map +1 -0
  157. package/dist/src/errors.js +97 -0
  158. package/dist/src/errors.js.map +1 -0
  159. package/dist/src/hooks/__tests__/events.test.d.ts +2 -0
  160. package/dist/src/hooks/__tests__/events.test.d.ts.map +1 -0
  161. package/dist/src/hooks/__tests__/events.test.js +347 -0
  162. package/dist/src/hooks/__tests__/events.test.js.map +1 -0
  163. package/dist/src/hooks/__tests__/registry.test.d.ts +2 -0
  164. package/dist/src/hooks/__tests__/registry.test.d.ts.map +1 -0
  165. package/dist/src/hooks/__tests__/registry.test.js +154 -0
  166. package/dist/src/hooks/__tests__/registry.test.js.map +1 -0
  167. package/dist/src/hooks/events.d.ts +199 -0
  168. package/dist/src/hooks/events.d.ts.map +1 -0
  169. package/dist/src/hooks/events.js +191 -0
  170. package/dist/src/hooks/events.js.map +1 -0
  171. package/dist/src/hooks/index.d.ts +11 -0
  172. package/dist/src/hooks/index.d.ts.map +1 -0
  173. package/dist/src/hooks/index.js +11 -0
  174. package/dist/src/hooks/index.js.map +1 -0
  175. package/dist/src/hooks/registry.d.ts +65 -0
  176. package/dist/src/hooks/registry.d.ts.map +1 -0
  177. package/dist/src/hooks/registry.js +65 -0
  178. package/dist/src/hooks/registry.js.map +1 -0
  179. package/dist/src/hooks/types.d.ts +49 -0
  180. package/dist/src/hooks/types.d.ts.map +1 -0
  181. package/dist/src/hooks/types.js +2 -0
  182. package/dist/src/hooks/types.js.map +1 -0
  183. package/dist/src/index.d.ts +32 -0
  184. package/dist/src/index.d.ts.map +1 -0
  185. package/dist/src/index.js +29 -0
  186. package/dist/src/index.js.map +1 -0
  187. package/dist/src/mcp.d.ts +51 -0
  188. package/dist/src/mcp.d.ts.map +1 -0
  189. package/dist/src/mcp.js +91 -0
  190. package/dist/src/mcp.js.map +1 -0
  191. package/dist/src/models/__tests__/bedrock.test.d.ts +2 -0
  192. package/dist/src/models/__tests__/bedrock.test.d.ts.map +1 -0
  193. package/dist/src/models/__tests__/bedrock.test.js +1388 -0
  194. package/dist/src/models/__tests__/bedrock.test.js.map +1 -0
  195. package/dist/src/models/__tests__/model.test.d.ts +2 -0
  196. package/dist/src/models/__tests__/model.test.d.ts.map +1 -0
  197. package/dist/src/models/__tests__/model.test.js +342 -0
  198. package/dist/src/models/__tests__/model.test.js.map +1 -0
  199. package/dist/src/models/__tests__/openai.test.d.ts +2 -0
  200. package/dist/src/models/__tests__/openai.test.d.ts.map +1 -0
  201. package/dist/src/models/__tests__/openai.test.js +1189 -0
  202. package/dist/src/models/__tests__/openai.test.js.map +1 -0
  203. package/dist/src/models/__tests__/test-utils.d.ts +10 -0
  204. package/dist/src/models/__tests__/test-utils.d.ts.map +1 -0
  205. package/dist/src/models/__tests__/test-utils.js +17 -0
  206. package/dist/src/models/__tests__/test-utils.js.map +1 -0
  207. package/dist/src/models/bedrock.d.ts +289 -0
  208. package/dist/src/models/bedrock.d.ts.map +1 -0
  209. package/dist/src/models/bedrock.js +804 -0
  210. package/dist/src/models/bedrock.js.map +1 -0
  211. package/dist/src/models/model.d.ts +99 -0
  212. package/dist/src/models/model.d.ts.map +1 -0
  213. package/dist/src/models/model.js +169 -0
  214. package/dist/src/models/model.js.map +1 -0
  215. package/dist/src/models/openai.d.ts +262 -0
  216. package/dist/src/models/openai.d.ts.map +1 -0
  217. package/dist/src/models/openai.js +752 -0
  218. package/dist/src/models/openai.js.map +1 -0
  219. package/dist/src/models/streaming.d.ts +318 -0
  220. package/dist/src/models/streaming.d.ts.map +1 -0
  221. package/dist/src/models/streaming.js +122 -0
  222. package/dist/src/models/streaming.js.map +1 -0
  223. package/dist/src/registry/registry.d.ts +117 -0
  224. package/dist/src/registry/registry.d.ts.map +1 -0
  225. package/dist/src/registry/registry.js +298 -0
  226. package/dist/src/registry/registry.js.map +1 -0
  227. package/dist/src/registry/tool-registry.d.ts +34 -0
  228. package/dist/src/registry/tool-registry.d.ts.map +1 -0
  229. package/dist/src/registry/tool-registry.js +178 -0
  230. package/dist/src/registry/tool-registry.js.map +1 -0
  231. package/dist/src/tools/__tests__/tool.test.d.ts +2 -0
  232. package/dist/src/tools/__tests__/tool.test.d.ts.map +1 -0
  233. package/dist/src/tools/__tests__/tool.test.js +877 -0
  234. package/dist/src/tools/__tests__/tool.test.js.map +1 -0
  235. package/dist/src/tools/__tests__/zod-tool.test-d.d.ts +2 -0
  236. package/dist/src/tools/__tests__/zod-tool.test-d.d.ts.map +1 -0
  237. package/dist/src/tools/__tests__/zod-tool.test-d.js +227 -0
  238. package/dist/src/tools/__tests__/zod-tool.test-d.js.map +1 -0
  239. package/dist/src/tools/__tests__/zod-tool.test.d.ts +2 -0
  240. package/dist/src/tools/__tests__/zod-tool.test.d.ts.map +1 -0
  241. package/dist/src/tools/__tests__/zod-tool.test.js +372 -0
  242. package/dist/src/tools/__tests__/zod-tool.test.js.map +1 -0
  243. package/dist/src/tools/function-tool.d.ts +146 -0
  244. package/dist/src/tools/function-tool.d.ts.map +1 -0
  245. package/dist/src/tools/function-tool.js +188 -0
  246. package/dist/src/tools/function-tool.js.map +1 -0
  247. package/dist/src/tools/mcp-tool.d.ts +36 -0
  248. package/dist/src/tools/mcp-tool.d.ts.map +1 -0
  249. package/dist/src/tools/mcp-tool.js +78 -0
  250. package/dist/src/tools/mcp-tool.js.map +1 -0
  251. package/dist/src/tools/tool.d.ts +167 -0
  252. package/dist/src/tools/tool.d.ts.map +1 -0
  253. package/dist/src/tools/tool.js +68 -0
  254. package/dist/src/tools/tool.js.map +1 -0
  255. package/dist/src/tools/types.d.ts +62 -0
  256. package/dist/src/tools/types.d.ts.map +1 -0
  257. package/dist/src/tools/types.js +2 -0
  258. package/dist/src/tools/types.js.map +1 -0
  259. package/dist/src/tools/zod-tool.d.ts +70 -0
  260. package/dist/src/tools/zod-tool.d.ts.map +1 -0
  261. package/dist/src/tools/zod-tool.js +149 -0
  262. package/dist/src/tools/zod-tool.js.map +1 -0
  263. package/dist/src/types/__tests__/agent.test.d.ts +2 -0
  264. package/dist/src/types/__tests__/agent.test.d.ts.map +1 -0
  265. package/dist/src/types/__tests__/agent.test.js +155 -0
  266. package/dist/src/types/__tests__/agent.test.js.map +1 -0
  267. package/dist/src/types/__tests__/json.test.d.ts +2 -0
  268. package/dist/src/types/__tests__/json.test.d.ts.map +1 -0
  269. package/dist/src/types/__tests__/json.test.js +298 -0
  270. package/dist/src/types/__tests__/json.test.js.map +1 -0
  271. package/dist/src/types/__tests__/media.test.d.ts +2 -0
  272. package/dist/src/types/__tests__/media.test.d.ts.map +1 -0
  273. package/dist/src/types/__tests__/media.test.js +257 -0
  274. package/dist/src/types/__tests__/media.test.js.map +1 -0
  275. package/dist/src/types/__tests__/messages.test.d.ts +2 -0
  276. package/dist/src/types/__tests__/messages.test.d.ts.map +1 -0
  277. package/dist/src/types/__tests__/messages.test.js +364 -0
  278. package/dist/src/types/__tests__/messages.test.js.map +1 -0
  279. package/dist/src/types/__tests__/validation.test.d.ts +2 -0
  280. package/dist/src/types/__tests__/validation.test.d.ts.map +1 -0
  281. package/dist/src/types/__tests__/validation.test.js +30 -0
  282. package/dist/src/types/__tests__/validation.test.js.map +1 -0
  283. package/dist/src/types/agent.d.ts +57 -0
  284. package/dist/src/types/agent.d.ts.map +1 -0
  285. package/dist/src/types/agent.js +47 -0
  286. package/dist/src/types/agent.js.map +1 -0
  287. package/dist/src/types/json.d.ts +55 -0
  288. package/dist/src/types/json.d.ts.map +1 -0
  289. package/dist/src/types/json.js +72 -0
  290. package/dist/src/types/json.js.map +1 -0
  291. package/dist/src/types/media.d.ts +249 -0
  292. package/dist/src/types/media.d.ts.map +1 -0
  293. package/dist/src/types/media.js +173 -0
  294. package/dist/src/types/media.js.map +1 -0
  295. package/dist/src/types/messages.d.ts +438 -0
  296. package/dist/src/types/messages.d.ts.map +1 -0
  297. package/dist/src/types/messages.js +286 -0
  298. package/dist/src/types/messages.js.map +1 -0
  299. package/dist/src/types/validation.d.ts +10 -0
  300. package/dist/src/types/validation.d.ts.map +1 -0
  301. package/dist/src/types/validation.js +15 -0
  302. package/dist/src/types/validation.js.map +1 -0
  303. package/dist/tools/__tests__/registry.test.d.ts +2 -0
  304. package/dist/tools/__tests__/registry.test.d.ts.map +1 -0
  305. package/dist/tools/__tests__/registry.test.js +253 -0
  306. package/dist/tools/__tests__/registry.test.js.map +1 -0
  307. package/dist/tools/__tests__/tool.test.d.ts +2 -0
  308. package/dist/tools/__tests__/tool.test.d.ts.map +1 -0
  309. package/dist/tools/__tests__/tool.test.js +761 -0
  310. package/dist/tools/__tests__/tool.test.js.map +1 -0
  311. package/dist/tools/__tests__/zod-tool.test-d.d.ts +2 -0
  312. package/dist/tools/__tests__/zod-tool.test-d.d.ts.map +1 -0
  313. package/dist/tools/__tests__/zod-tool.test-d.js +227 -0
  314. package/dist/tools/__tests__/zod-tool.test-d.js.map +1 -0
  315. package/dist/tools/__tests__/zod-tool.test.d.ts +2 -0
  316. package/dist/tools/__tests__/zod-tool.test.d.ts.map +1 -0
  317. package/dist/tools/__tests__/zod-tool.test.js +342 -0
  318. package/dist/tools/__tests__/zod-tool.test.js.map +1 -0
  319. package/dist/tools/function-tool.d.ts +156 -0
  320. package/dist/tools/function-tool.d.ts.map +1 -0
  321. package/dist/tools/function-tool.js +237 -0
  322. package/dist/tools/function-tool.js.map +1 -0
  323. package/dist/tools/registry.d.ts +43 -0
  324. package/dist/tools/registry.d.ts.map +1 -0
  325. package/dist/tools/registry.js +82 -0
  326. package/dist/tools/registry.js.map +1 -0
  327. package/dist/tools/tool.d.ts +157 -0
  328. package/dist/tools/tool.d.ts.map +1 -0
  329. package/dist/tools/tool.js +2 -0
  330. package/dist/tools/tool.js.map +1 -0
  331. package/dist/tools/types.d.ts +119 -0
  332. package/dist/tools/types.d.ts.map +1 -0
  333. package/dist/tools/types.js +2 -0
  334. package/dist/tools/types.js.map +1 -0
  335. package/dist/tools/zod-tool.d.ts +70 -0
  336. package/dist/tools/zod-tool.d.ts.map +1 -0
  337. package/dist/tools/zod-tool.js +96 -0
  338. package/dist/tools/zod-tool.js.map +1 -0
  339. package/dist/types/__tests__/json.test.d.ts +2 -0
  340. package/dist/types/__tests__/json.test.d.ts.map +1 -0
  341. package/dist/types/__tests__/json.test.js +129 -0
  342. package/dist/types/__tests__/json.test.js.map +1 -0
  343. package/dist/types/__tests__/validation.test.d.ts +2 -0
  344. package/dist/types/__tests__/validation.test.d.ts.map +1 -0
  345. package/dist/types/__tests__/validation.test.js +30 -0
  346. package/dist/types/__tests__/validation.test.js.map +1 -0
  347. package/dist/types/json.d.ts +45 -0
  348. package/dist/types/json.d.ts.map +1 -0
  349. package/dist/types/json.js +17 -0
  350. package/dist/types/json.js.map +1 -0
  351. package/dist/types/messages.d.ts +160 -0
  352. package/dist/types/messages.d.ts.map +1 -0
  353. package/dist/types/messages.js +2 -0
  354. package/dist/types/messages.js.map +1 -0
  355. package/dist/types/validation.d.ts +10 -0
  356. package/dist/types/validation.d.ts.map +1 -0
  357. package/dist/types/validation.js +15 -0
  358. package/dist/types/validation.js.map +1 -0
  359. package/dist/vended_tools/bash/__tests__/bash.test.d.ts +2 -0
  360. package/dist/vended_tools/bash/__tests__/bash.test.d.ts.map +1 -0
  361. package/dist/vended_tools/bash/__tests__/bash.test.js +333 -0
  362. package/dist/vended_tools/bash/__tests__/bash.test.js.map +1 -0
  363. package/dist/vended_tools/bash/bash.d.ts +33 -0
  364. package/dist/vended_tools/bash/bash.d.ts.map +1 -0
  365. package/dist/vended_tools/bash/bash.js +264 -0
  366. package/dist/vended_tools/bash/bash.js.map +1 -0
  367. package/dist/vended_tools/bash/index.d.ts +7 -0
  368. package/dist/vended_tools/bash/index.d.ts.map +1 -0
  369. package/dist/vended_tools/bash/index.js +6 -0
  370. package/dist/vended_tools/bash/index.js.map +1 -0
  371. package/dist/vended_tools/bash/types.d.ts +65 -0
  372. package/dist/vended_tools/bash/types.d.ts.map +1 -0
  373. package/dist/vended_tools/bash/types.js +22 -0
  374. package/dist/vended_tools/bash/types.js.map +1 -0
  375. package/dist/vended_tools/file_editor/__tests__/file-editor.test.d.ts +2 -0
  376. package/dist/vended_tools/file_editor/__tests__/file-editor.test.d.ts.map +1 -0
  377. package/dist/vended_tools/file_editor/__tests__/file-editor.test.js +359 -0
  378. package/dist/vended_tools/file_editor/__tests__/file-editor.test.js.map +1 -0
  379. package/dist/vended_tools/file_editor/file-editor.d.ts +31 -0
  380. package/dist/vended_tools/file_editor/file-editor.d.ts.map +1 -0
  381. package/dist/vended_tools/file_editor/file-editor.js +353 -0
  382. package/dist/vended_tools/file_editor/file-editor.js.map +1 -0
  383. package/dist/vended_tools/file_editor/index.d.ts +6 -0
  384. package/dist/vended_tools/file_editor/index.d.ts.map +1 -0
  385. package/dist/vended_tools/file_editor/index.js +5 -0
  386. package/dist/vended_tools/file_editor/index.js.map +1 -0
  387. package/dist/vended_tools/file_editor/types.d.ts +61 -0
  388. package/dist/vended_tools/file_editor/types.d.ts.map +1 -0
  389. package/dist/vended_tools/file_editor/types.js +2 -0
  390. package/dist/vended_tools/file_editor/types.js.map +1 -0
  391. package/dist/vended_tools/http_request/__tests__/http-request.test.d.ts +2 -0
  392. package/dist/vended_tools/http_request/__tests__/http-request.test.d.ts.map +1 -0
  393. package/dist/vended_tools/http_request/__tests__/http-request.test.js +189 -0
  394. package/dist/vended_tools/http_request/__tests__/http-request.test.js.map +1 -0
  395. package/dist/vended_tools/http_request/http-request.d.ts +35 -0
  396. package/dist/vended_tools/http_request/http-request.d.ts.map +1 -0
  397. package/dist/vended_tools/http_request/http-request.js +95 -0
  398. package/dist/vended_tools/http_request/http-request.js.map +1 -0
  399. package/dist/vended_tools/http_request/index.d.ts +6 -0
  400. package/dist/vended_tools/http_request/index.d.ts.map +1 -0
  401. package/dist/vended_tools/http_request/index.js +5 -0
  402. package/dist/vended_tools/http_request/index.js.map +1 -0
  403. package/dist/vended_tools/http_request/types.d.ts +47 -0
  404. package/dist/vended_tools/http_request/types.d.ts.map +1 -0
  405. package/dist/vended_tools/http_request/types.js +2 -0
  406. package/dist/vended_tools/http_request/types.js.map +1 -0
  407. package/dist/vended_tools/notebook/__tests__/notebook.test.d.ts +2 -0
  408. package/dist/vended_tools/notebook/__tests__/notebook.test.d.ts.map +1 -0
  409. package/dist/vended_tools/notebook/__tests__/notebook.test.js +371 -0
  410. package/dist/vended_tools/notebook/__tests__/notebook.test.js.map +1 -0
  411. package/dist/vended_tools/notebook/index.d.ts +6 -0
  412. package/dist/vended_tools/notebook/index.d.ts.map +1 -0
  413. package/dist/vended_tools/notebook/index.js +5 -0
  414. package/dist/vended_tools/notebook/index.js.map +1 -0
  415. package/dist/vended_tools/notebook/notebook.d.ts +29 -0
  416. package/dist/vended_tools/notebook/notebook.d.ts.map +1 -0
  417. package/dist/vended_tools/notebook/notebook.js +215 -0
  418. package/dist/vended_tools/notebook/notebook.js.map +1 -0
  419. package/dist/vended_tools/notebook/types.d.ts +79 -0
  420. package/dist/vended_tools/notebook/types.d.ts.map +1 -0
  421. package/dist/vended_tools/notebook/types.js +2 -0
  422. package/dist/vended_tools/notebook/types.js.map +1 -0
  423. package/package.json +112 -0
@@ -0,0 +1,1189 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import OpenAI from 'openai';
3
+ import { isNode } from '../../__fixtures__/environment.js';
4
+ import { OpenAIModel } from '../openai.js';
5
+ import { ContextWindowOverflowError } from '../../errors.js';
6
+ import { collectIterator } from '../../__fixtures__/model-test-helpers.js';
7
+ /**
8
+ * Helper to create a mock OpenAI client with streaming support
9
+ */
10
+ function createMockClient(streamGenerator) {
11
+ return {
12
+ chat: {
13
+ completions: {
14
+ create: vi.fn(async () => streamGenerator()),
15
+ },
16
+ },
17
+ };
18
+ }
19
+ // Mock the OpenAI SDK
20
+ vi.mock('openai', () => {
21
+ const mockConstructor = vi.fn(function () {
22
+ return {};
23
+ });
24
+ return {
25
+ default: mockConstructor,
26
+ };
27
+ });
28
+ describe('OpenAIModel', () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ vi.restoreAllMocks();
32
+ // Set default env var for most tests using Vitest's stubEnv (Node.js only)
33
+ if (isNode) {
34
+ vi.stubEnv('OPENAI_API_KEY', 'sk-test-env');
35
+ }
36
+ });
37
+ afterEach(() => {
38
+ vi.clearAllMocks();
39
+ // Restore all environment variables to their original state (Node.js only)
40
+ if (isNode) {
41
+ vi.unstubAllEnvs();
42
+ }
43
+ });
44
+ describe('constructor', () => {
45
+ it('creates an instance with required modelId', () => {
46
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', apiKey: 'sk-test' });
47
+ const config = provider.getConfig();
48
+ expect(config.modelId).toBe('gpt-4o');
49
+ });
50
+ it('uses custom model ID', () => {
51
+ const customModelId = 'gpt-3.5-turbo';
52
+ const provider = new OpenAIModel({ modelId: customModelId, apiKey: 'sk-test' });
53
+ expect(provider.getConfig()).toStrictEqual({
54
+ modelId: customModelId,
55
+ });
56
+ });
57
+ it('uses API key from constructor parameter', () => {
58
+ const apiKey = 'sk-explicit';
59
+ new OpenAIModel({ modelId: 'gpt-4o', apiKey });
60
+ expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({
61
+ apiKey: apiKey,
62
+ }));
63
+ });
64
+ // Node.js-specific test: environment variable usage
65
+ if (isNode) {
66
+ it('uses API key from environment variable', () => {
67
+ vi.stubEnv('OPENAI_API_KEY', 'sk-from-env');
68
+ new OpenAIModel({ modelId: 'gpt-4o' });
69
+ // OpenAI client should be called without explicit apiKey (uses env var internally)
70
+ expect(OpenAI).toHaveBeenCalled();
71
+ });
72
+ }
73
+ it('explicit API key takes precedence over environment variable', () => {
74
+ if (isNode) {
75
+ vi.stubEnv('OPENAI_API_KEY', 'sk-from-env');
76
+ }
77
+ const explicitKey = 'sk-explicit';
78
+ new OpenAIModel({ modelId: 'gpt-4o', apiKey: explicitKey });
79
+ expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({
80
+ apiKey: explicitKey,
81
+ }));
82
+ });
83
+ it('throws error when no API key is available', () => {
84
+ if (isNode) {
85
+ vi.stubEnv('OPENAI_API_KEY', '');
86
+ }
87
+ expect(() => new OpenAIModel({ modelId: 'gpt-4o' })).toThrow("OpenAI API key is required. Provide it via the 'apiKey' option or set the OPENAI_API_KEY environment variable.");
88
+ });
89
+ it('uses custom client configuration', () => {
90
+ const timeout = 30000;
91
+ new OpenAIModel({ modelId: 'gpt-4o', apiKey: 'sk-test', clientConfig: { timeout } });
92
+ expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({
93
+ timeout: timeout,
94
+ }));
95
+ });
96
+ it('uses provided client instance', () => {
97
+ vi.clearAllMocks();
98
+ const mockClient = {};
99
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
100
+ // Should not create a new OpenAI client
101
+ expect(OpenAI).not.toHaveBeenCalled();
102
+ expect(provider).toBeDefined();
103
+ });
104
+ it('provided client takes precedence over apiKey and clientConfig', () => {
105
+ vi.clearAllMocks();
106
+ const mockClient = {};
107
+ new OpenAIModel({
108
+ modelId: 'gpt-4o',
109
+ apiKey: 'sk-test',
110
+ client: mockClient,
111
+ clientConfig: { timeout: 30000 },
112
+ });
113
+ // Should not create a new OpenAI client when client is provided
114
+ expect(OpenAI).not.toHaveBeenCalled();
115
+ });
116
+ it('does not require API key when client is provided', () => {
117
+ vi.clearAllMocks();
118
+ if (isNode) {
119
+ vi.stubEnv('OPENAI_API_KEY', '');
120
+ }
121
+ const mockClient = {};
122
+ expect(() => new OpenAIModel({ modelId: 'gpt-4o', client: mockClient })).not.toThrow();
123
+ });
124
+ });
125
+ describe('updateConfig', () => {
126
+ it('merges new config with existing config', () => {
127
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', apiKey: 'sk-test', temperature: 0.5 });
128
+ provider.updateConfig({ modelId: 'gpt-4o', temperature: 0.8, maxTokens: 2048 });
129
+ expect(provider.getConfig()).toStrictEqual({
130
+ modelId: 'gpt-4o',
131
+ temperature: 0.8,
132
+ maxTokens: 2048,
133
+ });
134
+ });
135
+ it('preserves fields not included in the update', () => {
136
+ const provider = new OpenAIModel({
137
+ apiKey: 'sk-test',
138
+ modelId: 'gpt-3.5-turbo',
139
+ temperature: 0.5,
140
+ maxTokens: 1024,
141
+ });
142
+ provider.updateConfig({ modelId: 'gpt-3.5-turbo', temperature: 0.8 });
143
+ expect(provider.getConfig()).toStrictEqual({
144
+ modelId: 'gpt-3.5-turbo',
145
+ temperature: 0.8,
146
+ maxTokens: 1024,
147
+ });
148
+ });
149
+ });
150
+ describe('getConfig', () => {
151
+ it('returns the current configuration', () => {
152
+ const provider = new OpenAIModel({
153
+ modelId: 'gpt-4o',
154
+ apiKey: 'sk-test',
155
+ maxTokens: 1024,
156
+ temperature: 0.7,
157
+ });
158
+ expect(provider.getConfig()).toStrictEqual({
159
+ modelId: 'gpt-4o',
160
+ maxTokens: 1024,
161
+ temperature: 0.7,
162
+ });
163
+ });
164
+ });
165
+ describe('stream', () => {
166
+ describe('validation', () => {
167
+ it('throws error when messages array is empty', async () => {
168
+ const mockClient = createMockClient(async function* () { });
169
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
170
+ await expect(async () => {
171
+ await collectIterator(provider.stream([]));
172
+ }).rejects.toThrow('At least one message is required');
173
+ });
174
+ it('validates system prompt is not empty', async () => {
175
+ const mockClient = createMockClient(async function* () {
176
+ yield {
177
+ choices: [{ delta: { role: 'assistant', content: 'Hello' }, index: 0 }],
178
+ };
179
+ yield {
180
+ choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
181
+ };
182
+ });
183
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
184
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
185
+ // System prompt that's only whitespace should not be sent
186
+ const events = await collectIterator(provider.stream(messages, { systemPrompt: ' ' }));
187
+ // Should still get valid events
188
+ expect(events.length).toBeGreaterThan(0);
189
+ expect(events[0]?.type).toBe('modelMessageStartEvent');
190
+ });
191
+ it('throws error for streaming with n > 1', async () => {
192
+ const mockClient = createMockClient(async function* () { });
193
+ const provider = new OpenAIModel({
194
+ modelId: 'gpt-4o',
195
+ client: mockClient,
196
+ params: { n: 2 },
197
+ });
198
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
199
+ await expect(async () => {
200
+ for await (const _ of provider.stream(messages)) {
201
+ // Should not reach here
202
+ }
203
+ }).rejects.toThrow('Streaming with n > 1 is not supported');
204
+ });
205
+ it('throws error for tool spec without name or description', async () => {
206
+ const mockClient = createMockClient(async function* () { });
207
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
208
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
209
+ await expect(async () => {
210
+ for await (const _ of provider.stream(messages, {
211
+ toolSpecs: [{ name: '', description: 'test', inputSchema: {} }],
212
+ })) {
213
+ // Should not reach here
214
+ }
215
+ }).rejects.toThrow('Tool specification must have both name and description');
216
+ });
217
+ it('throws error for empty tool result content', async () => {
218
+ const mockClient = createMockClient(async function* () { });
219
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
220
+ const messages = [
221
+ {
222
+ type: 'message',
223
+ role: 'user',
224
+ content: [{ type: 'toolResultBlock', toolUseId: 'tool-123', status: 'success', content: [] }],
225
+ },
226
+ ];
227
+ await expect(async () => {
228
+ for await (const _ of provider.stream(messages)) {
229
+ // Should not reach here
230
+ }
231
+ }).rejects.toThrow('Tool result for toolUseId "tool-123" has empty content');
232
+ });
233
+ it('handles tool result with error status', async () => {
234
+ const mockClient = createMockClient(async function* () {
235
+ yield {
236
+ choices: [{ delta: { role: 'assistant', content: 'Ok' }, index: 0 }],
237
+ };
238
+ yield {
239
+ choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
240
+ };
241
+ });
242
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
243
+ const messages = [
244
+ { type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Run tool' }] },
245
+ {
246
+ type: 'message',
247
+ role: 'assistant',
248
+ content: [
249
+ {
250
+ type: 'toolUseBlock',
251
+ name: 'calculator',
252
+ toolUseId: 'tool-123',
253
+ input: { expr: 'invalid' },
254
+ },
255
+ ],
256
+ },
257
+ {
258
+ type: 'message',
259
+ role: 'user',
260
+ content: [
261
+ {
262
+ type: 'toolResultBlock',
263
+ toolUseId: 'tool-123',
264
+ status: 'error',
265
+ content: [{ type: 'textBlock', text: 'Division by zero' }],
266
+ },
267
+ ],
268
+ },
269
+ ];
270
+ // Should not throw - error status is handled by prepending [ERROR]
271
+ const events = await collectIterator(provider.stream(messages));
272
+ // Verify we got a response
273
+ expect(events.length).toBeGreaterThan(0);
274
+ expect(events[0]?.type).toBe('modelMessageStartEvent');
275
+ });
276
+ it('throws error for circular reference in tool input', async () => {
277
+ const mockClient = createMockClient(async function* () { });
278
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
279
+ const circular = { a: 1 };
280
+ circular.self = circular;
281
+ const messages = [
282
+ { type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] },
283
+ {
284
+ type: 'message',
285
+ role: 'assistant',
286
+ content: [
287
+ {
288
+ type: 'toolUseBlock',
289
+ name: 'test',
290
+ toolUseId: 'tool-1',
291
+ input: circular,
292
+ },
293
+ ],
294
+ },
295
+ ];
296
+ await expect(async () => {
297
+ for await (const _ of provider.stream(messages)) {
298
+ // Should not reach here
299
+ }
300
+ }).rejects.toThrow('Failed to serialize tool input');
301
+ });
302
+ });
303
+ describe('basic streaming', () => {
304
+ it('yields correct event sequence for simple text response', async () => {
305
+ const mockClient = createMockClient(async function* () {
306
+ yield {
307
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
308
+ };
309
+ yield {
310
+ choices: [{ delta: { content: 'Hello' }, index: 0 }],
311
+ };
312
+ yield {
313
+ choices: [{ delta: { content: ' world' }, index: 0 }],
314
+ };
315
+ yield {
316
+ choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
317
+ };
318
+ });
319
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
320
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
321
+ const events = await collectIterator(provider.stream(messages));
322
+ // Now includes complete content block lifecycle: start, deltas, stop
323
+ expect(events).toHaveLength(6);
324
+ expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
325
+ expect(events[1]).toEqual({
326
+ type: 'modelContentBlockStartEvent',
327
+ });
328
+ expect(events[2]).toEqual({
329
+ type: 'modelContentBlockDeltaEvent',
330
+ delta: { type: 'textDelta', text: 'Hello' },
331
+ });
332
+ expect(events[3]).toEqual({
333
+ type: 'modelContentBlockDeltaEvent',
334
+ delta: { type: 'textDelta', text: ' world' },
335
+ });
336
+ expect(events[4]).toEqual({
337
+ type: 'modelContentBlockStopEvent',
338
+ });
339
+ expect(events[5]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
340
+ });
341
+ });
342
+ it('emits modelMetadataEvent with usage information', async () => {
343
+ const mockClient = createMockClient(async function* () {
344
+ yield {
345
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
346
+ };
347
+ yield {
348
+ choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
349
+ };
350
+ yield {
351
+ choices: [],
352
+ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
353
+ };
354
+ });
355
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
356
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
357
+ const events = await collectIterator(provider.stream(messages));
358
+ const metadataEvent = events.find((e) => e.type === 'modelMetadataEvent');
359
+ expect(metadataEvent).toBeDefined();
360
+ expect(metadataEvent).toEqual({
361
+ type: 'modelMetadataEvent',
362
+ usage: {
363
+ inputTokens: 10,
364
+ outputTokens: 5,
365
+ totalTokens: 15,
366
+ },
367
+ });
368
+ });
369
+ it('handles usage with undefined properties', async () => {
370
+ const mockClient = createMockClient(async function* () {
371
+ yield {
372
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
373
+ };
374
+ yield {
375
+ choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
376
+ };
377
+ yield {
378
+ choices: [],
379
+ usage: {}, // Empty usage object
380
+ };
381
+ });
382
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
383
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
384
+ const events = await collectIterator(provider.stream(messages));
385
+ const metadataEvent = events.find((e) => e.type === 'modelMetadataEvent');
386
+ expect(metadataEvent).toBeDefined();
387
+ expect(metadataEvent).toEqual({
388
+ type: 'modelMetadataEvent',
389
+ usage: {
390
+ inputTokens: 0,
391
+ outputTokens: 0,
392
+ totalTokens: 0,
393
+ },
394
+ });
395
+ });
396
+ it('filters out empty string content deltas', async () => {
397
+ const mockClient = createMockClient(async function* () {
398
+ yield {
399
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
400
+ };
401
+ yield {
402
+ choices: [{ delta: { content: '' }, index: 0 }], // Empty content
403
+ };
404
+ yield {
405
+ choices: [{ delta: { content: 'Hello' }, index: 0 }],
406
+ };
407
+ yield {
408
+ choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
409
+ };
410
+ });
411
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
412
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
413
+ const events = await collectIterator(provider.stream(messages));
414
+ // Should not emit event for empty content
415
+ const contentEvents = events.filter((e) => e.type === 'modelContentBlockDeltaEvent');
416
+ expect(contentEvents).toHaveLength(1);
417
+ expect(contentEvents[0].delta.text).toBe('Hello');
418
+ });
419
+ it('prevents duplicate message start events', async () => {
420
+ const mockClient = createMockClient(async function* () {
421
+ yield {
422
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
423
+ };
424
+ yield {
425
+ choices: [{ delta: { role: 'assistant', content: 'Hello' }, index: 0 }], // Duplicate role
426
+ };
427
+ yield {
428
+ choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
429
+ };
430
+ });
431
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
432
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
433
+ // Suppress console.warn for this test
434
+ vi.spyOn(console, 'warn').mockImplementation(() => { });
435
+ const events = await collectIterator(provider.stream(messages));
436
+ // Should only have one message start event
437
+ const startEvents = events.filter((e) => e.type === 'modelMessageStartEvent');
438
+ expect(startEvents).toHaveLength(1);
439
+ });
440
+ });
441
+ describe('tool calling', () => {
442
+ it('handles tool use request with contentBlockStart and contentBlockStop events', async () => {
443
+ const mockClient = createMockClient(async function* () {
444
+ yield {
445
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
446
+ };
447
+ yield {
448
+ choices: [
449
+ {
450
+ delta: {
451
+ tool_calls: [
452
+ {
453
+ index: 0,
454
+ id: 'call_123',
455
+ type: 'function',
456
+ function: { name: 'calculator', arguments: '' },
457
+ },
458
+ ],
459
+ },
460
+ index: 0,
461
+ },
462
+ ],
463
+ };
464
+ yield {
465
+ choices: [
466
+ {
467
+ delta: {
468
+ tool_calls: [{ index: 0, function: { arguments: '{"expr' } }],
469
+ },
470
+ index: 0,
471
+ },
472
+ ],
473
+ };
474
+ yield {
475
+ choices: [
476
+ {
477
+ delta: {
478
+ tool_calls: [{ index: 0, function: { arguments: '":"2+2"}' } }],
479
+ },
480
+ index: 0,
481
+ },
482
+ ],
483
+ };
484
+ yield {
485
+ choices: [{ finish_reason: 'tool_calls', delta: {}, index: 0 }],
486
+ };
487
+ });
488
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
489
+ const messages = [
490
+ { type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Calculate 2+2' }] },
491
+ ];
492
+ const events = await collectIterator(provider.stream(messages));
493
+ // Verify key events in sequence
494
+ expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
495
+ expect(events[1]).toEqual({
496
+ type: 'modelContentBlockStartEvent',
497
+ start: {
498
+ type: 'toolUseStart',
499
+ name: 'calculator',
500
+ toolUseId: 'call_123',
501
+ },
502
+ });
503
+ expect(events[2]).toEqual({
504
+ type: 'modelContentBlockDeltaEvent',
505
+ delta: {
506
+ type: 'toolUseInputDelta',
507
+ input: '{"expr',
508
+ },
509
+ });
510
+ expect(events[3]).toEqual({
511
+ type: 'modelContentBlockDeltaEvent',
512
+ delta: {
513
+ type: 'toolUseInputDelta',
514
+ input: '":"2+2"}',
515
+ },
516
+ });
517
+ expect(events[4]).toEqual({
518
+ type: 'modelContentBlockStopEvent',
519
+ });
520
+ expect(events[5]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'toolUse' });
521
+ });
522
+ it('handles multiple tool calls', async () => {
523
+ const mockClient = createMockClient(async function* () {
524
+ yield {
525
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
526
+ };
527
+ yield {
528
+ choices: [
529
+ {
530
+ delta: {
531
+ tool_calls: [
532
+ {
533
+ index: 0,
534
+ id: 'call_1',
535
+ type: 'function',
536
+ function: { name: 'tool1', arguments: '{}' },
537
+ },
538
+ ],
539
+ },
540
+ index: 0,
541
+ },
542
+ ],
543
+ };
544
+ yield {
545
+ choices: [
546
+ {
547
+ delta: {
548
+ tool_calls: [
549
+ {
550
+ index: 1,
551
+ id: 'call_2',
552
+ type: 'function',
553
+ function: { name: 'tool2', arguments: '{}' },
554
+ },
555
+ ],
556
+ },
557
+ index: 0,
558
+ },
559
+ ],
560
+ };
561
+ yield {
562
+ choices: [{ finish_reason: 'tool_calls', delta: {}, index: 0 }],
563
+ };
564
+ });
565
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
566
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
567
+ const events = await collectIterator(provider.stream(messages));
568
+ // Should emit stop events for both tool calls
569
+ const stopEvents = events.filter((e) => e.type === 'modelContentBlockStopEvent');
570
+ expect(stopEvents).toHaveLength(2);
571
+ expect(stopEvents[0]).toEqual({ type: 'modelContentBlockStopEvent' });
572
+ expect(stopEvents[1]).toEqual({ type: 'modelContentBlockStopEvent' });
573
+ });
574
+ it('skips tool calls with invalid index', async () => {
575
+ const mockClient = createMockClient(async function* () {
576
+ yield {
577
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
578
+ };
579
+ yield {
580
+ choices: [
581
+ {
582
+ delta: {
583
+ tool_calls: [
584
+ {
585
+ index: undefined, // Invalid index
586
+ id: 'call_123',
587
+ type: 'function',
588
+ function: { name: 'tool', arguments: '{}' },
589
+ },
590
+ ],
591
+ },
592
+ index: 0,
593
+ },
594
+ ],
595
+ };
596
+ yield {
597
+ choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
598
+ };
599
+ });
600
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
601
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
602
+ // Suppress console.warn for this test
603
+ vi.spyOn(console, 'warn').mockImplementation(() => { });
604
+ const events = await collectIterator(provider.stream(messages));
605
+ // Should not emit any tool-related events
606
+ const toolEvents = events.filter((e) => e.type === 'modelContentBlockStartEvent' || e.type === 'modelContentBlockDeltaEvent');
607
+ expect(toolEvents).toHaveLength(0);
608
+ // The important thing is that invalid tool calls don't crash the stream
609
+ // and are properly skipped
610
+ expect(events.length).toBeGreaterThan(0); // Still got message events
611
+ });
612
+ it('tool argument deltas can be reassembled into valid JSON', async () => {
613
+ const mockClient = createMockClient(async function* () {
614
+ yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
615
+ yield {
616
+ choices: [
617
+ {
618
+ delta: {
619
+ tool_calls: [
620
+ {
621
+ index: 0,
622
+ id: 'call_123',
623
+ type: 'function',
624
+ function: { name: 'calculator', arguments: '' },
625
+ },
626
+ ],
627
+ },
628
+ index: 0,
629
+ },
630
+ ],
631
+ };
632
+ // Split JSON across multiple chunks in realistic ways
633
+ yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '{"' } }] }, index: 0 }] };
634
+ yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: 'x":' } }] }, index: 0 }] };
635
+ yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '10,' } }] }, index: 0 }] };
636
+ yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '"y":' } }] }, index: 0 }] };
637
+ yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '20}' } }] }, index: 0 }] };
638
+ yield { choices: [{ finish_reason: 'tool_calls', delta: {}, index: 0 }] };
639
+ });
640
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
641
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
642
+ const events = await collectIterator(provider.stream(messages));
643
+ // Extract and concatenate all tool input deltas
644
+ const inputDeltas = events
645
+ .filter((e) => e.type === 'modelContentBlockDeltaEvent' && e.delta.type === 'toolUseInputDelta')
646
+ .map((e) => e.delta.input);
647
+ const reassembled = inputDeltas.join('');
648
+ // Should be valid JSON
649
+ expect(() => JSON.parse(reassembled)).not.toThrow();
650
+ expect(JSON.parse(reassembled)).toEqual({ x: 10, y: 20 });
651
+ });
652
+ it('handles messages with both text and tool calls', async () => {
653
+ const mockClient = createMockClient(async function* () {
654
+ yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
655
+ // Text content first
656
+ yield { choices: [{ delta: { content: 'Let me calculate ' }, index: 0 }] };
657
+ yield { choices: [{ delta: { content: 'that for you.' }, index: 0 }] };
658
+ // Then tool call
659
+ yield {
660
+ choices: [
661
+ {
662
+ delta: {
663
+ tool_calls: [
664
+ {
665
+ index: 0,
666
+ id: 'call_123',
667
+ type: 'function',
668
+ function: { name: 'calculator', arguments: '{"expr":"2+2"}' },
669
+ },
670
+ ],
671
+ },
672
+ index: 0,
673
+ },
674
+ ],
675
+ };
676
+ yield { choices: [{ finish_reason: 'tool_calls', delta: {}, index: 0 }] };
677
+ });
678
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
679
+ const messages = [
680
+ { type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Calculate 2+2' }] },
681
+ ];
682
+ const events = await collectIterator(provider.stream(messages));
683
+ // Should have text deltas followed by tool events
684
+ expect(events[0]?.type).toBe('modelMessageStartEvent');
685
+ // Text content block start
686
+ expect(events[1]?.type).toBe('modelContentBlockStartEvent');
687
+ // Text deltas
688
+ expect(events[2]?.type).toBe('modelContentBlockDeltaEvent');
689
+ expect(events[2].delta.type).toBe('textDelta');
690
+ expect(events[2].delta.text).toBe('Let me calculate ');
691
+ // Tool events should follow
692
+ const toolStartEvent = events.find((e) => e.type === 'modelContentBlockStartEvent' && e.start?.type === 'toolUseStart');
693
+ expect(toolStartEvent).toBeDefined();
694
+ // Both text and tool blocks should have stop events
695
+ const stopEvents = events.filter((e) => e.type === 'modelContentBlockStopEvent');
696
+ expect(stopEvents.length).toBeGreaterThan(0);
697
+ });
698
+ });
699
+ describe('stop reasons', () => {
700
+ it('maps OpenAI stop reasons to SDK stop reasons', async () => {
701
+ const stopReasons = [
702
+ { openai: 'stop', sdk: 'endTurn' },
703
+ { openai: 'tool_calls', sdk: 'toolUse' },
704
+ { openai: 'length', sdk: 'maxTokens' },
705
+ { openai: 'content_filter', sdk: 'contentFiltered' },
706
+ ];
707
+ for (const { openai, sdk } of stopReasons) {
708
+ const mockClient = createMockClient(async function* () {
709
+ yield {
710
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
711
+ };
712
+ yield {
713
+ choices: [{ finish_reason: openai, delta: {}, index: 0 }],
714
+ };
715
+ });
716
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
717
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
718
+ const events = await collectIterator(provider.stream(messages));
719
+ const stopEvent = events.find((e) => e.type === 'modelMessageStopEvent');
720
+ expect(stopEvent).toBeDefined();
721
+ expect(stopEvent.stopReason).toBe(sdk);
722
+ }
723
+ });
724
+ it('handles unknown stop reasons with warning', async () => {
725
+ const mockClient = createMockClient(async function* () {
726
+ yield {
727
+ choices: [{ delta: { role: 'assistant' }, index: 0 }],
728
+ };
729
+ yield {
730
+ choices: [{ finish_reason: 'new_unknown_reason', delta: {}, index: 0 }],
731
+ };
732
+ });
733
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
734
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
735
+ const events = await collectIterator(provider.stream(messages));
736
+ // Should convert unknown stop reason to camelCase
737
+ const stopEvent = events.find((e) => e.type === 'modelMessageStopEvent');
738
+ expect(stopEvent).toBeDefined();
739
+ expect(stopEvent.stopReason).toBe('newUnknownReason');
740
+ // Note: Warning logging is verified manually/visually since console.warn spying
741
+ // has test isolation issues when running the full test suite
742
+ });
743
+ });
744
+ describe('API request formatting', () => {
745
+ it('formats API request correctly with all options', async () => {
746
+ let capturedRequest = null;
747
+ let callCount = 0;
748
+ const mockClient = {
749
+ chat: {
750
+ completions: {
751
+ create: vi.fn(async (request) => {
752
+ capturedRequest = request;
753
+ callCount++;
754
+ // Return an async generator
755
+ return (async function* () {
756
+ yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
757
+ yield { choices: [{ finish_reason: 'stop', delta: {}, index: 0 }] };
758
+ })();
759
+ }),
760
+ },
761
+ },
762
+ };
763
+ const provider = new OpenAIModel({
764
+ modelId: 'gpt-4o',
765
+ client: mockClient,
766
+ temperature: 0.7,
767
+ maxTokens: 1000,
768
+ });
769
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
770
+ const toolSpecs = [
771
+ {
772
+ name: 'calculator',
773
+ description: 'Calculate expressions',
774
+ inputSchema: { type: 'object', properties: { expr: { type: 'string' } } },
775
+ },
776
+ ];
777
+ await collectIterator(provider.stream(messages, {
778
+ systemPrompt: 'You are a helpful assistant',
779
+ toolSpecs,
780
+ toolChoice: { auto: {} },
781
+ }));
782
+ // Verify create was called with correct structure
783
+ expect(callCount).toBe(1);
784
+ expect(capturedRequest).toBeDefined();
785
+ expect(capturedRequest).toEqual({
786
+ model: 'gpt-4o',
787
+ stream: true,
788
+ stream_options: { include_usage: true },
789
+ temperature: 0.7,
790
+ max_tokens: 1000,
791
+ messages: [
792
+ { role: 'system', content: 'You are a helpful assistant' },
793
+ { role: 'user', content: [{ type: 'text', text: 'Hi' }] },
794
+ ],
795
+ tools: [
796
+ {
797
+ type: 'function',
798
+ function: {
799
+ name: 'calculator',
800
+ description: 'Calculate expressions',
801
+ parameters: { type: 'object', properties: { expr: { type: 'string' } } },
802
+ },
803
+ },
804
+ ],
805
+ tool_choice: 'auto',
806
+ });
807
+ });
808
+ });
809
+ describe('systemPrompt handling', () => {
810
+ // Create mock client factory that captures request in provided container
811
+ const createMockClientWithCapture = (captureContainer) => {
812
+ return {
813
+ chat: {
814
+ completions: {
815
+ create: vi.fn(async (request) => {
816
+ captureContainer.request = request;
817
+ return (async function* () {
818
+ yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
819
+ yield { choices: [{ finish_reason: 'stop', delta: {}, index: 0 }] };
820
+ })();
821
+ }),
822
+ },
823
+ },
824
+ };
825
+ };
826
+ it('formats array system prompt with text blocks only', async () => {
827
+ const captured = { request: null };
828
+ const mockClient = createMockClientWithCapture(captured);
829
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
830
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }];
831
+ await collectIterator(provider.stream(messages, {
832
+ systemPrompt: [
833
+ { type: 'textBlock', text: 'You are a helpful assistant' },
834
+ { type: 'textBlock', text: 'Additional context here' },
835
+ ],
836
+ }));
837
+ expect(captured.request).toBeDefined();
838
+ expect(captured.request.messages).toEqual([
839
+ { role: 'system', content: 'You are a helpful assistantAdditional context here' },
840
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
841
+ ]);
842
+ });
843
+ it('formats array system prompt with cache points', async () => {
844
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
845
+ const captured = { request: null };
846
+ const mockClient = createMockClientWithCapture(captured);
847
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
848
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }];
849
+ collectIterator(provider.stream(messages, {
850
+ systemPrompt: [
851
+ { type: 'textBlock', text: 'You are a helpful assistant' },
852
+ { type: 'textBlock', text: 'Large context document' },
853
+ { type: 'cachePointBlock', cacheType: 'default' },
854
+ ],
855
+ }));
856
+ // Verify warning was logged
857
+ expect(warnSpy).toHaveBeenCalledWith('Cache points are not supported in OpenAI system prompts and will be ignored.');
858
+ // Verify system message contains only text (cache points ignored)
859
+ expect(captured.request).toBeDefined();
860
+ expect(captured.request.messages).toEqual([
861
+ { role: 'system', content: 'You are a helpful assistantLarge context document' },
862
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
863
+ ]);
864
+ warnSpy.mockRestore();
865
+ });
866
+ it('handles empty array system prompt', async () => {
867
+ const captured = { request: null };
868
+ const mockClient = createMockClientWithCapture(captured);
869
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
870
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }];
871
+ await collectIterator(provider.stream(messages, {
872
+ systemPrompt: [],
873
+ }));
874
+ // Empty array should not add system message
875
+ expect(captured.request).toBeDefined();
876
+ expect(captured.request.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }]);
877
+ });
878
+ it('formats array system prompt with single text block', async () => {
879
+ const captured = { request: null };
880
+ const mockClient = createMockClientWithCapture(captured);
881
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
882
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }];
883
+ await collectIterator(provider.stream(messages, {
884
+ systemPrompt: [{ type: 'textBlock', text: 'You are a helpful assistant' }],
885
+ }));
886
+ expect(captured.request).toBeDefined();
887
+ expect(captured.request.messages).toEqual([
888
+ { role: 'system', content: 'You are a helpful assistant' },
889
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
890
+ ]);
891
+ });
892
+ it('warns and filters guard content from system prompt', async () => {
893
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
894
+ const captured = { request: null };
895
+ const mockClient = createMockClientWithCapture(captured);
896
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
897
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }];
898
+ await collectIterator(provider.stream(messages, {
899
+ systemPrompt: [
900
+ { type: 'textBlock', text: 'You are a helpful assistant' },
901
+ {
902
+ type: 'guardContentBlock',
903
+ text: {
904
+ qualifiers: ['grounding_source'],
905
+ text: 'Guard content',
906
+ },
907
+ },
908
+ ],
909
+ }));
910
+ // Verify warning was logged
911
+ expect(warnSpy).toHaveBeenCalledWith('OpenAI does not support guard content in system prompts. Removing guard content block.');
912
+ // Verify guard content is filtered out
913
+ expect(captured.request).toBeDefined();
914
+ expect(captured.request.messages).toEqual([
915
+ { role: 'system', content: 'You are a helpful assistant' },
916
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
917
+ ]);
918
+ warnSpy.mockRestore();
919
+ });
920
+ it('preserves text blocks when filtering guard content', async () => {
921
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
922
+ const captured = { request: null };
923
+ const mockClient = createMockClientWithCapture(captured);
924
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
925
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }];
926
+ await collectIterator(provider.stream(messages, {
927
+ systemPrompt: [
928
+ { type: 'textBlock', text: 'First text' },
929
+ {
930
+ type: 'guardContentBlock',
931
+ text: {
932
+ qualifiers: ['query'],
933
+ text: 'Guard content',
934
+ },
935
+ },
936
+ { type: 'textBlock', text: 'Second text' },
937
+ ],
938
+ }));
939
+ // Verify warning was logged
940
+ expect(warnSpy).toHaveBeenCalledWith('OpenAI does not support guard content in system prompts. Removing guard content block.');
941
+ // Verify both text blocks preserved, guard content removed
942
+ expect(captured.request).toBeDefined();
943
+ expect(captured.request.messages).toEqual([
944
+ { role: 'system', content: 'First textSecond text' },
945
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
946
+ ]);
947
+ warnSpy.mockRestore();
948
+ });
949
+ it('handles system prompt with only guard content', async () => {
950
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
951
+ const captured = { request: null };
952
+ const mockClient = createMockClientWithCapture(captured);
953
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
954
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }];
955
+ await collectIterator(provider.stream(messages, {
956
+ systemPrompt: [
957
+ {
958
+ type: 'guardContentBlock',
959
+ text: {
960
+ qualifiers: ['guard_content'],
961
+ text: 'Only guard content',
962
+ },
963
+ },
964
+ ],
965
+ }));
966
+ // Verify warning was logged
967
+ expect(warnSpy).toHaveBeenCalledWith('OpenAI does not support guard content in system prompts. Removing guard content block.');
968
+ // Verify no system message added (only guard content)
969
+ expect(captured.request).toBeDefined();
970
+ expect(captured.request.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }]);
971
+ warnSpy.mockRestore();
972
+ });
973
+ });
974
+ describe('guard content in messages', () => {
975
+ // Create mock client factory that captures request in provided container
976
+ const createMockClientWithCapture = (captureContainer) => {
977
+ return {
978
+ chat: {
979
+ completions: {
980
+ create: vi.fn(async (request) => {
981
+ captureContainer.request = request;
982
+ return (async function* () {
983
+ yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
984
+ yield { choices: [{ finish_reason: 'stop', delta: {}, index: 0 }] };
985
+ })();
986
+ }),
987
+ },
988
+ },
989
+ };
990
+ };
991
+ it('warns and filters guard content from user messages', async () => {
992
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
993
+ const captured = { request: null };
994
+ const mockClient = createMockClientWithCapture(captured);
995
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
996
+ const messages = [
997
+ {
998
+ type: 'message',
999
+ role: 'user',
1000
+ content: [
1001
+ { type: 'textBlock', text: 'Verify this:' },
1002
+ {
1003
+ type: 'guardContentBlock',
1004
+ text: {
1005
+ qualifiers: ['grounding_source'],
1006
+ text: 'Guard content',
1007
+ },
1008
+ },
1009
+ { type: 'textBlock', text: 'Is it correct?' },
1010
+ ],
1011
+ },
1012
+ ];
1013
+ await collectIterator(provider.stream(messages));
1014
+ // Verify warning was logged
1015
+ expect(warnSpy).toHaveBeenCalledWith('OpenAI ChatCompletions API does not support content type: guardContentBlock.');
1016
+ // Verify guard content filtered out
1017
+ expect(captured.request).toBeDefined();
1018
+ expect(captured.request.messages).toEqual([
1019
+ {
1020
+ role: 'user',
1021
+ content: [
1022
+ { type: 'text', text: 'Verify this:' },
1023
+ { type: 'text', text: 'Is it correct?' },
1024
+ ],
1025
+ },
1026
+ ]);
1027
+ warnSpy.mockRestore();
1028
+ });
1029
+ it('warns and filters guard content with image from user messages', async () => {
1030
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
1031
+ const captured = { request: null };
1032
+ const mockClient = createMockClientWithCapture(captured);
1033
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
1034
+ const imageBytes = new Uint8Array([1, 2, 3, 4]);
1035
+ const messages = [
1036
+ {
1037
+ type: 'message',
1038
+ role: 'user',
1039
+ content: [
1040
+ { type: 'textBlock', text: 'Check this image:' },
1041
+ {
1042
+ type: 'guardContentBlock',
1043
+ image: {
1044
+ format: 'jpeg',
1045
+ source: { bytes: imageBytes },
1046
+ },
1047
+ },
1048
+ ],
1049
+ },
1050
+ ];
1051
+ await collectIterator(provider.stream(messages));
1052
+ // Verify warning was logged
1053
+ expect(warnSpy).toHaveBeenCalledWith('OpenAI ChatCompletions API does not support content type: guardContentBlock.');
1054
+ // Verify guard content filtered out
1055
+ expect(captured.request).toBeDefined();
1056
+ expect(captured.request.messages).toEqual([
1057
+ { role: 'user', content: [{ type: 'text', text: 'Check this image:' }] },
1058
+ ]);
1059
+ warnSpy.mockRestore();
1060
+ });
1061
+ it('handles message with only guard content', async () => {
1062
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
1063
+ const captured = { request: null };
1064
+ const mockClient = createMockClientWithCapture(captured);
1065
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
1066
+ const messages = [
1067
+ {
1068
+ type: 'message',
1069
+ role: 'user',
1070
+ content: [
1071
+ {
1072
+ type: 'guardContentBlock',
1073
+ text: {
1074
+ qualifiers: ['guard_content'],
1075
+ text: 'Only guard content',
1076
+ },
1077
+ },
1078
+ ],
1079
+ },
1080
+ ];
1081
+ await collectIterator(provider.stream(messages));
1082
+ // Verify warning was logged
1083
+ expect(warnSpy).toHaveBeenCalledWith('OpenAI ChatCompletions API does not support content type: guardContentBlock.');
1084
+ // Verify no user message added (only guard content)
1085
+ expect(captured.request).toBeDefined();
1086
+ expect(captured.request.messages).toEqual([]);
1087
+ warnSpy.mockRestore();
1088
+ });
1089
+ });
1090
+ describe('error handling', () => {
1091
+ it('throws ContextWindowOverflowError for structured error with code', async () => {
1092
+ const mockClient = {
1093
+ chat: {
1094
+ completions: {
1095
+ create: vi.fn(async () => {
1096
+ const error = new Error('Context length exceeded');
1097
+ error.code = 'context_length_exceeded';
1098
+ throw error;
1099
+ }),
1100
+ },
1101
+ },
1102
+ };
1103
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
1104
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
1105
+ await expect(async () => {
1106
+ for await (const _ of provider.stream(messages)) {
1107
+ // Should not reach here
1108
+ }
1109
+ }).rejects.toThrow(ContextWindowOverflowError);
1110
+ });
1111
+ it('throws ContextWindowOverflowError for error with message pattern', async () => {
1112
+ const mockClient = {
1113
+ chat: {
1114
+ completions: {
1115
+ create: vi.fn(async () => {
1116
+ throw new Error('maximum context length exceeded');
1117
+ }),
1118
+ },
1119
+ },
1120
+ };
1121
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
1122
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
1123
+ await expect(async () => {
1124
+ for await (const _ of provider.stream(messages)) {
1125
+ // Should not reach here
1126
+ }
1127
+ }).rejects.toThrow(ContextWindowOverflowError);
1128
+ });
1129
+ it('throws ContextWindowOverflowError for APIError instance', async () => {
1130
+ const mockClient = {
1131
+ chat: {
1132
+ completions: {
1133
+ create: vi.fn(async () => {
1134
+ // Simulate APIError from openai package
1135
+ const error = new Error('Context length exceeded');
1136
+ error.name = 'APIError';
1137
+ error.status = 400;
1138
+ error.code = 'context_length_exceeded';
1139
+ // Make it behave like an APIError instance
1140
+ Object.setPrototypeOf(error, Error.prototype);
1141
+ throw error;
1142
+ }),
1143
+ },
1144
+ },
1145
+ };
1146
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
1147
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
1148
+ await expect(async () => {
1149
+ for await (const _ of provider.stream(messages)) {
1150
+ // Should not reach here
1151
+ }
1152
+ }).rejects.toThrow(ContextWindowOverflowError);
1153
+ });
1154
+ it('passes through other errors unchanged', async () => {
1155
+ const mockClient = {
1156
+ chat: {
1157
+ completions: {
1158
+ create: vi.fn(async () => {
1159
+ throw new Error('Invalid API key');
1160
+ }),
1161
+ },
1162
+ },
1163
+ };
1164
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
1165
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
1166
+ await expect(async () => {
1167
+ for await (const _ of provider.stream(messages)) {
1168
+ // Should not reach here
1169
+ }
1170
+ }).rejects.toThrow('Invalid API key');
1171
+ });
1172
+ it('handles stream interruption errors', async () => {
1173
+ const mockClient = createMockClient(async function* () {
1174
+ yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
1175
+ yield { choices: [{ delta: { content: 'Hello' }, index: 0 }] };
1176
+ // Stream interruption
1177
+ throw new Error('Network connection lost');
1178
+ });
1179
+ const provider = new OpenAIModel({ modelId: 'gpt-4o', client: mockClient });
1180
+ const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
1181
+ await expect(async () => {
1182
+ for await (const _ of provider.stream(messages)) {
1183
+ // Stream will be interrupted
1184
+ }
1185
+ }).rejects.toThrow('Network connection lost');
1186
+ });
1187
+ });
1188
+ });
1189
+ //# sourceMappingURL=openai.test.js.map