@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,266 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type OpenAI from 'openai';
3
+ import { assembleOpenAICompatibleStream } from './index';
4
+
5
+ async function* asyncIterableFrom<T>(items: T[]): AsyncIterable<T> {
6
+ for (const item of items) {
7
+ yield item;
8
+ }
9
+ }
10
+
11
+ function createHangingChunkStream(
12
+ onReturn: () => void,
13
+ ): AsyncIterable<OpenAI.Chat.ChatCompletionChunk> {
14
+ return {
15
+ [Symbol.asyncIterator](): AsyncIterator<OpenAI.Chat.ChatCompletionChunk> {
16
+ return {
17
+ next: () => new Promise<IteratorResult<OpenAI.Chat.ChatCompletionChunk>>(() => {}),
18
+ return: () => {
19
+ onReturn();
20
+ return Promise.resolve({
21
+ done: true,
22
+ value: createChunk(''),
23
+ });
24
+ },
25
+ };
26
+ },
27
+ };
28
+ }
29
+
30
+ function createChunk(
31
+ content: string,
32
+ finishReason: OpenAI.Chat.ChatCompletionChunk.Choice['finish_reason'] = null,
33
+ ): OpenAI.Chat.ChatCompletionChunk {
34
+ return {
35
+ id: 'chunk-1',
36
+ object: 'chat.completion.chunk',
37
+ created: 1,
38
+ model: 'local-model',
39
+ choices: [
40
+ {
41
+ index: 0,
42
+ delta: { content },
43
+ finish_reason: finishReason,
44
+ logprobs: null,
45
+ },
46
+ ],
47
+ };
48
+ }
49
+
50
+ describe('assembleOpenAICompatibleStream', () => {
51
+ it('assembles text deltas and emits projected text deltas', async () => {
52
+ const onTextDelta = vi.fn();
53
+ const projector = vi.fn((text: string) => text.replaceAll('[hidden]', ''));
54
+
55
+ const result = await assembleOpenAICompatibleStream({
56
+ stream: asyncIterableFrom([
57
+ createChunk('Hello '),
58
+ createChunk('[hidden]world'),
59
+ createChunk('', 'stop'),
60
+ ]),
61
+ onTextDelta,
62
+ textProjector: projector,
63
+ });
64
+
65
+ expect(result.role).toBe('assistant');
66
+ expect(result.content).toBe('Hello world');
67
+ expect(result.metadata?.['model']).toBe('local-model');
68
+ expect(result.metadata?.['finishReason']).toBe('stop');
69
+ expect(onTextDelta).toHaveBeenCalledTimes(2);
70
+ expect(onTextDelta).toHaveBeenNthCalledWith(1, 'Hello ');
71
+ expect(onTextDelta).toHaveBeenNthCalledWith(2, 'world');
72
+ });
73
+
74
+ it('assembles streamed tool call deltas by index', async () => {
75
+ const stream = asyncIterableFrom<OpenAI.Chat.ChatCompletionChunk>([
76
+ {
77
+ id: 'chunk-1',
78
+ object: 'chat.completion.chunk',
79
+ created: 1,
80
+ model: 'local-model',
81
+ choices: [
82
+ {
83
+ index: 0,
84
+ delta: {
85
+ tool_calls: [
86
+ {
87
+ index: 0,
88
+ id: 'call-1',
89
+ type: 'function',
90
+ function: { name: 'search', arguments: '{"q"' },
91
+ },
92
+ ],
93
+ },
94
+ finish_reason: null,
95
+ logprobs: null,
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ id: 'chunk-2',
101
+ object: 'chat.completion.chunk',
102
+ created: 1,
103
+ model: 'local-model',
104
+ choices: [
105
+ {
106
+ index: 0,
107
+ delta: {
108
+ tool_calls: [
109
+ {
110
+ index: 0,
111
+ function: { arguments: ':"docs"}' },
112
+ },
113
+ ],
114
+ },
115
+ finish_reason: 'tool_calls',
116
+ logprobs: null,
117
+ },
118
+ ],
119
+ },
120
+ ]);
121
+
122
+ const result = await assembleOpenAICompatibleStream({ stream });
123
+
124
+ expect(result.role).toBe('assistant');
125
+ if (result.role !== 'assistant') throw new Error('Expected assistant message');
126
+ expect(result.toolCalls).toEqual([
127
+ {
128
+ id: 'call-1',
129
+ type: 'function',
130
+ function: { name: 'search', arguments: '{"q":"docs"}' },
131
+ },
132
+ ]);
133
+ });
134
+
135
+ it('passes streamed native tool calls through so core can return a normal tool-result error', async () => {
136
+ const stream = asyncIterableFrom<OpenAI.Chat.ChatCompletionChunk>([
137
+ {
138
+ id: 'chunk-1',
139
+ object: 'chat.completion.chunk',
140
+ created: 1,
141
+ model: 'local-model',
142
+ choices: [
143
+ {
144
+ index: 0,
145
+ delta: {
146
+ tool_calls: [
147
+ {
148
+ index: 0,
149
+ id: 'call-1',
150
+ type: 'function',
151
+ function: { name: 'UndeclaredTool', arguments: '{}' },
152
+ },
153
+ ],
154
+ },
155
+ finish_reason: 'tool_calls',
156
+ logprobs: null,
157
+ },
158
+ ],
159
+ },
160
+ ]);
161
+
162
+ const result = await assembleOpenAICompatibleStream({ stream });
163
+
164
+ if (result.role !== 'assistant') throw new Error('Expected assistant message');
165
+ expect(result.toolCalls).toEqual([
166
+ {
167
+ id: 'call-1',
168
+ type: 'function',
169
+ function: { name: 'UndeclaredTool', arguments: '{}' },
170
+ },
171
+ ]);
172
+ });
173
+
174
+ it('applies an injected provider-owned text tool-call projector before text deltas', async () => {
175
+ const onTextDelta = vi.fn();
176
+ const projector = {
177
+ project: vi.fn((delta: string) => {
178
+ if (delta === 'provider native payload') {
179
+ return {
180
+ visibleText: '',
181
+ toolCalls: [
182
+ {
183
+ id: 'projected-call-1',
184
+ type: 'function' as const,
185
+ function: { name: 'InjectedTool', arguments: '{"value":"ok"}' },
186
+ },
187
+ ],
188
+ removedToolCallText: true,
189
+ rawToolCallText: delta,
190
+ };
191
+ }
192
+
193
+ return {
194
+ visibleText: delta,
195
+ toolCalls: [],
196
+ removedToolCallText: false,
197
+ };
198
+ }),
199
+ flush: vi.fn(() => ({
200
+ visibleText: '',
201
+ toolCalls: [],
202
+ removedToolCallText: false,
203
+ })),
204
+ };
205
+
206
+ const result = await assembleOpenAICompatibleStream({
207
+ stream: asyncIterableFrom([
208
+ createChunk('provider native payload'),
209
+ createChunk('visible', 'tool_calls'),
210
+ ]),
211
+ onTextDelta,
212
+ toolCallTextProjector: projector,
213
+ });
214
+
215
+ expect(onTextDelta).toHaveBeenCalledOnce();
216
+ expect(onTextDelta).toHaveBeenCalledWith('visible');
217
+ expect(result.content).toBe('visible');
218
+ expect(result.metadata?.['toolCallTextProjected']).toBe(true);
219
+ expect(result.metadata?.['rawToolCallText']).toBe('provider native payload');
220
+ if (result.role !== 'assistant') throw new Error('Expected assistant message');
221
+ expect(result.toolCalls).toEqual([
222
+ {
223
+ id: 'projected-call-1',
224
+ type: 'function',
225
+ function: { name: 'InjectedTool', arguments: '{"value":"ok"}' },
226
+ },
227
+ ]);
228
+ });
229
+
230
+ it('flushes projector-held text after the stream ends', async () => {
231
+ const onTextDelta = vi.fn();
232
+
233
+ const result = await assembleOpenAICompatibleStream({
234
+ stream: asyncIterableFrom([createChunk('Visible'), createChunk('', 'stop')]),
235
+ onTextDelta,
236
+ textProjector: (text) => text.slice(0, -1),
237
+ textProjectorFlush: () => 'e',
238
+ });
239
+
240
+ expect(result.content).toBe('Visible');
241
+ expect(onTextDelta).toHaveBeenNthCalledWith(1, 'Visibl');
242
+ expect(onTextDelta).toHaveBeenNthCalledWith(2, 'e');
243
+ });
244
+
245
+ it('settles when aborted while awaiting the next stream chunk', async () => {
246
+ const controller = new AbortController();
247
+ let returned = false;
248
+ const resultPromise = assembleOpenAICompatibleStream({
249
+ stream: createHangingChunkStream(() => {
250
+ returned = true;
251
+ }),
252
+ signal: controller.signal,
253
+ });
254
+
255
+ setTimeout(() => controller.abort(), 0);
256
+ const result = await Promise.race([
257
+ resultPromise,
258
+ new Promise<never>((_resolve, reject) =>
259
+ setTimeout(() => reject(new Error('stream abort did not settle')), 100),
260
+ ),
261
+ ]);
262
+
263
+ expect(result.content).toBe('');
264
+ expect(returned).toBe(true);
265
+ });
266
+ });
@@ -0,0 +1,248 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type OpenAI from 'openai';
3
+ import type { IToolCall, TUniversalMessage } from '@robota-sdk/agent-core';
4
+ import type {
5
+ IOpenAICompatibleToolCallTextProjection,
6
+ IOpenAICompatibleStreamAssemblyOptions,
7
+ TOpenAICompatibleTextProjector,
8
+ } from './types';
9
+
10
+ interface IToolCallPart {
11
+ id: string;
12
+ name: string;
13
+ arguments: string;
14
+ }
15
+
16
+ interface IAssemblyState {
17
+ textParts: string[];
18
+ toolCallParts: Map<number, IToolCallPart>;
19
+ projectedToolCalls: IToolCall[];
20
+ rawToolCallTextParts: string[];
21
+ toolCallTextProjected: boolean;
22
+ model: string;
23
+ finishReason: string | null;
24
+ }
25
+
26
+ export async function assembleOpenAICompatibleStream(
27
+ options: IOpenAICompatibleStreamAssemblyOptions,
28
+ ): Promise<TUniversalMessage> {
29
+ const state: IAssemblyState = {
30
+ textParts: [],
31
+ toolCallParts: new Map(),
32
+ projectedToolCalls: [],
33
+ rawToolCallTextParts: [],
34
+ toolCallTextProjected: false,
35
+ model: '',
36
+ finishReason: null,
37
+ };
38
+
39
+ for await (const chunk of streamWithAbort(options.stream, options.signal)) {
40
+ applyChunk(state, chunk, options);
41
+ }
42
+ applyProjectedTextFlush(state, options);
43
+
44
+ return buildMessage(state, options.metadata);
45
+ }
46
+
47
+ function applyChunk(
48
+ state: IAssemblyState,
49
+ chunk: OpenAI.Chat.ChatCompletionChunk,
50
+ options: IOpenAICompatibleStreamAssemblyOptions,
51
+ ): void {
52
+ if (chunk.model) {
53
+ state.model = chunk.model;
54
+ }
55
+
56
+ const choice = chunk.choices?.[0];
57
+ if (!choice) {
58
+ return;
59
+ }
60
+
61
+ state.finishReason = choice.finish_reason ?? state.finishReason;
62
+ applyTextDelta(state, choice.delta.content, options);
63
+ applyToolCallDeltas(state, choice.delta.tool_calls ?? []);
64
+ }
65
+
66
+ function applyTextDelta(
67
+ state: IAssemblyState,
68
+ content: string | null | undefined,
69
+ options: IOpenAICompatibleStreamAssemblyOptions,
70
+ ): void {
71
+ if (!content) {
72
+ return;
73
+ }
74
+
75
+ const toolProjection = projectToolCallText(content, options.toolCallTextProjector);
76
+ applyToolCallTextProjection(state, toolProjection);
77
+ const visibleContent = projectText(toolProjection.visibleText, options.textProjector);
78
+ if (visibleContent.length === 0) {
79
+ return;
80
+ }
81
+
82
+ state.textParts.push(visibleContent);
83
+ options.onTextDelta?.(visibleContent);
84
+ }
85
+
86
+ function projectText(text: string, projector?: TOpenAICompatibleTextProjector): string {
87
+ return projector ? projector(text) : text;
88
+ }
89
+
90
+ function applyProjectedTextFlush(
91
+ state: IAssemblyState,
92
+ options: IOpenAICompatibleStreamAssemblyOptions,
93
+ ): void {
94
+ const toolProjection = options.toolCallTextProjector?.flush();
95
+ if (toolProjection) {
96
+ applyToolCallTextProjection(state, toolProjection);
97
+ const visibleToolText = projectText(toolProjection.visibleText, options.textProjector);
98
+ if (visibleToolText.length > 0) {
99
+ state.textParts.push(visibleToolText);
100
+ options.onTextDelta?.(visibleToolText);
101
+ }
102
+ }
103
+
104
+ const visibleContent = options.textProjectorFlush?.();
105
+ if (!visibleContent) {
106
+ return;
107
+ }
108
+
109
+ state.textParts.push(visibleContent);
110
+ options.onTextDelta?.(visibleContent);
111
+ }
112
+
113
+ function projectToolCallText(
114
+ text: string,
115
+ projector: IOpenAICompatibleStreamAssemblyOptions['toolCallTextProjector'],
116
+ ): IOpenAICompatibleToolCallTextProjection {
117
+ return (
118
+ projector?.project(text) ?? {
119
+ visibleText: text,
120
+ toolCalls: [],
121
+ removedToolCallText: false,
122
+ }
123
+ );
124
+ }
125
+
126
+ function applyToolCallTextProjection(
127
+ state: IAssemblyState,
128
+ projection: IOpenAICompatibleToolCallTextProjection,
129
+ ): void {
130
+ if (projection.toolCalls.length > 0) {
131
+ state.projectedToolCalls.push(...projection.toolCalls);
132
+ }
133
+ if (projection.removedToolCallText) {
134
+ state.toolCallTextProjected = true;
135
+ }
136
+ if (projection.rawToolCallText && projection.rawToolCallText.length > 0) {
137
+ state.rawToolCallTextParts.push(projection.rawToolCallText);
138
+ }
139
+ }
140
+
141
+ function applyToolCallDeltas(
142
+ state: IAssemblyState,
143
+ deltas: OpenAI.Chat.ChatCompletionChunk.Choice.Delta.ToolCall[],
144
+ ): void {
145
+ for (const delta of deltas) {
146
+ const index = delta.index ?? 0;
147
+ const current = state.toolCallParts.get(index) ?? { id: '', name: '', arguments: '' };
148
+ const nextName = delta.function?.name ?? current.name;
149
+ state.toolCallParts.set(index, {
150
+ id: delta.id ?? current.id,
151
+ name: nextName,
152
+ arguments: current.arguments + (delta.function?.arguments ?? ''),
153
+ });
154
+ }
155
+ }
156
+
157
+ function buildMessage(
158
+ state: IAssemblyState,
159
+ metadata: Record<string, string | number | boolean> = {},
160
+ ): TUniversalMessage {
161
+ const resultMetadata: NonNullable<TUniversalMessage['metadata']> = { ...metadata };
162
+ if (state.model) {
163
+ resultMetadata['model'] = state.model;
164
+ }
165
+ if (state.finishReason) {
166
+ resultMetadata['finishReason'] = state.finishReason;
167
+ }
168
+ if (state.toolCallTextProjected) {
169
+ resultMetadata['toolCallTextProjected'] = true;
170
+ }
171
+ if (state.rawToolCallTextParts.length > 0) {
172
+ resultMetadata['rawToolCallText'] = state.rawToolCallTextParts.join('');
173
+ }
174
+
175
+ const toolCalls = buildAllToolCalls(state);
176
+
177
+ return {
178
+ id: randomUUID(),
179
+ role: 'assistant',
180
+ content: state.textParts.join(''),
181
+ state: 'complete',
182
+ timestamp: new Date(),
183
+ ...(toolCalls.length > 0 && { toolCalls }),
184
+ ...(Object.keys(resultMetadata).length > 0 && { metadata: resultMetadata }),
185
+ };
186
+ }
187
+
188
+ function buildAllToolCalls(state: IAssemblyState): IToolCall[] {
189
+ return [...buildToolCalls(state.toolCallParts), ...state.projectedToolCalls];
190
+ }
191
+
192
+ function buildToolCalls(toolCallParts: Map<number, IToolCallPart>): IToolCall[] {
193
+ return Array.from(toolCallParts.entries())
194
+ .sort(([left], [right]) => left - right)
195
+ .map(([index, toolCall]) => ({
196
+ id: toolCall.id || `call_${index}`,
197
+ type: 'function',
198
+ function: {
199
+ name: toolCall.name,
200
+ arguments: toolCall.arguments || '{}',
201
+ },
202
+ }));
203
+ }
204
+
205
+ async function* streamWithAbort<T>(
206
+ source: AsyncIterable<T>,
207
+ signal?: AbortSignal,
208
+ ): AsyncGenerator<T> {
209
+ const iterator = source[Symbol.asyncIterator]();
210
+ try {
211
+ while (!signal?.aborted) {
212
+ const item = await nextStreamItem(iterator, signal);
213
+ if (item.done) break;
214
+ await yieldToMacrotask(signal);
215
+ if (signal?.aborted) break;
216
+ yield item.value;
217
+ }
218
+ } finally {
219
+ if (signal?.aborted) {
220
+ await iterator.return?.();
221
+ }
222
+ }
223
+ }
224
+
225
+ async function nextStreamItem<T>(
226
+ iterator: AsyncIterator<T>,
227
+ signal?: AbortSignal,
228
+ ): Promise<IteratorResult<T>> {
229
+ if (!signal) return iterator.next();
230
+ if (signal.aborted) return { done: true, value: undefined as T };
231
+
232
+ let abortListener: (() => void) | undefined;
233
+ const aborted = new Promise<IteratorResult<T>>((resolve) => {
234
+ abortListener = (): void => resolve({ done: true, value: undefined as T });
235
+ signal.addEventListener('abort', abortListener, { once: true });
236
+ });
237
+
238
+ try {
239
+ return await Promise.race([iterator.next(), aborted]);
240
+ } finally {
241
+ if (abortListener) signal.removeEventListener('abort', abortListener);
242
+ }
243
+ }
244
+
245
+ async function yieldToMacrotask(signal?: AbortSignal): Promise<void> {
246
+ if (signal?.aborted) return;
247
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
248
+ }
@@ -0,0 +1,59 @@
1
+ import type OpenAI from 'openai';
2
+ import type { IToolCall, TTextDeltaCallback } from '@robota-sdk/agent-core';
3
+
4
+ export interface IOpenAICompatibleChatRequestParams {
5
+ model: string;
6
+ messages: OpenAI.Chat.ChatCompletionMessageParam[];
7
+ temperature?: number | undefined;
8
+ max_tokens?: number | undefined;
9
+ tools?: OpenAI.Chat.ChatCompletionTool[] | undefined;
10
+ tool_choice?: 'auto' | 'none' | OpenAI.Chat.ChatCompletionNamedToolChoice | undefined;
11
+ stream?: boolean | undefined;
12
+ }
13
+
14
+ export interface IOpenAICompatibleStreamRequestParams
15
+ extends Omit<IOpenAICompatibleChatRequestParams, 'stream'> {
16
+ stream: true;
17
+ }
18
+
19
+ export interface IOpenAICompatibleError {
20
+ message: string;
21
+ type?: string;
22
+ param?: string | null;
23
+ code?: string | null;
24
+ }
25
+
26
+ export interface IOpenAICompatibleLogData {
27
+ model: string;
28
+ messagesCount: number;
29
+ hasTools: boolean;
30
+ temperature?: number | undefined;
31
+ maxTokens?: number | undefined;
32
+ timestamp: string;
33
+ requestId?: string | undefined;
34
+ }
35
+
36
+ export type TOpenAICompatibleTextProjector = (text: string) => string;
37
+ export type TOpenAICompatibleTextProjectorFlush = () => string;
38
+
39
+ export interface IOpenAICompatibleToolCallTextProjection {
40
+ visibleText: string;
41
+ toolCalls: IToolCall[];
42
+ removedToolCallText: boolean;
43
+ rawToolCallText?: string | undefined;
44
+ }
45
+
46
+ export interface IOpenAICompatibleToolCallTextProjector {
47
+ project(text: string): IOpenAICompatibleToolCallTextProjection;
48
+ flush(): IOpenAICompatibleToolCallTextProjection;
49
+ }
50
+
51
+ export interface IOpenAICompatibleStreamAssemblyOptions {
52
+ stream: AsyncIterable<OpenAI.Chat.ChatCompletionChunk>;
53
+ onTextDelta?: TTextDeltaCallback;
54
+ signal?: AbortSignal;
55
+ textProjector?: TOpenAICompatibleTextProjector;
56
+ textProjectorFlush?: TOpenAICompatibleTextProjectorFlush;
57
+ toolCallTextProjector?: IOpenAICompatibleToolCallTextProjector;
58
+ metadata?: Record<string, string | number | boolean>;
59
+ }