@robota-sdk/agent-provider 3.0.0-beta.64

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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/dist/browser/index.d.ts +1104 -0
  3. package/dist/browser/index.d.ts.map +1 -0
  4. package/dist/browser/index.js +7 -0
  5. package/dist/browser/index.js.map +1 -0
  6. package/dist/loggers/index.cjs +1 -0
  7. package/dist/loggers/index.d.ts +151 -0
  8. package/dist/loggers/index.d.ts.map +1 -0
  9. package/dist/loggers/index.js +2 -0
  10. package/dist/loggers/index.js.map +1 -0
  11. package/dist/node/anthropic/index.cjs +1 -0
  12. package/dist/node/anthropic/index.d.ts +158 -0
  13. package/dist/node/anthropic/index.d.ts.map +1 -0
  14. package/dist/node/anthropic/index.js +1 -0
  15. package/dist/node/anthropic--1vgLC-e.js +5 -0
  16. package/dist/node/anthropic--1vgLC-e.js.map +1 -0
  17. package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
  18. package/dist/node/bytedance/index.cjs +1 -0
  19. package/dist/node/bytedance/index.d.ts +74 -0
  20. package/dist/node/bytedance/index.d.ts.map +1 -0
  21. package/dist/node/bytedance/index.js +1 -0
  22. package/dist/node/bytedance-C_0sF_pJ.js +2 -0
  23. package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
  24. package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
  25. package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
  26. package/dist/node/deepseek/index.cjs +1 -0
  27. package/dist/node/deepseek/index.d.ts +2 -0
  28. package/dist/node/deepseek/index.js +1 -0
  29. package/dist/node/deepseek-_8Ixx7rA.js +2 -0
  30. package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
  31. package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
  32. package/dist/node/gemini/index.cjs +1 -0
  33. package/dist/node/gemini/index.d.ts +173 -0
  34. package/dist/node/gemini/index.d.ts.map +1 -0
  35. package/dist/node/gemini/index.js +1 -0
  36. package/dist/node/gemini-Bh2U87MY.js +4 -0
  37. package/dist/node/gemini-Bh2U87MY.js.map +1 -0
  38. package/dist/node/gemini-DSaNCxZj.cjs +3 -0
  39. package/dist/node/gemma/index.cjs +1 -0
  40. package/dist/node/gemma/index.d.ts +2 -0
  41. package/dist/node/gemma/index.js +1 -0
  42. package/dist/node/gemma-Dp_AfCUR.js +2 -0
  43. package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
  44. package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
  45. package/dist/node/google/index.cjs +1 -0
  46. package/dist/node/google/index.d.ts +14 -0
  47. package/dist/node/google/index.d.ts.map +1 -0
  48. package/dist/node/google/index.js +2 -0
  49. package/dist/node/google/index.js.map +1 -0
  50. package/dist/node/index-B6PnlDMd.d.ts +82 -0
  51. package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
  52. package/dist/node/index-B7UvPJcI.d.ts +315 -0
  53. package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
  54. package/dist/node/index-BLPOTNb5.d.ts +98 -0
  55. package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
  56. package/dist/node/index-BqixM_XD.d.ts +231 -0
  57. package/dist/node/index-BqixM_XD.d.ts.map +1 -0
  58. package/dist/node/index-C3beaqKO.d.ts +231 -0
  59. package/dist/node/index-C3beaqKO.d.ts.map +1 -0
  60. package/dist/node/index-Cp2XRh9G.d.ts +82 -0
  61. package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
  62. package/dist/node/index-DSv5xruI.d.ts +98 -0
  63. package/dist/node/index-DSv5xruI.d.ts.map +1 -0
  64. package/dist/node/index-w0bV1uaP.d.ts +315 -0
  65. package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
  66. package/dist/node/index.cjs +1 -0
  67. package/dist/node/index.d.ts +8 -0
  68. package/dist/node/index.js +1 -0
  69. package/dist/node/openai/index.cjs +1 -0
  70. package/dist/node/openai/index.d.ts +2 -0
  71. package/dist/node/openai/index.js +1 -0
  72. package/dist/node/openai-CRQjg4xF.js +2 -0
  73. package/dist/node/openai-CRQjg4xF.js.map +1 -0
  74. package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
  75. package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
  76. package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
  77. package/dist/node/openai-xWC6pY7r.cjs +1 -0
  78. package/dist/node/qwen/index.cjs +1 -0
  79. package/dist/node/qwen/index.d.ts +2 -0
  80. package/dist/node/qwen/index.js +1 -0
  81. package/dist/node/qwen-ChUZobTL.js +2 -0
  82. package/dist/node/qwen-ChUZobTL.js.map +1 -0
  83. package/dist/node/qwen-CjT71vSM.cjs +1 -0
  84. package/package.json +157 -0
  85. package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
  86. package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
  87. package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
  88. package/src/anthropic/__tests__/provider.test.ts +1357 -0
  89. package/src/anthropic/__tests__/response-parser.test.ts +326 -0
  90. package/src/anthropic/index.ts +22 -0
  91. package/src/anthropic/message-converter.ts +181 -0
  92. package/src/anthropic/model-catalog-refresh.ts +128 -0
  93. package/src/anthropic/parsers/response-parser.ts +184 -0
  94. package/src/anthropic/provider-definition.ts +93 -0
  95. package/src/anthropic/provider.ts +290 -0
  96. package/src/anthropic/streaming-handler.ts +204 -0
  97. package/src/anthropic/types/api-types.ts +158 -0
  98. package/src/anthropic/types.ts +79 -0
  99. package/src/bytedance/http-client.test.ts +288 -0
  100. package/src/bytedance/http-client.ts +163 -0
  101. package/src/bytedance/index.ts +2 -0
  102. package/src/bytedance/provider.spec.ts +320 -0
  103. package/src/bytedance/provider.ts +171 -0
  104. package/src/bytedance/status-mapper.test.ts +299 -0
  105. package/src/bytedance/status-mapper.ts +141 -0
  106. package/src/bytedance/types.ts +68 -0
  107. package/src/deepseek/defaults.ts +4 -0
  108. package/src/deepseek/index.ts +22 -0
  109. package/src/deepseek/model-catalog-refresh.test.ts +57 -0
  110. package/src/deepseek/model-catalog-refresh.ts +105 -0
  111. package/src/deepseek/model-catalog.ts +55 -0
  112. package/src/deepseek/provider-definition.test.ts +109 -0
  113. package/src/deepseek/provider-definition.ts +132 -0
  114. package/src/deepseek/provider.test.ts +324 -0
  115. package/src/deepseek/provider.ts +298 -0
  116. package/src/deepseek/types.ts +37 -0
  117. package/src/gemini/execution-helpers.ts +233 -0
  118. package/src/gemini/genai-transport.test.ts +208 -0
  119. package/src/gemini/image-operations.test.ts +448 -0
  120. package/src/gemini/image-operations.ts +261 -0
  121. package/src/gemini/index.ts +11 -0
  122. package/src/gemini/message-converter.test.ts +616 -0
  123. package/src/gemini/message-converter.ts +140 -0
  124. package/src/gemini/model-catalog-refresh.test.ts +107 -0
  125. package/src/gemini/model-catalog-refresh.ts +92 -0
  126. package/src/gemini/provider-definition.test.ts +70 -0
  127. package/src/gemini/provider-definition.ts +78 -0
  128. package/src/gemini/provider-extended.test.ts +898 -0
  129. package/src/gemini/provider.spec.ts +216 -0
  130. package/src/gemini/provider.ts +279 -0
  131. package/src/gemini/request-converter.ts +226 -0
  132. package/src/gemini/tool-schema-converter.ts +78 -0
  133. package/src/gemini/types/api-types.ts +235 -0
  134. package/src/gemini/types.ts +121 -0
  135. package/src/gemma/index.ts +5 -0
  136. package/src/gemma/message-factory.ts +38 -0
  137. package/src/gemma/provider-definition.test.ts +43 -0
  138. package/src/gemma/provider-definition.ts +84 -0
  139. package/src/gemma/provider-projection.ts +49 -0
  140. package/src/gemma/provider.test.ts +628 -0
  141. package/src/gemma/provider.ts +308 -0
  142. package/src/gemma/pseudo-command-envelope.ts +58 -0
  143. package/src/gemma/pseudo-tool-call-projector.ts +243 -0
  144. package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
  145. package/src/gemma/pseudo-tool-call-types.ts +31 -0
  146. package/src/gemma/reasoning-projector.test.ts +52 -0
  147. package/src/gemma/reasoning-projector.ts +144 -0
  148. package/src/gemma/streaming-projection.ts +79 -0
  149. package/src/gemma/tool-call-argument-parser.ts +126 -0
  150. package/src/gemma/tool-call-projector.test.ts +227 -0
  151. package/src/gemma/tool-call-projector.ts +264 -0
  152. package/src/gemma/types.ts +27 -0
  153. package/src/google/index.ts +11 -0
  154. package/src/google/provider-compat.test.ts +19 -0
  155. package/src/google/provider-definition.ts +6 -0
  156. package/src/google/provider.ts +10 -0
  157. package/src/google/types.ts +5 -0
  158. package/src/index.ts +9 -0
  159. package/src/openai/adapter.test.ts +494 -0
  160. package/src/openai/adapter.ts +145 -0
  161. package/src/openai/chat-completions-chat.ts +189 -0
  162. package/src/openai/executor-integration.test.ts +206 -0
  163. package/src/openai/index.ts +21 -0
  164. package/src/openai/interfaces/payload-logger.ts +48 -0
  165. package/src/openai/loggers/console-payload-logger.test.ts +173 -0
  166. package/src/openai/loggers/console-payload-logger.ts +94 -0
  167. package/src/openai/loggers/console.ts +9 -0
  168. package/src/openai/loggers/file-payload-logger.test.ts +238 -0
  169. package/src/openai/loggers/file-payload-logger.ts +112 -0
  170. package/src/openai/loggers/file.ts +9 -0
  171. package/src/openai/loggers/index.ts +12 -0
  172. package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
  173. package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
  174. package/src/openai/message-converter.ts +22 -0
  175. package/src/openai/model-catalog-refresh.test.ts +92 -0
  176. package/src/openai/model-catalog-refresh.ts +115 -0
  177. package/src/openai/openai-request-format.ts +92 -0
  178. package/src/openai/parsers/response-parser.test.ts +407 -0
  179. package/src/openai/parsers/response-parser.ts +47 -0
  180. package/src/openai/provider-definition.test.ts +75 -0
  181. package/src/openai/provider-definition.ts +132 -0
  182. package/src/openai/provider.test.ts +1402 -0
  183. package/src/openai/provider.ts +237 -0
  184. package/src/openai/responses-chat.ts +258 -0
  185. package/src/openai/responses-converter.ts +112 -0
  186. package/src/openai/responses-parser.ts +285 -0
  187. package/src/openai/responses-stream-utils.ts +45 -0
  188. package/src/openai/responses-types.ts +195 -0
  189. package/src/openai/streaming/stream-assembler.ts +3 -0
  190. package/src/openai/streaming/stream-handler.test.ts +367 -0
  191. package/src/openai/streaming/stream-handler.ts +119 -0
  192. package/src/openai/types/api-types.ts +112 -0
  193. package/src/openai/types.ts +194 -0
  194. package/src/qwen/defaults.ts +26 -0
  195. package/src/qwen/index.ts +5 -0
  196. package/src/qwen/model-catalog-refresh.test.ts +91 -0
  197. package/src/qwen/model-catalog-refresh.ts +97 -0
  198. package/src/qwen/provider-capabilities.ts +34 -0
  199. package/src/qwen/provider-definition.test.ts +139 -0
  200. package/src/qwen/provider-definition.ts +173 -0
  201. package/src/qwen/provider-streaming-assembly.ts +40 -0
  202. package/src/qwen/provider.test.ts +640 -0
  203. package/src/qwen/provider.ts +293 -0
  204. package/src/qwen/responses-chat.ts +194 -0
  205. package/src/qwen/responses-converter.ts +104 -0
  206. package/src/qwen/responses-parser.ts +299 -0
  207. package/src/qwen/responses-stream-utils.ts +38 -0
  208. package/src/qwen/types.ts +228 -0
  209. package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
  210. package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
  211. package/src/shared/openai-compatible/index.ts +6 -0
  212. package/src/shared/openai-compatible/message-converter.test.ts +111 -0
  213. package/src/shared/openai-compatible/message-converter.ts +84 -0
  214. package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
  215. package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
  216. package/src/shared/openai-compatible/response-parser.test.ts +172 -0
  217. package/src/shared/openai-compatible/response-parser.ts +180 -0
  218. package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
  219. package/src/shared/openai-compatible/stream-assembler.ts +248 -0
  220. package/src/shared/openai-compatible/types.ts +59 -0
@@ -0,0 +1,494 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { OpenAIConversationAdapter } from './adapter';
3
+ import type {
4
+ TUniversalMessage,
5
+ IUserMessage,
6
+ ISystemMessage,
7
+ IAssistantMessage,
8
+ IToolMessage,
9
+ } from '@robota-sdk/agent-core';
10
+
11
+ describe('OpenAIConversationAdapter', () => {
12
+ describe('convertMessage', () => {
13
+ it('should convert user message correctly', () => {
14
+ const userMessage: IUserMessage = {
15
+ id: 'msg-1',
16
+ state: 'complete' as const,
17
+ role: 'user',
18
+ content: 'Hello, how are you?',
19
+ timestamp: new Date(),
20
+ };
21
+
22
+ const result = OpenAIConversationAdapter.convertMessage(userMessage);
23
+
24
+ expect(result).toEqual({
25
+ role: 'user',
26
+ content: 'Hello, how are you?',
27
+ });
28
+ });
29
+
30
+ it('should convert system message correctly', () => {
31
+ const systemMessage: ISystemMessage = {
32
+ id: 'msg-1',
33
+ state: 'complete' as const,
34
+ role: 'system',
35
+ content: 'You are a helpful assistant.',
36
+ timestamp: new Date(),
37
+ };
38
+
39
+ const result = OpenAIConversationAdapter.convertMessage(systemMessage);
40
+
41
+ expect(result).toEqual({
42
+ role: 'system',
43
+ content: 'You are a helpful assistant.',
44
+ });
45
+ });
46
+
47
+ it('should convert regular assistant message with content correctly', () => {
48
+ const assistantMessage: IAssistantMessage = {
49
+ id: 'msg-1',
50
+ state: 'complete' as const,
51
+ role: 'assistant',
52
+ content: 'I can help you with that.',
53
+ timestamp: new Date(),
54
+ };
55
+
56
+ const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
57
+
58
+ expect(result).toEqual({
59
+ role: 'assistant',
60
+ content: 'I can help you with that.',
61
+ });
62
+ });
63
+
64
+ it('should convert assistant message with null content correctly', () => {
65
+ const assistantMessage: IAssistantMessage = {
66
+ id: 'msg-1',
67
+ state: 'complete' as const,
68
+ role: 'assistant',
69
+ content: null,
70
+ timestamp: new Date(),
71
+ };
72
+
73
+ const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
74
+
75
+ expect(result).toEqual({
76
+ role: 'assistant',
77
+ content: null,
78
+ });
79
+ });
80
+
81
+ it('should convert assistant message with empty content correctly', () => {
82
+ const assistantMessage: IAssistantMessage = {
83
+ id: 'msg-1',
84
+ state: 'complete' as const,
85
+ role: 'assistant',
86
+ content: '',
87
+ timestamp: new Date(),
88
+ };
89
+
90
+ const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
91
+
92
+ expect(result).toEqual({
93
+ role: 'assistant',
94
+ content: null, // Empty string is also converted to null
95
+ });
96
+ });
97
+
98
+ it('should convert assistant message with tool calls and null content correctly', () => {
99
+ const assistantMessage: IAssistantMessage = {
100
+ id: 'msg-1',
101
+ state: 'complete' as const,
102
+ role: 'assistant',
103
+ content: null,
104
+ toolCalls: [
105
+ {
106
+ id: 'call_123',
107
+ type: 'function',
108
+ function: {
109
+ name: 'calculate',
110
+ arguments: '{"operation":"add","a":5,"b":3}',
111
+ },
112
+ },
113
+ ],
114
+ timestamp: new Date(),
115
+ };
116
+
117
+ const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
118
+
119
+ expect(result).toEqual({
120
+ role: 'assistant',
121
+ content: null,
122
+ tool_calls: [
123
+ {
124
+ id: 'call_123',
125
+ type: 'function',
126
+ function: {
127
+ name: 'calculate',
128
+ arguments: '{"operation":"add","a":5,"b":3}',
129
+ },
130
+ },
131
+ ],
132
+ });
133
+ });
134
+
135
+ it('should convert assistant message with tool calls and empty content correctly', () => {
136
+ const assistantMessage: IAssistantMessage = {
137
+ id: 'msg-1',
138
+ state: 'complete' as const,
139
+ role: 'assistant',
140
+ content: '',
141
+ toolCalls: [
142
+ {
143
+ id: 'call_456',
144
+ type: 'function',
145
+ function: {
146
+ name: 'getWeather',
147
+ arguments: '{"city":"Seoul"}',
148
+ },
149
+ },
150
+ ],
151
+ timestamp: new Date(),
152
+ };
153
+
154
+ const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
155
+
156
+ expect(result).toEqual({
157
+ role: 'assistant',
158
+ content: null,
159
+ tool_calls: [
160
+ {
161
+ id: 'call_456',
162
+ type: 'function',
163
+ function: {
164
+ name: 'getWeather',
165
+ arguments: '{"city":"Seoul"}',
166
+ },
167
+ },
168
+ ],
169
+ });
170
+ });
171
+
172
+ it('should convert assistant message with tool calls and meaningful content correctly', () => {
173
+ const assistantMessage: IAssistantMessage = {
174
+ id: 'msg-1',
175
+ state: 'complete' as const,
176
+ role: 'assistant',
177
+ content: 'Let me calculate that for you.',
178
+ toolCalls: [
179
+ {
180
+ id: 'call_789',
181
+ type: 'function',
182
+ function: {
183
+ name: 'calculate',
184
+ arguments: '{"operation":"multiply","a":7,"b":8}',
185
+ },
186
+ },
187
+ ],
188
+ timestamp: new Date(),
189
+ };
190
+
191
+ const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
192
+
193
+ expect(result).toEqual({
194
+ role: 'assistant',
195
+ content: 'Let me calculate that for you.',
196
+ tool_calls: [
197
+ {
198
+ id: 'call_789',
199
+ type: 'function',
200
+ function: {
201
+ name: 'calculate',
202
+ arguments: '{"operation":"multiply","a":7,"b":8}',
203
+ },
204
+ },
205
+ ],
206
+ });
207
+ });
208
+
209
+ it('should convert tool message correctly', () => {
210
+ const toolMessage: IToolMessage = {
211
+ id: 'msg-1',
212
+ state: 'complete' as const,
213
+ role: 'tool',
214
+ content: '{"result":8,"operation":"5 + 3 = 8"}',
215
+ toolCallId: 'call_123',
216
+ name: 'calculate',
217
+ timestamp: new Date(),
218
+ };
219
+
220
+ const result = OpenAIConversationAdapter.convertMessage(toolMessage);
221
+
222
+ expect(result).toEqual({
223
+ role: 'tool',
224
+ content: '{"result":8,"operation":"5 + 3 = 8"}',
225
+ tool_call_id: 'call_123',
226
+ });
227
+ });
228
+
229
+ it('should throw error for tool message without toolCallId', () => {
230
+ const toolMessage: IToolMessage = {
231
+ id: 'msg-1',
232
+ state: 'complete' as const,
233
+ role: 'tool',
234
+ content: '{"result":8}',
235
+ toolCallId: '',
236
+ timestamp: new Date(),
237
+ };
238
+
239
+ expect(() => {
240
+ OpenAIConversationAdapter.convertMessage(toolMessage);
241
+ }).toThrow('Tool message missing toolCallId');
242
+ });
243
+ });
244
+
245
+ describe('toOpenAIFormat', () => {
246
+ it('should convert a complete conversation with tool calls correctly', () => {
247
+ const messages: TUniversalMessage[] = [
248
+ {
249
+ id: 'msg-1',
250
+ state: 'complete' as const,
251
+ role: 'system',
252
+ content: 'You are a helpful assistant.',
253
+ timestamp: new Date(),
254
+ },
255
+ {
256
+ id: 'msg-2',
257
+ state: 'complete' as const,
258
+ role: 'user',
259
+ content: 'What is 5 plus 3?',
260
+ timestamp: new Date(),
261
+ },
262
+ {
263
+ id: 'msg-3',
264
+ state: 'complete' as const,
265
+ role: 'assistant',
266
+ content: null,
267
+ toolCalls: [
268
+ {
269
+ id: 'call_123',
270
+ type: 'function',
271
+ function: {
272
+ name: 'calculate',
273
+ arguments: '{"operation":"add","a":5,"b":3}',
274
+ },
275
+ },
276
+ ],
277
+ timestamp: new Date(),
278
+ },
279
+ {
280
+ id: 'msg-4',
281
+ state: 'complete' as const,
282
+ role: 'tool',
283
+ content: '{"result":8,"operation":"5 + 3 = 8"}',
284
+ toolCallId: 'call_123',
285
+ name: 'calculate',
286
+ timestamp: new Date(),
287
+ },
288
+ {
289
+ id: 'msg-5',
290
+ state: 'complete' as const,
291
+ role: 'assistant',
292
+ content: 'The result of 5 plus 3 is 8.',
293
+ timestamp: new Date(),
294
+ },
295
+ ];
296
+
297
+ const result = OpenAIConversationAdapter.toOpenAIFormat(messages);
298
+
299
+ expect(result).toEqual([
300
+ {
301
+ role: 'system',
302
+ content: 'You are a helpful assistant.',
303
+ },
304
+ {
305
+ role: 'user',
306
+ content: 'What is 5 plus 3?',
307
+ },
308
+ {
309
+ role: 'assistant',
310
+ content: null,
311
+ tool_calls: [
312
+ {
313
+ id: 'call_123',
314
+ type: 'function',
315
+ function: {
316
+ name: 'calculate',
317
+ arguments: '{"operation":"add","a":5,"b":3}',
318
+ },
319
+ },
320
+ ],
321
+ },
322
+ {
323
+ role: 'tool',
324
+ content: '{"result":8,"operation":"5 + 3 = 8"}',
325
+ tool_call_id: 'call_123',
326
+ },
327
+ {
328
+ role: 'assistant',
329
+ content: 'The result of 5 plus 3 is 8.',
330
+ },
331
+ ]);
332
+ });
333
+
334
+ it('should filter out tool messages with invalid toolCallId', () => {
335
+ const messages: TUniversalMessage[] = [
336
+ {
337
+ id: 'msg-1',
338
+ state: 'complete' as const,
339
+ role: 'user',
340
+ content: 'Hello',
341
+ timestamp: new Date(),
342
+ },
343
+ {
344
+ id: 'msg-2',
345
+ state: 'complete' as const,
346
+ role: 'tool',
347
+ content: 'Invalid tool message',
348
+ toolCallId: '',
349
+ timestamp: new Date(),
350
+ },
351
+ {
352
+ id: 'msg-3',
353
+ state: 'complete' as const,
354
+ role: 'tool',
355
+ content: 'Another invalid tool message',
356
+ toolCallId: 'unknown',
357
+ timestamp: new Date(),
358
+ },
359
+ {
360
+ id: 'msg-4',
361
+ state: 'complete' as const,
362
+ role: 'assistant',
363
+ content: 'Hi there!',
364
+ timestamp: new Date(),
365
+ },
366
+ ];
367
+
368
+ const result = OpenAIConversationAdapter.toOpenAIFormat(messages);
369
+
370
+ expect(result).toEqual([
371
+ {
372
+ role: 'user',
373
+ content: 'Hello',
374
+ },
375
+ {
376
+ role: 'assistant',
377
+ content: 'Hi there!',
378
+ },
379
+ ]);
380
+ });
381
+
382
+ it('should handle the tool execution loop prevention scenario correctly', () => {
383
+ const messages: TUniversalMessage[] = [
384
+ {
385
+ id: 'msg-1',
386
+ state: 'complete' as const,
387
+ role: 'system',
388
+ content: 'You are a helpful assistant.',
389
+ timestamp: new Date(),
390
+ },
391
+ {
392
+ id: 'msg-2',
393
+ state: 'complete' as const,
394
+ role: 'user',
395
+ content: 'What is 5 plus 3?',
396
+ timestamp: new Date(),
397
+ },
398
+ {
399
+ id: 'msg-3',
400
+ state: 'complete' as const,
401
+ role: 'assistant',
402
+ content: null,
403
+ toolCalls: [
404
+ {
405
+ id: 'call_calculate_123',
406
+ type: 'function',
407
+ function: {
408
+ name: 'calculate',
409
+ arguments: '{"operation":"add","a":5,"b":3}',
410
+ },
411
+ },
412
+ ],
413
+ timestamp: new Date(),
414
+ },
415
+ {
416
+ id: 'msg-4',
417
+ state: 'complete' as const,
418
+ role: 'tool',
419
+ content: '{"result":8,"operation":"5 + 3 = 8"}',
420
+ toolCallId: 'call_calculate_123',
421
+ name: 'calculate',
422
+ timestamp: new Date(),
423
+ },
424
+ ];
425
+
426
+ const result = OpenAIConversationAdapter.toOpenAIFormat(messages);
427
+
428
+ expect(result[2]).toEqual({
429
+ role: 'assistant',
430
+ content: null,
431
+ tool_calls: [
432
+ {
433
+ id: 'call_calculate_123',
434
+ type: 'function',
435
+ function: {
436
+ name: 'calculate',
437
+ arguments: '{"operation":"add","a":5,"b":3}',
438
+ },
439
+ },
440
+ ],
441
+ });
442
+
443
+ expect(result[3]).toEqual({
444
+ role: 'tool',
445
+ content: '{"result":8,"operation":"5 + 3 = 8"}',
446
+ tool_call_id: 'call_calculate_123',
447
+ });
448
+ });
449
+
450
+ it('should demonstrate the difference between correct and incorrect content handling', () => {
451
+ const correctAssistantMessage: IAssistantMessage = {
452
+ id: 'msg-1',
453
+ state: 'complete' as const,
454
+ role: 'assistant',
455
+ content: null,
456
+ toolCalls: [
457
+ {
458
+ id: 'call_123',
459
+ type: 'function',
460
+ function: {
461
+ name: 'calculate',
462
+ arguments: '{"a":5,"b":3}',
463
+ },
464
+ },
465
+ ],
466
+ timestamp: new Date(),
467
+ };
468
+
469
+ const incorrectAssistantMessage: IAssistantMessage = {
470
+ id: 'msg-2',
471
+ state: 'complete' as const,
472
+ role: 'assistant',
473
+ content: '',
474
+ toolCalls: [
475
+ {
476
+ id: 'call_123',
477
+ type: 'function',
478
+ function: {
479
+ name: 'calculate',
480
+ arguments: '{"a":5,"b":3}',
481
+ },
482
+ },
483
+ ],
484
+ timestamp: new Date(),
485
+ };
486
+
487
+ const correctResult = OpenAIConversationAdapter.convertMessage(correctAssistantMessage);
488
+ const incorrectResult = OpenAIConversationAdapter.convertMessage(incorrectAssistantMessage);
489
+
490
+ expect(correctResult.content).toBe(null);
491
+ expect(incorrectResult.content).toBe(null);
492
+ });
493
+ });
494
+ });
@@ -0,0 +1,145 @@
1
+ import OpenAI from 'openai';
2
+ import type { TUniversalMessage, IAssistantMessage } from '@robota-sdk/agent-core';
3
+
4
+ /**
5
+ * OpenAI Conversation Adapter
6
+ *
7
+ * Converts between TUniversalMessage format and OpenAI native types.
8
+ * Provides bidirectional conversion for seamless integration.
9
+ *
10
+ * @public
11
+ */
12
+ export class OpenAIConversationAdapter {
13
+ /**
14
+ * Filter messages for OpenAI compatibility
15
+ *
16
+ * OpenAI has specific requirements:
17
+ * - Tool messages must have valid toolCallId
18
+ * - Messages must be in proper sequence
19
+ * - Tool messages without toolCallId should be excluded
20
+ */
21
+ static filterMessagesForOpenAI(messages: TUniversalMessage[]): TUniversalMessage[] {
22
+ return messages.filter((msg) => {
23
+ // Always include user, assistant, and system messages
24
+ if (msg.role === 'user' || msg.role === 'assistant' || msg.role === 'system') {
25
+ return true;
26
+ }
27
+
28
+ // For tool messages, only include if they have a valid toolCallId
29
+ if (msg.role === 'tool') {
30
+ // Must have toolCallId and it must not be empty or 'unknown'
31
+ return !!(msg.toolCallId && msg.toolCallId.trim() !== '' && msg.toolCallId !== 'unknown');
32
+ }
33
+
34
+ return false;
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Convert TUniversalMessage array to OpenAI message format
40
+ * Now properly handles tool messages for OpenAI's tool calling feature
41
+ */
42
+ static toOpenAIFormat(messages: TUniversalMessage[]): OpenAI.Chat.ChatCompletionMessageParam[] {
43
+ // First filter messages for OpenAI compatibility
44
+ const filteredMessages = this.filterMessagesForOpenAI(messages);
45
+ return filteredMessages.map((msg) => this.convertMessage(msg));
46
+ }
47
+
48
+ /**
49
+ * Convert a single TUniversalMessage to OpenAI format
50
+ * Handles all message types including tool messages
51
+ */
52
+ static convertMessage(msg: TUniversalMessage): OpenAI.Chat.ChatCompletionMessageParam {
53
+ const messageRole = msg.role;
54
+
55
+ if (messageRole === 'user') {
56
+ return {
57
+ role: 'user',
58
+ content: msg.content,
59
+ };
60
+ }
61
+
62
+ if (messageRole === 'assistant') {
63
+ const assistantMsg = msg as IAssistantMessage;
64
+
65
+ // Handle tool_calls format
66
+ if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
67
+ const result: OpenAI.Chat.ChatCompletionAssistantMessageParam = {
68
+ role: 'assistant',
69
+ // CRITICAL: OpenAI API requires content to be null (not empty string) when tool_calls are present
70
+ // VERIFIED: 2024-12 - This prevents "400 Bad Request" errors from OpenAI API
71
+ // DO NOT CHANGE without testing against actual OpenAI API
72
+ content: assistantMsg.content === '' ? null : assistantMsg.content || null,
73
+ tool_calls: assistantMsg.toolCalls.map((toolCall) => ({
74
+ id: toolCall.id,
75
+ type: 'function',
76
+ function: {
77
+ name: toolCall.function.name,
78
+ arguments: toolCall.function.arguments,
79
+ },
80
+ })),
81
+ };
82
+ return result;
83
+ }
84
+
85
+ // Regular assistant message (without tool calls)
86
+ // VERIFIED: OpenAI accepts both null and string content for regular messages
87
+ // We preserve null when content is null or empty string for API consistency
88
+ return {
89
+ role: 'assistant',
90
+ content:
91
+ assistantMsg.content === null
92
+ ? null
93
+ : assistantMsg.content === ''
94
+ ? null
95
+ : assistantMsg.content || '',
96
+ };
97
+ }
98
+
99
+ if (messageRole === 'system') {
100
+ return {
101
+ role: 'system',
102
+ content: msg.content,
103
+ };
104
+ }
105
+
106
+ // Handle tool messages for OpenAI tool calling
107
+ if (messageRole === 'tool') {
108
+ if (!msg.toolCallId || msg.toolCallId.trim() === '') {
109
+ throw new Error(`Tool message missing toolCallId: ${JSON.stringify(msg)}`);
110
+ }
111
+
112
+ const result: OpenAI.Chat.ChatCompletionToolMessageParam = {
113
+ role: 'tool',
114
+ content: msg.content,
115
+ tool_call_id: msg.toolCallId,
116
+ };
117
+ return result;
118
+ }
119
+
120
+ const exhaustive: never = messageRole;
121
+ throw new Error(`Unsupported message role: ${exhaustive}`);
122
+ }
123
+
124
+ /**
125
+ * Add system prompt to message array if needed
126
+ */
127
+ static addSystemPromptIfNeeded(
128
+ messages: OpenAI.Chat.ChatCompletionMessageParam[],
129
+ systemPrompt?: string,
130
+ ): OpenAI.Chat.ChatCompletionMessageParam[] {
131
+ if (!systemPrompt) {
132
+ return messages;
133
+ }
134
+
135
+ // Check if system message already exists
136
+ const hasSystemMessage = messages.some((msg) => msg.role === 'system');
137
+
138
+ if (hasSystemMessage) {
139
+ return messages;
140
+ }
141
+
142
+ // Add system prompt at the beginning
143
+ return [{ role: 'system', content: systemPrompt }, ...messages];
144
+ }
145
+ }