@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,299 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { IToolCall, TTextDeltaCallback, TUniversalMessage } from '@robota-sdk/agent-core';
3
+ import type {
4
+ IQwenResponsesFunctionCallOutputItem,
5
+ IQwenResponsesErrorEvent,
6
+ IQwenResponsesResponse,
7
+ IQwenResponsesUsage,
8
+ TQwenBuiltInWebToolName,
9
+ TQwenResponsesOutputItem,
10
+ TQwenResponsesStreamEvent,
11
+ } from './types';
12
+ import { streamWithAbort } from './responses-stream-utils';
13
+
14
+ interface IQwenResponsesParseOptions {
15
+ enabledBuiltInTools: readonly TQwenBuiltInWebToolName[];
16
+ }
17
+
18
+ interface IQwenResponsesStreamAssemblyOptions extends IQwenResponsesParseOptions {
19
+ stream: AsyncIterable<TQwenResponsesStreamEvent>;
20
+ onTextDelta?: TTextDeltaCallback;
21
+ signal?: AbortSignal;
22
+ }
23
+
24
+ interface IQwenProviderToolUsage {
25
+ webSearchCalls: number;
26
+ webExtractorCalls: number;
27
+ unsupportedToolTypes: Set<string>;
28
+ }
29
+
30
+ interface IQwenResponsesStreamState {
31
+ textParts: string[];
32
+ toolCalls: IToolCall[];
33
+ completedResponse?: IQwenResponsesResponse;
34
+ usage: IQwenProviderToolUsage;
35
+ }
36
+
37
+ export function parseQwenResponsesResponse(
38
+ response: IQwenResponsesResponse,
39
+ options: IQwenResponsesParseOptions,
40
+ ): TUniversalMessage {
41
+ const output = response.output ?? [];
42
+ const usage = collectProviderToolUsage(output, response.usage);
43
+ const toolCalls = extractFunctionToolCalls(output);
44
+ const content = response.output_text ?? extractMessageText(output);
45
+
46
+ return buildAssistantMessage({
47
+ content,
48
+ toolCalls,
49
+ response,
50
+ usage,
51
+ enabledBuiltInTools: options.enabledBuiltInTools,
52
+ });
53
+ }
54
+
55
+ export async function assembleQwenResponsesStream(
56
+ options: IQwenResponsesStreamAssemblyOptions,
57
+ ): Promise<TUniversalMessage> {
58
+ const state: IQwenResponsesStreamState = {
59
+ textParts: [],
60
+ toolCalls: [],
61
+ usage: createEmptyToolUsage(),
62
+ };
63
+
64
+ for await (const event of streamWithAbort(options.stream, options.signal)) {
65
+ applyStreamEvent(state, event, options.onTextDelta);
66
+ }
67
+
68
+ if (state.completedResponse !== undefined) {
69
+ return buildMessageFromCompletedResponse(state, options.enabledBuiltInTools);
70
+ }
71
+
72
+ return buildAssistantMessage({
73
+ content: state.textParts.join(''),
74
+ toolCalls: state.toolCalls,
75
+ usage: state.usage,
76
+ enabledBuiltInTools: options.enabledBuiltInTools,
77
+ });
78
+ }
79
+
80
+ function applyStreamEvent(
81
+ state: IQwenResponsesStreamState,
82
+ event: TQwenResponsesStreamEvent,
83
+ onTextDelta: TTextDeltaCallback | undefined,
84
+ ): void {
85
+ if (event.type === 'response.output_text.delta') {
86
+ state.textParts.push(event.delta);
87
+ onTextDelta?.(event.delta);
88
+ return;
89
+ }
90
+
91
+ if (event.type === 'response.completed') {
92
+ state.completedResponse = event.response;
93
+ mergeToolUsage(
94
+ state.usage,
95
+ collectProviderToolUsage(event.response.output ?? [], event.response.usage),
96
+ );
97
+ return;
98
+ }
99
+
100
+ if (event.type === 'response.output_item.done') {
101
+ applyOutputItem(state, event.item);
102
+ return;
103
+ }
104
+
105
+ if (event.type === 'response.web_search_call.completed') {
106
+ state.usage.webSearchCalls += 1;
107
+ return;
108
+ }
109
+
110
+ if (event.type === 'response.error' || event.type === 'response.failed') {
111
+ throw new Error(`Qwen Responses API failed: ${extractErrorMessage(event)}`);
112
+ }
113
+ }
114
+
115
+ function buildMessageFromCompletedResponse(
116
+ state: IQwenResponsesStreamState,
117
+ enabledBuiltInTools: readonly TQwenBuiltInWebToolName[],
118
+ ): TUniversalMessage {
119
+ const response = state.completedResponse;
120
+ if (response === undefined) {
121
+ throw new Error('Qwen Responses stream completed without response metadata');
122
+ }
123
+
124
+ const output = response.output ?? [];
125
+ const responseToolCalls = extractFunctionToolCalls(output);
126
+ const content =
127
+ state.textParts.length > 0
128
+ ? state.textParts.join('')
129
+ : (response.output_text ?? extractMessageText(output));
130
+ const usage = collectProviderToolUsage(output, response.usage);
131
+ mergeToolUsage(usage, state.usage);
132
+
133
+ return buildAssistantMessage({
134
+ content,
135
+ toolCalls: responseToolCalls.length > 0 ? responseToolCalls : state.toolCalls,
136
+ response,
137
+ usage,
138
+ enabledBuiltInTools,
139
+ });
140
+ }
141
+
142
+ function applyOutputItem(state: IQwenResponsesStreamState, item: TQwenResponsesOutputItem): void {
143
+ if (item.type === 'function_call') {
144
+ if (isFunctionCallOutputItem(item)) {
145
+ state.toolCalls.push(convertFunctionCall(item));
146
+ }
147
+ return;
148
+ }
149
+ mergeToolUsage(state.usage, collectProviderToolUsage([item], undefined));
150
+ }
151
+
152
+ function buildAssistantMessage(input: {
153
+ content: string;
154
+ toolCalls: IToolCall[];
155
+ response?: IQwenResponsesResponse;
156
+ usage: IQwenProviderToolUsage;
157
+ enabledBuiltInTools: readonly TQwenBuiltInWebToolName[];
158
+ }): TUniversalMessage {
159
+ return {
160
+ id: randomUUID(),
161
+ role: 'assistant',
162
+ content: input.content,
163
+ state: 'complete',
164
+ timestamp: new Date(),
165
+ ...(input.toolCalls.length > 0 && { toolCalls: input.toolCalls }),
166
+ ...(input.response?.usage !== undefined && {
167
+ usage: {
168
+ promptTokens: input.response.usage.input_tokens ?? 0,
169
+ completionTokens: input.response.usage.output_tokens ?? 0,
170
+ totalTokens: input.response.usage.total_tokens ?? 0,
171
+ },
172
+ }),
173
+ metadata: buildProviderToolMetadata(input.enabledBuiltInTools, input.usage, input.response),
174
+ };
175
+ }
176
+
177
+ function buildProviderToolMetadata(
178
+ enabledBuiltInTools: readonly TQwenBuiltInWebToolName[],
179
+ usage: IQwenProviderToolUsage,
180
+ response: IQwenResponsesResponse | undefined,
181
+ ): NonNullable<TUniversalMessage['metadata']> {
182
+ const usedTools = collectUsedToolNames(usage);
183
+ return {
184
+ providerToolMode: 'qwen_responses',
185
+ providerBuiltInToolsEnabled: [...enabledBuiltInTools],
186
+ ...(usedTools.length > 0 && { providerBuiltInToolsUsed: usedTools }),
187
+ qwenWebSearchCalls: usage.webSearchCalls,
188
+ qwenWebExtractorCalls: usage.webExtractorCalls,
189
+ ...(usage.unsupportedToolTypes.size > 0 && {
190
+ qwenUnsupportedProviderToolTypes: [...usage.unsupportedToolTypes].sort(),
191
+ }),
192
+ ...(response?.id !== undefined && { responseId: response.id }),
193
+ ...(response?.model !== undefined && { model: response.model }),
194
+ ...(response?.status !== undefined && { finishReason: response.status }),
195
+ };
196
+ }
197
+
198
+ function collectUsedToolNames(usage: IQwenProviderToolUsage): TQwenBuiltInWebToolName[] {
199
+ const used: TQwenBuiltInWebToolName[] = [];
200
+ if (usage.webSearchCalls > 0) {
201
+ used.push('web_search');
202
+ }
203
+ if (usage.webExtractorCalls > 0) {
204
+ used.push('web_extractor');
205
+ }
206
+ return used;
207
+ }
208
+
209
+ function collectProviderToolUsage(
210
+ output: readonly TQwenResponsesOutputItem[],
211
+ responseUsage: IQwenResponsesUsage | undefined,
212
+ ): IQwenProviderToolUsage {
213
+ const usage = createEmptyToolUsage();
214
+ for (const item of output) {
215
+ if (item.type === 'web_search_call') {
216
+ usage.webSearchCalls += 1;
217
+ } else if (item.type === 'web_extractor_call') {
218
+ usage.webExtractorCalls += 1;
219
+ } else if (isProviderToolOutput(item.type)) {
220
+ usage.unsupportedToolTypes.add(item.type);
221
+ }
222
+ }
223
+ applyUsageCounts(usage, responseUsage);
224
+ return usage;
225
+ }
226
+
227
+ function applyUsageCounts(
228
+ usage: IQwenProviderToolUsage,
229
+ responseUsage: IQwenResponsesUsage | undefined,
230
+ ): void {
231
+ const xTools = responseUsage?.x_tools;
232
+ if (xTools === undefined) {
233
+ return;
234
+ }
235
+ usage.webSearchCalls = Math.max(usage.webSearchCalls, xTools['web_search']?.count ?? 0);
236
+ usage.webExtractorCalls = Math.max(usage.webExtractorCalls, xTools['web_extractor']?.count ?? 0);
237
+ for (const toolName of Object.keys(xTools)) {
238
+ if (toolName !== 'web_search' && toolName !== 'web_extractor') {
239
+ usage.unsupportedToolTypes.add(toolName);
240
+ }
241
+ }
242
+ }
243
+
244
+ function mergeToolUsage(target: IQwenProviderToolUsage, source: IQwenProviderToolUsage): void {
245
+ target.webSearchCalls = Math.max(target.webSearchCalls, source.webSearchCalls);
246
+ target.webExtractorCalls = Math.max(target.webExtractorCalls, source.webExtractorCalls);
247
+ for (const toolType of source.unsupportedToolTypes) {
248
+ target.unsupportedToolTypes.add(toolType);
249
+ }
250
+ }
251
+
252
+ function createEmptyToolUsage(): IQwenProviderToolUsage {
253
+ return {
254
+ webSearchCalls: 0,
255
+ webExtractorCalls: 0,
256
+ unsupportedToolTypes: new Set(),
257
+ };
258
+ }
259
+
260
+ function isProviderToolOutput(type: string): boolean {
261
+ return type.endsWith('_call') && type !== 'function_call';
262
+ }
263
+
264
+ function extractFunctionToolCalls(output: readonly TQwenResponsesOutputItem[]): IToolCall[] {
265
+ return output.filter(isFunctionCallOutputItem).map((item) => convertFunctionCall(item));
266
+ }
267
+
268
+ function isFunctionCallOutputItem(
269
+ item: TQwenResponsesOutputItem,
270
+ ): item is IQwenResponsesFunctionCallOutputItem {
271
+ return (
272
+ item.type === 'function_call' && 'call_id' in item && 'name' in item && 'arguments' in item
273
+ );
274
+ }
275
+
276
+ function convertFunctionCall(item: IQwenResponsesFunctionCallOutputItem): IToolCall {
277
+ return {
278
+ id: item.call_id,
279
+ type: 'function',
280
+ function: {
281
+ name: item.name,
282
+ arguments: item.arguments,
283
+ },
284
+ };
285
+ }
286
+
287
+ function extractMessageText(output: readonly TQwenResponsesOutputItem[]): string {
288
+ return output
289
+ .filter((item): item is Extract<TQwenResponsesOutputItem, { type: 'message' }> => {
290
+ return item.type === 'message';
291
+ })
292
+ .flatMap((item) => item.content)
293
+ .map((part) => part.text)
294
+ .join('');
295
+ }
296
+
297
+ function extractErrorMessage(event: IQwenResponsesErrorEvent): string {
298
+ return event.message ?? event.error?.message ?? event.response?.error?.message ?? 'unknown error';
299
+ }
@@ -0,0 +1,38 @@
1
+ export async function* streamWithAbort<T>(
2
+ source: AsyncIterable<T>,
3
+ signal?: AbortSignal,
4
+ ): AsyncGenerator<T> {
5
+ const iterator = source[Symbol.asyncIterator]();
6
+ try {
7
+ while (!signal?.aborted) {
8
+ const item = await nextStreamItem(iterator, signal);
9
+ if (item.done) break;
10
+ if (signal?.aborted) break;
11
+ yield item.value;
12
+ }
13
+ } finally {
14
+ if (signal?.aborted) {
15
+ await iterator.return?.();
16
+ }
17
+ }
18
+ }
19
+
20
+ async function nextStreamItem<T>(
21
+ iterator: AsyncIterator<T>,
22
+ signal?: AbortSignal,
23
+ ): Promise<IteratorResult<T>> {
24
+ if (!signal) return iterator.next();
25
+ if (signal.aborted) return { done: true, value: undefined as T };
26
+
27
+ let abortListener: (() => void) | undefined;
28
+ const aborted = new Promise<IteratorResult<T>>((resolve) => {
29
+ abortListener = (): void => resolve({ done: true, value: undefined as T });
30
+ signal.addEventListener('abort', abortListener, { once: true });
31
+ });
32
+
33
+ try {
34
+ return await Promise.race([iterator.next(), aborted]);
35
+ } finally {
36
+ if (abortListener) signal.removeEventListener('abort', abortListener);
37
+ }
38
+ }
@@ -0,0 +1,228 @@
1
+ import type OpenAI from 'openai';
2
+ import type {
3
+ IExecutor,
4
+ ILogger,
5
+ IToolSchema,
6
+ TProviderOptionValueBase,
7
+ TUniversalMessage,
8
+ } from '@robota-sdk/agent-core';
9
+
10
+ export type TQwenBuiltInWebToolName = 'web_search' | 'web_extractor';
11
+
12
+ export interface IQwenBuiltInWebToolsOptions {
13
+ webSearch?: boolean;
14
+ webFetch?: boolean;
15
+ enableThinking?: boolean;
16
+ }
17
+
18
+ export interface IQwenResponsesWebSearchTool {
19
+ type: 'web_search';
20
+ }
21
+
22
+ export interface IQwenResponsesWebExtractorTool {
23
+ type: 'web_extractor';
24
+ }
25
+
26
+ export interface IQwenResponsesFunctionTool {
27
+ type: 'function';
28
+ name: string;
29
+ description?: string;
30
+ parameters: IToolSchema['parameters'];
31
+ }
32
+
33
+ export type TQwenResponsesTool =
34
+ | IQwenResponsesWebSearchTool
35
+ | IQwenResponsesWebExtractorTool
36
+ | IQwenResponsesFunctionTool;
37
+
38
+ export interface IQwenResponsesMessageInput {
39
+ type?: 'message';
40
+ role: 'user' | 'assistant' | 'system' | 'developer';
41
+ content: string;
42
+ }
43
+
44
+ export interface IQwenResponsesFunctionCallInput {
45
+ type: 'function_call';
46
+ call_id: string;
47
+ name: string;
48
+ arguments: string;
49
+ }
50
+
51
+ export interface IQwenResponsesFunctionCallOutputInput {
52
+ type: 'function_call_output';
53
+ call_id: string;
54
+ output: string;
55
+ }
56
+
57
+ export type TQwenResponsesInputItem =
58
+ | IQwenResponsesMessageInput
59
+ | IQwenResponsesFunctionCallInput
60
+ | IQwenResponsesFunctionCallOutputInput;
61
+
62
+ export interface IQwenResponsesRequestBase {
63
+ model: string;
64
+ input: TQwenResponsesInputItem[];
65
+ tools?: TQwenResponsesTool[];
66
+ temperature?: number;
67
+ max_output_tokens?: number;
68
+ enable_thinking?: boolean;
69
+ }
70
+
71
+ export interface IQwenResponsesRequestNonStreaming extends IQwenResponsesRequestBase {
72
+ stream?: false;
73
+ }
74
+
75
+ export interface IQwenResponsesRequestStreaming extends IQwenResponsesRequestBase {
76
+ stream: true;
77
+ }
78
+
79
+ export interface IQwenResponsesTextContent {
80
+ type: 'output_text' | 'text' | 'input_text';
81
+ text: string;
82
+ }
83
+
84
+ export interface IQwenResponsesMessageOutputItem {
85
+ type: 'message';
86
+ content: IQwenResponsesTextContent[];
87
+ }
88
+
89
+ export interface IQwenResponsesFunctionCallOutputItem {
90
+ type: 'function_call';
91
+ call_id: string;
92
+ name: string;
93
+ arguments: string;
94
+ id?: string;
95
+ status?: string;
96
+ }
97
+
98
+ export interface IQwenResponsesWebSearchAction {
99
+ query?: string;
100
+ }
101
+
102
+ export interface IQwenResponsesWebSearchOutputItem {
103
+ type: 'web_search_call';
104
+ id?: string;
105
+ status?: string;
106
+ action?: IQwenResponsesWebSearchAction;
107
+ }
108
+
109
+ export interface IQwenResponsesWebExtractorOutputItem {
110
+ type: 'web_extractor_call';
111
+ id?: string;
112
+ status?: string;
113
+ goal?: string;
114
+ output?: string;
115
+ }
116
+
117
+ export interface IQwenResponsesGenericOutputItem {
118
+ type: string;
119
+ id?: string;
120
+ status?: string;
121
+ }
122
+
123
+ export type TQwenResponsesOutputItem =
124
+ | IQwenResponsesMessageOutputItem
125
+ | IQwenResponsesFunctionCallOutputItem
126
+ | IQwenResponsesWebSearchOutputItem
127
+ | IQwenResponsesWebExtractorOutputItem
128
+ | IQwenResponsesGenericOutputItem;
129
+
130
+ export interface IQwenResponsesToolUsageCount {
131
+ count?: number;
132
+ }
133
+
134
+ export interface IQwenResponsesUsage {
135
+ input_tokens?: number;
136
+ output_tokens?: number;
137
+ total_tokens?: number;
138
+ x_tools?: Record<string, IQwenResponsesToolUsageCount>;
139
+ }
140
+
141
+ export interface IQwenResponsesResponse {
142
+ id?: string;
143
+ model?: string;
144
+ output_text?: string;
145
+ output?: TQwenResponsesOutputItem[];
146
+ usage?: IQwenResponsesUsage;
147
+ status?: string;
148
+ }
149
+
150
+ export interface IQwenResponsesTextDeltaEvent {
151
+ type: 'response.output_text.delta';
152
+ delta: string;
153
+ }
154
+
155
+ export interface IQwenResponsesCompletedEvent {
156
+ type: 'response.completed';
157
+ response: IQwenResponsesResponse;
158
+ }
159
+
160
+ export interface IQwenResponsesOutputItemDoneEvent {
161
+ type: 'response.output_item.done';
162
+ item: TQwenResponsesOutputItem;
163
+ }
164
+
165
+ export interface IQwenResponsesErrorEvent {
166
+ type: 'response.error' | 'response.failed';
167
+ message?: string;
168
+ error?: {
169
+ message?: string;
170
+ };
171
+ response?: {
172
+ error?: {
173
+ message?: string;
174
+ };
175
+ };
176
+ }
177
+
178
+ export interface IQwenResponsesWebSearchEvent {
179
+ type:
180
+ | 'response.web_search_call.in_progress'
181
+ | 'response.web_search_call.searching'
182
+ | 'response.web_search_call.completed';
183
+ }
184
+
185
+ export interface IQwenResponsesGenericEvent {
186
+ type: string;
187
+ item?: TQwenResponsesOutputItem;
188
+ response?: IQwenResponsesResponse;
189
+ }
190
+
191
+ export type TQwenResponsesStreamEvent =
192
+ | IQwenResponsesTextDeltaEvent
193
+ | IQwenResponsesCompletedEvent
194
+ | IQwenResponsesOutputItemDoneEvent
195
+ | IQwenResponsesErrorEvent
196
+ | IQwenResponsesWebSearchEvent;
197
+
198
+ export type TQwenMessagesToResponsesInput = (
199
+ messages: TUniversalMessage[],
200
+ ) => TQwenResponsesInputItem[];
201
+
202
+ export type TQwenProviderOptionValue =
203
+ | string
204
+ | number
205
+ | boolean
206
+ | undefined
207
+ | null
208
+ | IQwenBuiltInWebToolsOptions
209
+ | OpenAI
210
+ | ILogger
211
+ | IExecutor
212
+ | TProviderOptionValueBase
213
+ | TQwenProviderOptionValue[]
214
+ | { [key: string]: TQwenProviderOptionValue };
215
+
216
+ export interface IQwenProviderOptions {
217
+ [key: string]: TQwenProviderOptionValue;
218
+
219
+ apiKey?: string;
220
+ baseURL?: string;
221
+ responsesBaseURL?: string;
222
+ timeout?: number;
223
+ defaultModel?: string;
224
+ builtInWebTools?: IQwenBuiltInWebToolsOptions;
225
+ client?: OpenAI;
226
+ executor?: IExecutor;
227
+ logger?: ILogger;
228
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { probeOpenAICompatibleProfile, type TOpenAICompatibleFetch } from './endpoint-probe';
3
+
4
+ describe('probeOpenAICompatibleProfile', () => {
5
+ it('should skip endpoint calls when baseURL is missing', async () => {
6
+ const result = await probeOpenAICompatibleProfile({});
7
+
8
+ expect(result).toEqual({
9
+ ok: true,
10
+ message: 'Profile fields are valid; no endpoint probe configured.',
11
+ });
12
+ });
13
+
14
+ it('should fetch models from the OpenAI-compatible models endpoint', async () => {
15
+ const requestedUrls: string[] = [];
16
+ const fetcher: TOpenAICompatibleFetch = async (url) => {
17
+ requestedUrls.push(url);
18
+ return {
19
+ ok: true,
20
+ status: 200,
21
+ json: async () => ({ data: [{ id: 'supergemma4' }, { id: undefined }] }),
22
+ };
23
+ };
24
+
25
+ const result = await probeOpenAICompatibleProfile(
26
+ { baseURL: 'http://localhost:1234/v1/' },
27
+ fetcher,
28
+ );
29
+
30
+ expect(requestedUrls).toEqual(['http://localhost:1234/v1/models']);
31
+ expect(result).toEqual({
32
+ ok: true,
33
+ message: '1 model(s) discovered',
34
+ models: ['supergemma4'],
35
+ });
36
+ });
37
+
38
+ it('should return HTTP failure status', async () => {
39
+ const fetcher: TOpenAICompatibleFetch = async () => ({
40
+ ok: false,
41
+ status: 503,
42
+ json: async () => ({}),
43
+ });
44
+
45
+ const result = await probeOpenAICompatibleProfile(
46
+ { baseURL: 'http://localhost:1234/v1' },
47
+ fetcher,
48
+ );
49
+
50
+ expect(result).toEqual({ ok: false, message: 'HTTP 503' });
51
+ });
52
+ });
@@ -0,0 +1,43 @@
1
+ import type { IProviderProfileConfig, IProviderProbeResult } from '@robota-sdk/agent-core';
2
+
3
+ export interface IOpenAICompatibleModelsResponse {
4
+ data?: Array<{ id?: string }>;
5
+ }
6
+
7
+ export interface IOpenAICompatibleFetchResponse {
8
+ ok: boolean;
9
+ status: number;
10
+ json: () => Promise<IOpenAICompatibleModelsResponse>;
11
+ }
12
+
13
+ export type TOpenAICompatibleFetch = (url: string) => Promise<IOpenAICompatibleFetchResponse>;
14
+
15
+ export async function probeOpenAICompatibleProfile(
16
+ profile: IProviderProfileConfig,
17
+ fetcher: TOpenAICompatibleFetch = defaultOpenAICompatibleFetch,
18
+ ): Promise<IProviderProbeResult> {
19
+ if (!profile.baseURL) {
20
+ return { ok: true, message: 'Profile fields are valid; no endpoint probe configured.' };
21
+ }
22
+
23
+ try {
24
+ const response = await fetcher(`${profile.baseURL.replace(/\/$/, '')}/models`);
25
+ if (!response.ok) {
26
+ return { ok: false, message: `HTTP ${response.status}` };
27
+ }
28
+ const body = await response.json();
29
+ const models = (body.data ?? []).map((item) => item.id).filter((id): id is string => !!id);
30
+ return { ok: true, message: `${models.length} model(s) discovered`, models };
31
+ } catch (error) {
32
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
33
+ }
34
+ }
35
+
36
+ async function defaultOpenAICompatibleFetch(url: string): Promise<IOpenAICompatibleFetchResponse> {
37
+ const response = await fetch(url);
38
+ return {
39
+ ok: response.ok,
40
+ status: response.status,
41
+ json: () => response.json() as Promise<IOpenAICompatibleModelsResponse>,
42
+ };
43
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './message-converter';
3
+ export * from './response-parser';
4
+ export * from './stream-assembler';
5
+ export * from './native-payload-observer';
6
+ export * from './endpoint-probe';