@strands-agents/sdk 1.0.0-rc.5 → 1.0.0

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 (250) hide show
  1. package/LICENSE +175 -0
  2. package/README.md +340 -0
  3. package/dist/src/__fixtures__/agent-helpers.d.ts +6 -0
  4. package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
  5. package/dist/src/__fixtures__/agent-helpers.js +3 -1
  6. package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
  7. package/dist/src/__fixtures__/mock-plugin.d.ts.map +1 -1
  8. package/dist/src/__fixtures__/mock-plugin.js +3 -1
  9. package/dist/src/__fixtures__/mock-plugin.js.map +1 -1
  10. package/dist/src/__fixtures__/tool-helpers.d.ts +3 -1
  11. package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -1
  12. package/dist/src/__fixtures__/tool-helpers.js +3 -1
  13. package/dist/src/__fixtures__/tool-helpers.js.map +1 -1
  14. package/dist/src/__tests__/mcp.test.js +222 -2
  15. package/dist/src/__tests__/mcp.test.js.map +1 -1
  16. package/dist/src/a2a/__tests__/events.test.js +2 -0
  17. package/dist/src/a2a/__tests__/events.test.js.map +1 -1
  18. package/dist/src/a2a/__tests__/executor.test.js +16 -5
  19. package/dist/src/a2a/__tests__/executor.test.js.map +1 -1
  20. package/dist/src/a2a/a2a-agent.d.ts +8 -3
  21. package/dist/src/a2a/a2a-agent.d.ts.map +1 -1
  22. package/dist/src/a2a/a2a-agent.js +12 -6
  23. package/dist/src/a2a/a2a-agent.js.map +1 -1
  24. package/dist/src/a2a/executor.d.ts +13 -0
  25. package/dist/src/a2a/executor.d.ts.map +1 -1
  26. package/dist/src/a2a/executor.js +19 -1
  27. package/dist/src/a2a/executor.js.map +1 -1
  28. package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.d.ts +2 -0
  29. package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.d.ts.map +1 -0
  30. package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.js +23 -0
  31. package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.js.map +1 -0
  32. package/dist/src/agent/__tests__/agent.cancel.test.js +1 -1
  33. package/dist/src/agent/__tests__/agent.cancel.test.js.map +1 -1
  34. package/dist/src/agent/__tests__/agent.concurrent.test.d.ts +2 -0
  35. package/dist/src/agent/__tests__/agent.concurrent.test.d.ts.map +1 -0
  36. package/dist/src/agent/__tests__/agent.concurrent.test.js +488 -0
  37. package/dist/src/agent/__tests__/agent.concurrent.test.js.map +1 -0
  38. package/dist/src/agent/__tests__/agent.hook.test.js +174 -12
  39. package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -1
  40. package/dist/src/agent/__tests__/agent.invocation-state.test.d.ts +2 -0
  41. package/dist/src/agent/__tests__/agent.invocation-state.test.d.ts.map +1 -0
  42. package/dist/src/agent/__tests__/agent.invocation-state.test.js +219 -0
  43. package/dist/src/agent/__tests__/agent.invocation-state.test.js.map +1 -0
  44. package/dist/src/agent/__tests__/agent.stateful-model.test.d.ts +2 -0
  45. package/dist/src/agent/__tests__/agent.stateful-model.test.d.ts.map +1 -0
  46. package/dist/src/agent/__tests__/agent.stateful-model.test.js +169 -0
  47. package/dist/src/agent/__tests__/agent.stateful-model.test.js.map +1 -0
  48. package/dist/src/agent/__tests__/agent.test.js +99 -2
  49. package/dist/src/agent/__tests__/agent.test.js.map +1 -1
  50. package/dist/src/agent/__tests__/agent.tracer.test.node.js +39 -0
  51. package/dist/src/agent/__tests__/agent.tracer.test.node.js.map +1 -1
  52. package/dist/src/agent/__tests__/snapshot.test.js +5 -4
  53. package/dist/src/agent/__tests__/snapshot.test.js.map +1 -1
  54. package/dist/src/agent/agent-as-tool.d.ts.map +1 -1
  55. package/dist/src/agent/agent-as-tool.js +4 -2
  56. package/dist/src/agent/agent-as-tool.js.map +1 -1
  57. package/dist/src/agent/agent.d.ts +75 -1
  58. package/dist/src/agent/agent.d.ts.map +1 -1
  59. package/dist/src/agent/agent.js +323 -83
  60. package/dist/src/agent/agent.js.map +1 -1
  61. package/dist/src/agent/snapshot.d.ts +2 -2
  62. package/dist/src/agent/snapshot.d.ts.map +1 -1
  63. package/dist/src/agent/snapshot.js +8 -2
  64. package/dist/src/agent/snapshot.js.map +1 -1
  65. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js +4 -4
  66. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js.map +1 -1
  67. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js +2 -2
  68. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js.map +1 -1
  69. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +8 -3
  70. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js.map +1 -1
  71. package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js +1 -0
  72. package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js.map +1 -1
  73. package/dist/src/errors.d.ts +11 -0
  74. package/dist/src/errors.d.ts.map +1 -1
  75. package/dist/src/errors.js +12 -0
  76. package/dist/src/errors.js.map +1 -1
  77. package/dist/src/hooks/__tests__/events.test.js +177 -70
  78. package/dist/src/hooks/__tests__/events.test.js.map +1 -1
  79. package/dist/src/hooks/__tests__/registry.test.js +16 -16
  80. package/dist/src/hooks/__tests__/registry.test.js.map +1 -1
  81. package/dist/src/hooks/events.d.ts +95 -25
  82. package/dist/src/hooks/events.d.ts.map +1 -1
  83. package/dist/src/hooks/events.js +98 -23
  84. package/dist/src/hooks/events.js.map +1 -1
  85. package/dist/src/index.d.ts +5 -4
  86. package/dist/src/index.d.ts.map +1 -1
  87. package/dist/src/index.js.map +1 -1
  88. package/dist/src/logging/__tests__/warn-once.test.d.ts +2 -0
  89. package/dist/src/logging/__tests__/warn-once.test.d.ts.map +1 -0
  90. package/dist/src/logging/__tests__/warn-once.test.js +30 -0
  91. package/dist/src/logging/__tests__/warn-once.test.js.map +1 -0
  92. package/dist/src/logging/warn-once.d.ts +13 -0
  93. package/dist/src/logging/warn-once.d.ts.map +1 -0
  94. package/dist/src/logging/warn-once.js +18 -0
  95. package/dist/src/logging/warn-once.js.map +1 -0
  96. package/dist/src/mcp.d.ts +20 -1
  97. package/dist/src/mcp.d.ts.map +1 -1
  98. package/dist/src/mcp.js +10 -1
  99. package/dist/src/mcp.js.map +1 -1
  100. package/dist/src/mime.d.ts +2 -1
  101. package/dist/src/mime.d.ts.map +1 -1
  102. package/dist/src/mime.js +1 -0
  103. package/dist/src/mime.js.map +1 -1
  104. package/dist/src/models/__tests__/anthropic.test.js +92 -3
  105. package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
  106. package/dist/src/models/__tests__/bedrock.test.js +113 -2
  107. package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
  108. package/dist/src/models/__tests__/google.test.js +77 -0
  109. package/dist/src/models/__tests__/google.test.js.map +1 -1
  110. package/dist/src/models/__tests__/model.test.js +149 -1
  111. package/dist/src/models/__tests__/model.test.js.map +1 -1
  112. package/dist/src/models/anthropic.d.ts +12 -1
  113. package/dist/src/models/anthropic.d.ts.map +1 -1
  114. package/dist/src/models/anthropic.js +38 -6
  115. package/dist/src/models/anthropic.js.map +1 -1
  116. package/dist/src/models/bedrock.d.ts +12 -1
  117. package/dist/src/models/bedrock.d.ts.map +1 -1
  118. package/dist/src/models/bedrock.js +45 -11
  119. package/dist/src/models/bedrock.js.map +1 -1
  120. package/dist/src/models/defaults.d.ts +37 -0
  121. package/dist/src/models/defaults.d.ts.map +1 -0
  122. package/dist/src/models/defaults.js +41 -0
  123. package/dist/src/models/defaults.js.map +1 -0
  124. package/dist/src/models/google/model.d.ts +14 -1
  125. package/dist/src/models/google/model.d.ts.map +1 -1
  126. package/dist/src/models/google/model.js +50 -6
  127. package/dist/src/models/google/model.js.map +1 -1
  128. package/dist/src/models/model.d.ts +50 -0
  129. package/dist/src/models/model.d.ts.map +1 -1
  130. package/dist/src/models/model.js +120 -0
  131. package/dist/src/models/model.js.map +1 -1
  132. package/dist/src/models/openai/__tests__/chat.test.d.ts +2 -0
  133. package/dist/src/models/openai/__tests__/chat.test.d.ts.map +1 -0
  134. package/dist/src/models/{__tests__/openai.test.js → openai/__tests__/chat.test.js} +72 -7
  135. package/dist/src/models/openai/__tests__/chat.test.js.map +1 -0
  136. package/dist/src/models/openai/__tests__/responses.test.d.ts +2 -0
  137. package/dist/src/models/openai/__tests__/responses.test.d.ts.map +1 -0
  138. package/dist/src/models/openai/__tests__/responses.test.js +668 -0
  139. package/dist/src/models/openai/__tests__/responses.test.js.map +1 -0
  140. package/dist/src/models/openai/chat-adapter.d.ts +33 -0
  141. package/dist/src/models/openai/chat-adapter.d.ts.map +1 -0
  142. package/dist/src/models/openai/chat-adapter.js +383 -0
  143. package/dist/src/models/openai/chat-adapter.js.map +1 -0
  144. package/dist/src/models/openai/errors.d.ts +16 -0
  145. package/dist/src/models/openai/errors.d.ts.map +1 -0
  146. package/dist/src/models/openai/errors.js +40 -0
  147. package/dist/src/models/openai/errors.js.map +1 -0
  148. package/dist/src/models/openai/formatting.d.ts +18 -0
  149. package/dist/src/models/openai/formatting.d.ts.map +1 -0
  150. package/dist/src/models/openai/formatting.js +38 -0
  151. package/dist/src/models/openai/formatting.js.map +1 -0
  152. package/dist/src/models/openai/index.d.ts +19 -0
  153. package/dist/src/models/openai/index.d.ts.map +1 -0
  154. package/dist/src/models/openai/index.js +18 -0
  155. package/dist/src/models/openai/index.js.map +1 -0
  156. package/dist/src/models/openai/model.d.ts +77 -0
  157. package/dist/src/models/openai/model.d.ts.map +1 -0
  158. package/dist/src/models/openai/model.js +211 -0
  159. package/dist/src/models/openai/model.js.map +1 -0
  160. package/dist/src/models/openai/responses-adapter.d.ts +78 -0
  161. package/dist/src/models/openai/responses-adapter.d.ts.map +1 -0
  162. package/dist/src/models/openai/responses-adapter.js +467 -0
  163. package/dist/src/models/openai/responses-adapter.js.map +1 -0
  164. package/dist/src/models/openai/types.d.ts +131 -0
  165. package/dist/src/models/openai/types.d.ts.map +1 -0
  166. package/dist/src/models/openai/types.js +5 -0
  167. package/dist/src/models/openai/types.js.map +1 -0
  168. package/dist/src/multiagent/__tests__/events.test.js +122 -28
  169. package/dist/src/multiagent/__tests__/events.test.js.map +1 -1
  170. package/dist/src/multiagent/__tests__/graph.invocation-state.test.d.ts +2 -0
  171. package/dist/src/multiagent/__tests__/graph.invocation-state.test.d.ts.map +1 -0
  172. package/dist/src/multiagent/__tests__/graph.invocation-state.test.js +95 -0
  173. package/dist/src/multiagent/__tests__/graph.invocation-state.test.js.map +1 -0
  174. package/dist/src/multiagent/__tests__/nodes.test.js +5 -2
  175. package/dist/src/multiagent/__tests__/nodes.test.js.map +1 -1
  176. package/dist/src/multiagent/__tests__/swarm.invocation-state.test.d.ts +2 -0
  177. package/dist/src/multiagent/__tests__/swarm.invocation-state.test.d.ts.map +1 -0
  178. package/dist/src/multiagent/__tests__/swarm.invocation-state.test.js +56 -0
  179. package/dist/src/multiagent/__tests__/swarm.invocation-state.test.js.map +1 -0
  180. package/dist/src/multiagent/events.d.ts +19 -1
  181. package/dist/src/multiagent/events.d.ts.map +1 -1
  182. package/dist/src/multiagent/events.js +18 -0
  183. package/dist/src/multiagent/events.js.map +1 -1
  184. package/dist/src/multiagent/graph.d.ts +5 -3
  185. package/dist/src/multiagent/graph.d.ts.map +1 -1
  186. package/dist/src/multiagent/graph.js +22 -15
  187. package/dist/src/multiagent/graph.js.map +1 -1
  188. package/dist/src/multiagent/index.d.ts +1 -1
  189. package/dist/src/multiagent/index.d.ts.map +1 -1
  190. package/dist/src/multiagent/multiagent.d.ts +16 -3
  191. package/dist/src/multiagent/multiagent.d.ts.map +1 -1
  192. package/dist/src/multiagent/nodes.d.ts +10 -3
  193. package/dist/src/multiagent/nodes.d.ts.map +1 -1
  194. package/dist/src/multiagent/nodes.js +28 -6
  195. package/dist/src/multiagent/nodes.js.map +1 -1
  196. package/dist/src/multiagent/swarm.d.ts +5 -3
  197. package/dist/src/multiagent/swarm.d.ts.map +1 -1
  198. package/dist/src/multiagent/swarm.js +22 -16
  199. package/dist/src/multiagent/swarm.js.map +1 -1
  200. package/dist/src/plugins/__tests__/registry.test.js +1 -1
  201. package/dist/src/plugins/__tests__/registry.test.js.map +1 -1
  202. package/dist/src/plugins/model-plugin.d.ts +20 -0
  203. package/dist/src/plugins/model-plugin.d.ts.map +1 -0
  204. package/dist/src/plugins/model-plugin.js +29 -0
  205. package/dist/src/plugins/model-plugin.js.map +1 -0
  206. package/dist/src/session/__tests__/session-manager.test.js +13 -11
  207. package/dist/src/session/__tests__/session-manager.test.js.map +1 -1
  208. package/dist/src/session/session-manager.d.ts.map +1 -1
  209. package/dist/src/session/session-manager.js +9 -0
  210. package/dist/src/session/session-manager.js.map +1 -1
  211. package/dist/src/telemetry/__tests__/meter.test.js +23 -0
  212. package/dist/src/telemetry/__tests__/meter.test.js.map +1 -1
  213. package/dist/src/telemetry/meter.d.ts +15 -0
  214. package/dist/src/telemetry/meter.d.ts.map +1 -1
  215. package/dist/src/telemetry/meter.js +14 -0
  216. package/dist/src/telemetry/meter.js.map +1 -1
  217. package/dist/src/tools/mcp-tool.d.ts +24 -3
  218. package/dist/src/tools/mcp-tool.d.ts.map +1 -1
  219. package/dist/src/tools/mcp-tool.js +103 -31
  220. package/dist/src/tools/mcp-tool.js.map +1 -1
  221. package/dist/src/tools/tool.d.ts +11 -1
  222. package/dist/src/tools/tool.d.ts.map +1 -1
  223. package/dist/src/tools/tool.js.map +1 -1
  224. package/dist/src/tsconfig.tsbuildinfo +1 -1
  225. package/dist/src/types/__tests__/agent.test.js +48 -0
  226. package/dist/src/types/__tests__/agent.test.js.map +1 -1
  227. package/dist/src/types/agent.d.ts +55 -6
  228. package/dist/src/types/agent.d.ts.map +1 -1
  229. package/dist/src/types/agent.js +22 -6
  230. package/dist/src/types/agent.js.map +1 -1
  231. package/dist/src/types/elicitation.d.ts +15 -0
  232. package/dist/src/types/elicitation.d.ts.map +1 -0
  233. package/dist/src/types/elicitation.js +2 -0
  234. package/dist/src/types/elicitation.js.map +1 -0
  235. package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js +9 -5
  236. package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js.map +1 -1
  237. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js +1 -0
  238. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -1
  239. package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js +1 -0
  240. package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js.map +1 -1
  241. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js +1 -0
  242. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js.map +1 -1
  243. package/package.json +9 -5
  244. package/dist/src/models/__tests__/openai.test.d.ts +0 -2
  245. package/dist/src/models/__tests__/openai.test.d.ts.map +0 -1
  246. package/dist/src/models/__tests__/openai.test.js.map +0 -1
  247. package/dist/src/models/openai.d.ts +0 -312
  248. package/dist/src/models/openai.d.ts.map +0 -1
  249. package/dist/src/models/openai.js +0 -789
  250. package/dist/src/models/openai.js.map +0 -1
@@ -1,789 +0,0 @@
1
- /**
2
- * OpenAI model provider implementation.
3
- *
4
- * This module provides integration with OpenAI's Chat Completions API,
5
- * supporting streaming responses, tool use, and configurable model parameters.
6
- *
7
- * @see https://platform.openai.com/docs/api-reference/chat/create
8
- */
9
- import OpenAI, {} from 'openai';
10
- import { Model } from '../models/model.js';
11
- import { encodeBase64 } from '../types/media.js';
12
- import { toMimeType } from '../mime.js';
13
- import { ContextWindowOverflowError, ModelThrottledError } from '../errors.js';
14
- import { logger } from '../logging/logger.js';
15
- const DEFAULT_OPENAI_MODEL_ID = 'gpt-5.4';
16
- /**
17
- * Error message patterns that indicate context window overflow.
18
- * Used to detect when input exceeds the model's context window.
19
- *
20
- * @see https://platform.openai.com/docs/guides/error-codes
21
- */
22
- const OPENAI_CONTEXT_WINDOW_OVERFLOW_PATTERNS = [
23
- 'maximum context length',
24
- 'context_length_exceeded',
25
- 'too many tokens',
26
- 'context length',
27
- ];
28
- /**
29
- * Error patterns and status codes that indicate rate limiting.
30
- * Used to detect when the API is throttling requests.
31
- *
32
- * @see https://platform.openai.com/docs/guides/error-codes
33
- */
34
- const OPENAI_RATE_LIMIT_PATTERNS = ['rate_limit_exceeded', 'rate limit', 'too many requests'];
35
- /**
36
- * OpenAI model provider implementation.
37
- *
38
- * Implements the Model interface for OpenAI using the Chat Completions API.
39
- * Supports streaming responses, tool use, and comprehensive configuration.
40
- *
41
- * @example
42
- * ```typescript
43
- * const provider = new OpenAIModel({
44
- * api: 'chat',
45
- * apiKey: 'sk-...',
46
- * modelId: 'gpt-5.4',
47
- * temperature: 0.7,
48
- * maxTokens: 1024
49
- * })
50
- *
51
- * const messages: Message[] = [
52
- * { role: 'user', content: [{ type: 'textBlock', text: 'Hello!' }] }
53
- * ]
54
- *
55
- * for await (const event of provider.stream(messages)) {
56
- * if (event.type === 'modelContentBlockDeltaEvent' && event.delta.type === 'textDelta') {
57
- * process.stdout.write(event.delta.text)
58
- * }
59
- * }
60
- * ```
61
- */
62
- export class OpenAIModel extends Model {
63
- _config;
64
- _client;
65
- /**
66
- * Creates a new OpenAIModel instance.
67
- *
68
- * @param options - Configuration for model and client
69
- *
70
- * @example
71
- * ```typescript
72
- * // Minimal configuration with API key and model ID
73
- * const provider = new OpenAIModel({
74
- * api: 'chat',
75
- * modelId: 'gpt-5.4',
76
- * apiKey: 'sk-...'
77
- * })
78
- *
79
- * // With additional model configuration
80
- * const provider = new OpenAIModel({
81
- * api: 'chat',
82
- * modelId: 'gpt-5.4',
83
- * apiKey: 'sk-...',
84
- * temperature: 0.8,
85
- * maxTokens: 2048
86
- * })
87
- *
88
- * // Using environment variable for API key
89
- * const provider = new OpenAIModel({
90
- * api: 'chat',
91
- * modelId: 'gpt-5.4-mini'
92
- * })
93
- *
94
- * // Using function-based API key for dynamic key retrieval
95
- * const provider = new OpenAIModel({
96
- * api: 'chat',
97
- * modelId: 'gpt-5.4',
98
- * apiKey: async () => await getRotatingApiKey()
99
- * })
100
- *
101
- * // Using a pre-configured client instance
102
- * const client = new OpenAI({ apiKey: 'sk-...', timeout: 60000 })
103
- * const provider = new OpenAIModel({
104
- * api: 'chat',
105
- * modelId: 'gpt-5.4',
106
- * client
107
- * })
108
- * ```
109
- */
110
- constructor(options) {
111
- super();
112
- const { api, apiKey, client, clientConfig, ...modelConfig } = options;
113
- // Validate api field
114
- if (api !== 'chat') {
115
- throw new Error(`Unsupported OpenAI API: '${api}'. Supported values: 'chat'`);
116
- }
117
- // Initialize model config
118
- this._config = modelConfig;
119
- // Use provided client or create a new one
120
- if (client) {
121
- this._client = client;
122
- }
123
- else {
124
- // Check if API key is available when creating a new client
125
- // In browsers, apiKey must be provided directly
126
- // In Node.js, can use OPENAI_API_KEY environment variable as fallback
127
- const hasEnvKey = typeof process !== 'undefined' && typeof process.env !== 'undefined' && process.env.OPENAI_API_KEY;
128
- if (!apiKey && !hasEnvKey) {
129
- throw new Error("OpenAI API key is required. Provide it via the 'apiKey' option (string or function) or set the OPENAI_API_KEY environment variable.");
130
- }
131
- // Initialize OpenAI client
132
- // Only include apiKey if explicitly provided, otherwise let client use env var
133
- this._client = new OpenAI({
134
- ...(apiKey ? { apiKey } : {}),
135
- ...clientConfig,
136
- });
137
- }
138
- }
139
- /**
140
- * Updates the model configuration.
141
- * Merges the provided configuration with existing settings.
142
- *
143
- * @param modelConfig - Configuration object with model-specific settings to update
144
- *
145
- * @example
146
- * ```typescript
147
- * // Update temperature and maxTokens
148
- * provider.updateConfig({
149
- * temperature: 0.9,
150
- * maxTokens: 2048
151
- * })
152
- * ```
153
- */
154
- updateConfig(modelConfig) {
155
- this._config = { ...this._config, ...modelConfig };
156
- }
157
- /**
158
- * Retrieves the current model configuration.
159
- *
160
- * @returns The current configuration object
161
- *
162
- * @example
163
- * ```typescript
164
- * const config = provider.getConfig()
165
- * console.log(config.modelId)
166
- * ```
167
- */
168
- getConfig() {
169
- return this._config;
170
- }
171
- /**
172
- * Streams a conversation with the OpenAI model.
173
- * Returns an async iterable that yields streaming events as they occur.
174
- *
175
- * @param messages - Array of conversation messages
176
- * @param options - Optional streaming configuration
177
- * @returns Async iterable of streaming events
178
- *
179
- * @throws \{ContextWindowOverflowError\} When input exceeds the model's context window
180
- *
181
- * @example
182
- * ```typescript
183
- * const provider = new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4', apiKey: 'sk-...' })
184
- * const messages: Message[] = [
185
- * { role: 'user', content: [{ type: 'textBlock', text: 'What is 2+2?' }] }
186
- * ]
187
- *
188
- * for await (const event of provider.stream(messages)) {
189
- * if (event.type === 'modelContentBlockDeltaEvent' && event.delta.type === 'textDelta') {
190
- * process.stdout.write(event.delta.text)
191
- * }
192
- * }
193
- * ```
194
- *
195
- * @example
196
- * ```typescript
197
- * // With tool use
198
- * const options: StreamOptions = {
199
- * systemPrompt: 'You are a helpful assistant',
200
- * toolSpecs: [calculatorTool]
201
- * }
202
- *
203
- * for await (const event of provider.stream(messages, options)) {
204
- * if (event.type === 'modelMessageStopEvent' && event.stopReason === 'toolUse') {
205
- * console.log('Model wants to use a tool')
206
- * }
207
- * }
208
- * ```
209
- */
210
- async *stream(messages, options) {
211
- // Validate messages array is not empty
212
- if (!messages || messages.length === 0) {
213
- throw new Error('At least one message is required');
214
- }
215
- try {
216
- // Format the request
217
- const request = this._formatRequest(messages, options);
218
- // Create streaming request with usage tracking
219
- const stream = await this._client.chat.completions.create(request);
220
- // Track streaming state (Use mutable object for proper state tracking)
221
- const streamState = {
222
- messageStarted: false,
223
- textContentBlockStarted: false,
224
- };
225
- // Track active tool calls for stop events
226
- const activeToolCalls = new Map();
227
- // Buffer usage to emit before message stop
228
- let bufferedUsage = null;
229
- // Process streaming response
230
- for await (const chunk of stream) {
231
- if (!chunk.choices || chunk.choices.length === 0) {
232
- // Handle usage chunk (no choices)
233
- // Buffer usage to emit before message stop
234
- if (chunk.usage) {
235
- bufferedUsage = {
236
- type: 'modelMetadataEvent',
237
- usage: {
238
- inputTokens: chunk.usage.prompt_tokens ?? 0,
239
- outputTokens: chunk.usage.completion_tokens ?? 0,
240
- totalTokens: chunk.usage.total_tokens ?? 0,
241
- },
242
- };
243
- }
244
- continue;
245
- }
246
- // Map chunk to SDK events
247
- const events = this._mapOpenAIChunkToSDKEvents(chunk, streamState, activeToolCalls);
248
- for (const event of events) {
249
- // Emit buffered usage before message stop
250
- if (event.type === 'modelMessageStopEvent' && bufferedUsage) {
251
- yield bufferedUsage;
252
- bufferedUsage = null;
253
- }
254
- yield event;
255
- }
256
- }
257
- // Emit any remaining buffered usage
258
- if (bufferedUsage) {
259
- yield bufferedUsage;
260
- }
261
- }
262
- catch (error) {
263
- const err = error;
264
- // Check for rate limit errors - OpenAI SDK throws errors with status 429
265
- // or code 'rate_limit_exceeded' for all rate limiting scenarios (TPM, RPM, etc.)
266
- // This matches Python SDK behavior: `except openai.RateLimitError as e`
267
- if (err.status === 429 ||
268
- err.code === 'rate_limit_exceeded' ||
269
- OPENAI_RATE_LIMIT_PATTERNS.some((pattern) => err.message?.toLowerCase().includes(pattern))) {
270
- const message = err.message ?? 'Request was throttled by the model provider';
271
- logger.debug(`throttled | error_message=<${message}>`);
272
- throw new ModelThrottledError(message, { cause: err });
273
- }
274
- // Check for context window overflow using simple pattern matching
275
- if (OPENAI_CONTEXT_WINDOW_OVERFLOW_PATTERNS.some((pattern) => err.message?.toLowerCase().includes(pattern))) {
276
- throw new ContextWindowOverflowError(err.message);
277
- }
278
- // Re-throw other errors unchanged
279
- throw err;
280
- }
281
- }
282
- /**
283
- * Formats a request for the OpenAI Chat Completions API.
284
- *
285
- * @param messages - Conversation messages
286
- * @param options - Stream options
287
- * @returns Formatted OpenAI request
288
- */
289
- _formatRequest(messages, options) {
290
- // Start with required fields
291
- const request = {
292
- model: this._config.modelId ?? DEFAULT_OPENAI_MODEL_ID,
293
- messages: [],
294
- stream: true,
295
- stream_options: { include_usage: true },
296
- };
297
- // Handle system prompt (string or array format)
298
- if (options?.systemPrompt !== undefined) {
299
- if (typeof options.systemPrompt === 'string') {
300
- // String path: validate and add as-is
301
- if (options.systemPrompt.trim().length > 0) {
302
- request.messages.push({
303
- role: 'system',
304
- content: options.systemPrompt,
305
- });
306
- }
307
- }
308
- else if (Array.isArray(options.systemPrompt) && options.systemPrompt.length > 0) {
309
- // Array path: extract text blocks and warn about cache points
310
- const textBlocks = [];
311
- let hasCachePoints = false;
312
- let hasGuardContent = false;
313
- for (const block of options.systemPrompt) {
314
- if (block.type === 'textBlock') {
315
- textBlocks.push(block.text);
316
- }
317
- else if (block.type === 'cachePointBlock') {
318
- hasCachePoints = true;
319
- }
320
- else if (block.type === 'guardContentBlock') {
321
- hasGuardContent = true;
322
- }
323
- }
324
- if (hasCachePoints) {
325
- logger.warn('cache points are not supported in openai system prompts, ignoring cache points');
326
- }
327
- if (hasGuardContent) {
328
- logger.warn('guard content is not supported in openai system prompts, removing guard content block');
329
- }
330
- if (textBlocks.length > 0) {
331
- request.messages.push({
332
- role: 'system',
333
- content: textBlocks.join(''),
334
- });
335
- }
336
- }
337
- }
338
- // Add formatted messages
339
- const formattedMessages = this._formatMessages(messages);
340
- request.messages.push(...formattedMessages);
341
- // Add model configuration parameters
342
- if (this._config.temperature !== undefined) {
343
- request.temperature = this._config.temperature;
344
- }
345
- if (this._config.maxTokens !== undefined) {
346
- request.max_completion_tokens = this._config.maxTokens;
347
- }
348
- if (this._config.topP !== undefined) {
349
- request.top_p = this._config.topP;
350
- }
351
- if (this._config.frequencyPenalty !== undefined) {
352
- request.frequency_penalty = this._config.frequencyPenalty;
353
- }
354
- if (this._config.presencePenalty !== undefined) {
355
- request.presence_penalty = this._config.presencePenalty;
356
- }
357
- // Add tool specifications with validation
358
- if (options?.toolSpecs && options.toolSpecs.length > 0) {
359
- request.tools = options.toolSpecs.map((spec) => {
360
- if (!spec.name || !spec.description) {
361
- throw new Error('Tool specification must have both name and description');
362
- }
363
- return {
364
- type: 'function',
365
- function: {
366
- name: spec.name,
367
- description: spec.description,
368
- parameters: spec.inputSchema,
369
- },
370
- };
371
- });
372
- // Add tool choice if specified
373
- if (options.toolChoice) {
374
- if ('auto' in options.toolChoice) {
375
- request.tool_choice = 'auto';
376
- }
377
- else if ('any' in options.toolChoice) {
378
- request.tool_choice = 'required';
379
- }
380
- else if ('tool' in options.toolChoice) {
381
- request.tool_choice = {
382
- type: 'function',
383
- function: { name: options.toolChoice.tool.name },
384
- };
385
- }
386
- }
387
- }
388
- // Spread params object last for forward compatibility
389
- if (this._config.params) {
390
- Object.assign(request, this._config.params);
391
- }
392
- // Validate n parameter (number of completions) - only n=1 supported for streaming
393
- if ('n' in request && request.n !== undefined && request.n !== null && request.n > 1) {
394
- throw new Error('Streaming with n > 1 is not supported');
395
- }
396
- return request;
397
- }
398
- /**
399
- * Formats messages for OpenAI API.
400
- * Handles splitting tool results into separate messages.
401
- *
402
- * @param messages - SDK messages
403
- * @returns OpenAI-formatted messages
404
- */
405
- _formatMessages(messages) {
406
- const openAIMessages = [];
407
- for (const message of messages) {
408
- if (message.role === 'user') {
409
- // Separate tool results from other content
410
- const toolResults = message.content.filter((b) => b.type === 'toolResultBlock');
411
- const otherContent = message.content.filter((b) => b.type !== 'toolResultBlock');
412
- // Add non-tool-result content as user message
413
- if (otherContent.length > 0) {
414
- const contentParts = [];
415
- for (const block of otherContent) {
416
- switch (block.type) {
417
- case 'textBlock': {
418
- contentParts.push({
419
- type: 'text',
420
- text: block.text,
421
- });
422
- break;
423
- }
424
- case 'imageBlock': {
425
- const formatted = this._formatImageBlock(block);
426
- if (formatted) {
427
- contentParts.push(formatted);
428
- }
429
- break;
430
- }
431
- case 'documentBlock': {
432
- const docBlock = block;
433
- switch (docBlock.source.type) {
434
- case 'documentSourceBytes': {
435
- const mimeType = toMimeType(docBlock.format) || `application/${docBlock.format}`;
436
- const base64 = encodeBase64(docBlock.source.bytes);
437
- const file = {
438
- type: 'file',
439
- file: {
440
- file_data: `data:${mimeType};base64,${base64}`,
441
- filename: docBlock.name,
442
- },
443
- };
444
- contentParts.push(file);
445
- break;
446
- }
447
- case 'documentSourceText': {
448
- // Text documents can be added directly
449
- logger.warn('source_type=<documentSourceText> | openai does not support text document sources directly | converting to string content');
450
- contentParts.push({
451
- type: 'text',
452
- text: docBlock.source.text,
453
- });
454
- break;
455
- }
456
- case 'documentSourceContentBlock': {
457
- // Push each content block as a content part
458
- contentParts.push(...docBlock.source.content.map((block) => {
459
- return {
460
- type: 'text',
461
- text: block.text,
462
- };
463
- }));
464
- break;
465
- }
466
- default: {
467
- logger.warn(`source_type=<${docBlock.source.type}> | openai only supports text content in user messages | skipping document block`);
468
- break;
469
- }
470
- }
471
- break;
472
- }
473
- default: {
474
- logger.warn(`block_type=<${block.type}> | unsupported content type in openai user message | skipping`);
475
- break;
476
- }
477
- }
478
- }
479
- // Validate content is not empty before adding
480
- if (contentParts.length > 0) {
481
- openAIMessages.push({
482
- role: 'user',
483
- content: contentParts,
484
- });
485
- }
486
- }
487
- // Process tool results - split media into separate user messages
488
- // OpenAI API restricts media to user role messages only
489
- const userMessagesWithMedia = [];
490
- for (const toolResult of toolResults) {
491
- // Split tool result into text and image content
492
- const [textContent, imageParts] = this._splitToolResultMedia(toolResult);
493
- // Log warning if images are present
494
- if (imageParts.length > 0) {
495
- logger.warn(`tool_call_id=<${toolResult.toolUseId}> | moving images from tool result to separate user message for openai compatibility`);
496
- }
497
- // Inject placeholder text when tool result contains only images
498
- const effectiveTextContent = textContent.trim().length === 0 && imageParts.length > 0
499
- ? 'Tool successfully returned an image. The image is being provided in the following user message.'
500
- : textContent;
501
- if (!effectiveTextContent || effectiveTextContent.trim().length === 0) {
502
- throw new Error(`Tool result for toolUseId "${toolResult.toolUseId}" has empty content. ` +
503
- 'OpenAI requires tool messages to have non-empty content.');
504
- }
505
- // Prepend error indicator if status is error
506
- const finalContent = toolResult.status === 'error' ? `[ERROR] ${effectiveTextContent}` : effectiveTextContent;
507
- // Add text-only tool message
508
- openAIMessages.push({
509
- role: 'tool',
510
- tool_call_id: toolResult.toolUseId,
511
- content: finalContent,
512
- });
513
- // Queue images for separate user message
514
- if (imageParts.length > 0) {
515
- userMessagesWithMedia.push({
516
- role: 'user',
517
- content: imageParts,
518
- });
519
- }
520
- }
521
- // Add all user messages with images after tool messages
522
- // This maintains proper message ordering for OpenAI API
523
- openAIMessages.push(...userMessagesWithMedia);
524
- }
525
- else {
526
- // Handle assistant messages
527
- const toolUseCalls = [];
528
- // Use array + join pattern for efficient string concatenation
529
- const textParts = [];
530
- for (const block of message.content) {
531
- switch (block.type) {
532
- case 'textBlock': {
533
- textParts.push(block.text);
534
- break;
535
- }
536
- case 'toolUseBlock': {
537
- try {
538
- toolUseCalls.push({
539
- id: block.toolUseId,
540
- type: 'function',
541
- function: {
542
- name: block.name,
543
- arguments: JSON.stringify(block.input),
544
- },
545
- });
546
- }
547
- catch (error) {
548
- if (error instanceof Error) {
549
- throw new Error(`Failed to serialize tool input for "${block.name}`, error);
550
- }
551
- throw error;
552
- }
553
- break;
554
- }
555
- case 'reasoningBlock': {
556
- if (block.text) {
557
- logger.warn('block_type=<reasoningBlock> | reasoning blocks not supported by openai | converting to text');
558
- textParts.push(block.text);
559
- }
560
- break;
561
- }
562
- default: {
563
- logger.warn(`block_type=<${block.type}> | unsupported content type in openai assistant message | skipping`);
564
- }
565
- }
566
- }
567
- // Trim text content to avoid whitespace-only messages
568
- const textContent = textParts.join('').trim();
569
- const assistantMessage = {
570
- role: 'assistant',
571
- content: textContent,
572
- };
573
- if (toolUseCalls.length > 0) {
574
- assistantMessage.tool_calls = toolUseCalls;
575
- }
576
- // Only add if message has content or tool calls
577
- if (textContent.length > 0 || toolUseCalls.length > 0) {
578
- openAIMessages.push(assistantMessage);
579
- }
580
- }
581
- }
582
- return openAIMessages;
583
- }
584
- /**
585
- * Formats an image block to OpenAI image_url format.
586
- *
587
- * @param imageBlock - Image block to format
588
- * @returns OpenAI image_url content part, or undefined if unsupported
589
- */
590
- _formatImageBlock(imageBlock) {
591
- if (imageBlock.source.type === 'imageSourceBytes') {
592
- const base64 = encodeBase64(imageBlock.source.bytes);
593
- const mimeType = toMimeType(imageBlock.format) || `image/${imageBlock.format}`;
594
- return {
595
- type: 'image_url',
596
- image_url: {
597
- url: `data:${mimeType};base64,${base64}`,
598
- },
599
- };
600
- }
601
- else if (imageBlock.source.type === 'imageSourceUrl') {
602
- return {
603
- type: 'image_url',
604
- image_url: {
605
- url: imageBlock.source.url,
606
- },
607
- };
608
- }
609
- return undefined;
610
- }
611
- /**
612
- * Splits tool result content into text and image parts.
613
- * OpenAI API restricts images to user role messages only.
614
- *
615
- * @param toolResult - Tool result block to split
616
- * @returns Tuple of [text content, image parts for user message]
617
- */
618
- _splitToolResultMedia(toolResult) {
619
- const textParts = [];
620
- const imageParts = [];
621
- for (const c of toolResult.content) {
622
- if (c.type === 'textBlock') {
623
- textParts.push(c.text);
624
- }
625
- else if (c.type === 'jsonBlock') {
626
- try {
627
- textParts.push(JSON.stringify(c.json));
628
- }
629
- catch (error) {
630
- if (error instanceof Error) {
631
- const dataPreview = typeof c.json === 'object' && c.json !== null
632
- ? `object with keys: ${Object.keys(c.json).slice(0, 5).join(', ')}`
633
- : typeof c.json;
634
- textParts.push(`[JSON Serialization Error: ${error.message}. Data type: ${dataPreview}]`);
635
- }
636
- }
637
- }
638
- else if (c.type === 'imageBlock') {
639
- const formatted = this._formatImageBlock(c);
640
- if (formatted) {
641
- imageParts.push(formatted);
642
- }
643
- }
644
- else if (c.type === 'documentBlock') {
645
- logger.warn('block_type=<documentBlock> | documents not supported in openai tool results, skipping');
646
- }
647
- else if (c.type === 'videoBlock') {
648
- logger.warn('block_type=<videoBlock> | videos not supported in openai tool results, skipping');
649
- }
650
- }
651
- return [textParts.join(''), imageParts];
652
- }
653
- /**
654
- * Converts a snake_case string to camelCase.
655
- * Used for mapping OpenAI stop reasons to SDK format.
656
- *
657
- * @param str - Snake case string (e.g., 'content_filter')
658
- * @returns Camel case string (e.g., 'contentFilter')
659
- *
660
- * @example
661
- * ```typescript
662
- * _snakeToCamel('context_length_exceeded') // => 'contextLengthExceeded'
663
- * _snakeToCamel('tool_calls') // => 'toolCalls'
664
- * ```
665
- */
666
- _snakeToCamel(str) {
667
- return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
668
- }
669
- /**
670
- * Maps an OpenAI chunk to SDK streaming events.
671
- *
672
- * @param chunk - OpenAI chunk
673
- * @param streamState - Mutable state object tracking message and content block state
674
- * @param activeToolCalls - Map tracking active tool calls by index
675
- * @returns Array of SDK streaming events
676
- */
677
- _mapOpenAIChunkToSDKEvents(chunk, streamState, activeToolCalls) {
678
- const events = [];
679
- // Validate choices array has at least one element
680
- if (!chunk.choices || chunk.choices.length === 0) {
681
- return events;
682
- }
683
- const choice = chunk.choices[0];
684
- // Validate choice is an object
685
- if (!choice || typeof choice !== 'object') {
686
- logger.warn(`choice=<${choice}> | invalid choice format in openai chunk`);
687
- return events;
688
- }
689
- // Process first choice (OpenAI typically returns one choice in streaming)
690
- const typedChoice = choice;
691
- if (!typedChoice.delta && !typedChoice.finish_reason) {
692
- return events;
693
- }
694
- const delta = typedChoice.delta;
695
- // Handle message start (role appears) - update mutable state
696
- if (delta?.role && !streamState.messageStarted) {
697
- streamState.messageStarted = true;
698
- events.push({
699
- type: 'modelMessageStartEvent',
700
- role: delta.role,
701
- });
702
- }
703
- // Handle text content delta and start event
704
- if (delta?.content && delta.content.length > 0) {
705
- // Emit start event on first text delta
706
- if (!streamState.textContentBlockStarted) {
707
- streamState.textContentBlockStarted = true;
708
- events.push({
709
- type: 'modelContentBlockStartEvent',
710
- });
711
- }
712
- events.push({
713
- type: 'modelContentBlockDeltaEvent',
714
- delta: {
715
- type: 'textDelta',
716
- text: delta.content,
717
- },
718
- });
719
- }
720
- // Handle tool calls
721
- if (delta?.tool_calls && delta.tool_calls.length > 0) {
722
- for (const toolCall of delta.tool_calls) {
723
- // Validate tool call index
724
- if (toolCall.index === undefined || typeof toolCall.index !== 'number') {
725
- logger.warn(`tool_call=<${JSON.stringify(toolCall)}> | received tool call with invalid index`);
726
- continue;
727
- }
728
- // If tool call has id and name, it's the start of a new tool call
729
- if (toolCall.id && toolCall.function?.name) {
730
- events.push({
731
- type: 'modelContentBlockStartEvent',
732
- start: {
733
- type: 'toolUseStart',
734
- name: toolCall.function.name,
735
- toolUseId: toolCall.id,
736
- },
737
- });
738
- // Track active tool calls
739
- activeToolCalls.set(toolCall.index, true);
740
- }
741
- // If tool call has arguments, it's a delta
742
- if (toolCall.function?.arguments) {
743
- events.push({
744
- type: 'modelContentBlockDeltaEvent',
745
- delta: {
746
- type: 'toolUseInputDelta',
747
- input: toolCall.function.arguments,
748
- },
749
- });
750
- }
751
- }
752
- }
753
- // Handle finish reason (message stop)
754
- if (typedChoice.finish_reason) {
755
- // Emit stop event for text content if it was started
756
- if (streamState.textContentBlockStarted) {
757
- events.push({
758
- type: 'modelContentBlockStopEvent',
759
- });
760
- streamState.textContentBlockStarted = false;
761
- }
762
- // Emit stop events for all active tool calls and delete during iteration
763
- for (const [index] of activeToolCalls) {
764
- events.push({
765
- type: 'modelContentBlockStopEvent',
766
- });
767
- activeToolCalls.delete(index);
768
- }
769
- // Map OpenAI stop reason to SDK stop reason
770
- const stopReasonMap = {
771
- stop: 'endTurn',
772
- tool_calls: 'toolUse',
773
- length: 'maxTokens',
774
- content_filter: 'contentFiltered',
775
- };
776
- // Log unknown stop reasons
777
- const stopReason = stopReasonMap[typedChoice.finish_reason] ?? this._snakeToCamel(typedChoice.finish_reason);
778
- if (!stopReasonMap[typedChoice.finish_reason]) {
779
- logger.warn(`finish_reason=<${typedChoice.finish_reason}>, fallback=<${stopReason}> | unknown openai stop reason, using camelCase conversion as fallback`);
780
- }
781
- events.push({
782
- type: 'modelMessageStopEvent',
783
- stopReason,
784
- });
785
- }
786
- return events;
787
- }
788
- }
789
- //# sourceMappingURL=openai.js.map