@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,194 @@
1
+ import OpenAI from 'openai';
2
+
3
+ import type { IPayloadLogger } from './interfaces/payload-logger';
4
+ import type { IExecutor, ILogger, TProviderOptionValueBase } from '@robota-sdk/agent-core';
5
+
6
+ export type TOpenAIApiSurface = 'responses' | 'chat-completions';
7
+
8
+ export interface IOpenAIJsonSchemaDefinition {
9
+ name: string;
10
+ description?: string;
11
+ schema?: Record<string, TOpenAIProviderOptionValue>;
12
+ strict?: boolean;
13
+ }
14
+
15
+ export interface IOpenAIResponsesReasoningOptions {
16
+ effort?: 'low' | 'medium' | 'high';
17
+ summary?: 'auto' | 'concise' | 'detailed';
18
+ }
19
+
20
+ export interface IOpenAINativeWebToolsOptions {
21
+ webSearch?: boolean;
22
+ webFetch?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Valid provider option value types
27
+ */
28
+ export type TOpenAIProviderOptionValue =
29
+ | string
30
+ | number
31
+ | boolean
32
+ | undefined
33
+ | null
34
+ | IOpenAIJsonSchemaDefinition
35
+ | IOpenAINativeWebToolsOptions
36
+ | IOpenAIResponsesReasoningOptions
37
+ | OpenAI
38
+ | IPayloadLogger
39
+ | ILogger
40
+ | IExecutor
41
+ | TProviderOptionValueBase
42
+ | TOpenAIProviderOptionValue[]
43
+ | { [key: string]: TOpenAIProviderOptionValue };
44
+
45
+ /**
46
+ * OpenAI provider options
47
+ */
48
+ export interface IOpenAIProviderOptions {
49
+ /**
50
+ * Additional provider-specific options
51
+ */
52
+ [key: string]: TOpenAIProviderOptionValue;
53
+
54
+ /**
55
+ * OpenAI API key (required when client is not provided)
56
+ */
57
+ apiKey?: string;
58
+
59
+ /**
60
+ * OpenAI organization ID (optional)
61
+ */
62
+ organization?: string;
63
+
64
+ /**
65
+ * API request timeout (milliseconds)
66
+ */
67
+ timeout?: number;
68
+
69
+ /**
70
+ * API base URL (default: 'https://api.openai.com/v1')
71
+ */
72
+ baseURL?: string;
73
+
74
+ /**
75
+ * Default model used when chat options do not provide a model.
76
+ */
77
+ defaultModel?: string;
78
+
79
+ /**
80
+ * API surface to use for direct OpenAI calls.
81
+ *
82
+ * Defaults to Responses for official OpenAI calls. Profiles with baseURL use
83
+ * Chat Completions by default for OpenAI-compatible endpoint compatibility.
84
+ */
85
+ apiSurface?: TOpenAIApiSurface;
86
+
87
+ /**
88
+ * Response format (default: 'text')
89
+ * - 'text': Plain text response
90
+ * - 'json_object': JSON object mode (requires system message)
91
+ * - 'json_schema': Structured Outputs with schema validation
92
+ */
93
+ responseFormat?: 'text' | 'json_object' | 'json_schema';
94
+
95
+ /**
96
+ * JSON schema for structured outputs (required when responseFormat is 'json_schema')
97
+ */
98
+ jsonSchema?: IOpenAIJsonSchemaDefinition;
99
+
100
+ /**
101
+ * Responses API reasoning controls. Hidden reasoning is never exposed in message
102
+ * content; only explicit summaries/encrypted items requested here are represented.
103
+ */
104
+ reasoning?: IOpenAIResponsesReasoningOptions;
105
+
106
+ /**
107
+ * Whether OpenAI should store Responses API results. Defaults to OpenAI API behavior.
108
+ */
109
+ store?: boolean;
110
+
111
+ /**
112
+ * Include encrypted reasoning items for stateless reasoning continuation.
113
+ */
114
+ includeEncryptedReasoning?: boolean;
115
+
116
+ /**
117
+ * Opt into strict custom function parameter validation where supported.
118
+ */
119
+ strictTools?: boolean;
120
+
121
+ /**
122
+ * Provider-native hosted web tool request from provider profile options.
123
+ *
124
+ * OpenAI-compatible Chat Completions endpoints do not support this Robota
125
+ * native web contract. The provider rejects unsupported configurations before
126
+ * any model request is sent.
127
+ */
128
+ nativeWebTools?: IOpenAINativeWebToolsOptions;
129
+
130
+ /**
131
+ * OpenAI client instance (optional: will be created from apiKey if not provided)
132
+ */
133
+ client?: OpenAI;
134
+
135
+ /**
136
+ * Payload logger instance for debugging API requests/responses
137
+ *
138
+ * Use different implementations based on your environment:
139
+ * - FilePayloadLogger: Node.js file-based logging
140
+ * - ConsolePayloadLogger: Browser console-based logging
141
+ * - Custom: Implement IPayloadLogger interface
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * // Node.js
146
+ * import { FilePayloadLogger } from '@robota-sdk/agent-provider/openai/loggers';
147
+ * const provider = new OpenAIProvider({
148
+ * client: openaiClient,
149
+ * payloadLogger: new FilePayloadLogger({ logDir: './logs/openai' })
150
+ * });
151
+ *
152
+ * // Browser
153
+ * import { ConsolePayloadLogger } from '@robota-sdk/agent-provider/openai/loggers';
154
+ * const provider = new OpenAIProvider({
155
+ * client: openaiClient,
156
+ * payloadLogger: new ConsolePayloadLogger()
157
+ * });
158
+ * ```
159
+ */
160
+ payloadLogger?: IPayloadLogger;
161
+
162
+ /**
163
+ * Optional executor for handling AI requests
164
+ *
165
+ * When provided, the provider will delegate all chat operations to this executor
166
+ * instead of making direct API calls. This enables remote execution capabilities.
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * import { LocalExecutor, RemoteExecutor } from '@robota-sdk/agent-core';
171
+ *
172
+ * // Local execution (registers this provider)
173
+ * const localExecutor = new LocalExecutor();
174
+ * localExecutor.registerProvider('openai', new OpenAIProvider({ apiKey: 'sk-...' }));
175
+ *
176
+ * // Remote execution
177
+ * const remoteExecutor = new RemoteExecutor({
178
+ * serverUrl: 'https://api.robota.io',
179
+ * userApiKey: 'user-token-123'
180
+ * });
181
+ *
182
+ * const provider = new OpenAIProvider({
183
+ * executor: remoteExecutor // No direct API key needed
184
+ * });
185
+ * ```
186
+ */
187
+ executor?: IExecutor;
188
+
189
+ /**
190
+ * Logger instance for internal OpenAI provider logging
191
+ * @defaultValue SilentLogger
192
+ */
193
+ logger?: ILogger;
194
+ }
@@ -0,0 +1,26 @@
1
+ export const QWEN_PROVIDER_BASE_URLS = {
2
+ singapore: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
3
+ usVirginia: 'https://dashscope-us.aliyuncs.com/compatible-mode/v1',
4
+ beijing: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
5
+ hongKong: 'https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1',
6
+ } as const;
7
+
8
+ export type TQwenProviderRegion = keyof typeof QWEN_PROVIDER_BASE_URLS;
9
+
10
+ export const QWEN_PROVIDER_RESPONSES_BASE_URLS = {
11
+ singapore: 'https://dashscope-intl.aliyuncs.com/api/v2/apps/protocols/compatible-mode/v1',
12
+ usVirginia: 'https://dashscope-us.aliyuncs.com/api/v2/apps/protocols/compatible-mode/v1',
13
+ beijing: 'https://dashscope.aliyuncs.com/api/v2/apps/protocols/compatible-mode/v1',
14
+ } as const;
15
+
16
+ export type TQwenProviderResponsesRegion = keyof typeof QWEN_PROVIDER_RESPONSES_BASE_URLS;
17
+
18
+ export const DEFAULT_QWEN_PROVIDER_MODEL = 'qwen-plus';
19
+ export const DEFAULT_QWEN_PROVIDER_API_KEY_ENV = 'DASHSCOPE_API_KEY';
20
+ export const DEFAULT_QWEN_PROVIDER_API_KEY_REFERENCE = `$ENV:${DEFAULT_QWEN_PROVIDER_API_KEY_ENV}`;
21
+ export const DEFAULT_QWEN_PROVIDER_BASE_URL = QWEN_PROVIDER_BASE_URLS.singapore;
22
+ export const DEFAULT_QWEN_PROVIDER_RESPONSES_BASE_URL = QWEN_PROVIDER_RESPONSES_BASE_URLS.singapore;
23
+
24
+ export const QWEN_MODEL_SOURCE_URL =
25
+ 'https://www.alibabacloud.com/help/en/model-studio/compatibility-of-openai-with-dashscope';
26
+ export const QWEN_MODEL_LAST_VERIFIED_AT = '2026-05-04';
@@ -0,0 +1,5 @@
1
+ export * from './defaults';
2
+ export * from './types';
3
+ export * from './provider';
4
+ export * from './provider-definition';
5
+ export * from './model-catalog-refresh';
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { refreshQwenModelCatalog } from './model-catalog-refresh';
3
+
4
+ describe('refreshQwenModelCatalog', () => {
5
+ it('maps the live Qwen /models response into provider model catalog entries', async () => {
6
+ const fetcher = vi.fn(async () => ({
7
+ ok: true,
8
+ status: 200,
9
+ json: async () => ({
10
+ data: [
11
+ { id: 'qwen-plus', object: 'model', owned_by: 'alibaba-cloud' },
12
+ { id: 'qwen-max', object: 'model', owned_by: 'alibaba-cloud' },
13
+ { id: 'qwen-turbo', object: 'model', owned_by: 'alibaba-cloud' },
14
+ ],
15
+ }),
16
+ }));
17
+
18
+ const catalog = await refreshQwenModelCatalog(
19
+ {
20
+ baseURL: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/',
21
+ apiKey: 'test-api-key',
22
+ },
23
+ fetcher,
24
+ );
25
+
26
+ expect(fetcher).toHaveBeenCalledWith(
27
+ 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models',
28
+ {
29
+ headers: {
30
+ Authorization: 'Bearer test-api-key',
31
+ },
32
+ },
33
+ );
34
+ expect(catalog.status).toBe('live');
35
+ expect(catalog.entries?.map((entry) => entry.id)).toEqual([
36
+ 'qwen-plus',
37
+ 'qwen-max',
38
+ 'qwen-turbo',
39
+ ]);
40
+ expect(catalog.entries?.[0]).toMatchObject({
41
+ id: 'qwen-plus',
42
+ displayName: 'qwen-plus',
43
+ lifecycle: 'active',
44
+ });
45
+ });
46
+
47
+ it('uses the default base URL when profile.baseURL is not set', async () => {
48
+ const fetcher = vi.fn(async () => ({
49
+ ok: true,
50
+ status: 200,
51
+ json: async () => ({ data: [{ id: 'qwen-flash' }] }),
52
+ }));
53
+
54
+ await refreshQwenModelCatalog({ apiKey: 'test-key' }, fetcher);
55
+
56
+ expect(fetcher).toHaveBeenCalledWith(
57
+ 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models',
58
+ expect.anything(),
59
+ );
60
+ });
61
+
62
+ it('omits Authorization header when no apiKey is provided', async () => {
63
+ const fetcher = vi.fn(async () => ({
64
+ ok: true,
65
+ status: 200,
66
+ json: async () => ({ data: [] }),
67
+ }));
68
+
69
+ await refreshQwenModelCatalog({}, fetcher);
70
+
71
+ expect(fetcher).toHaveBeenCalledWith(
72
+ 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models',
73
+ undefined,
74
+ );
75
+ });
76
+
77
+ it('returns an unavailable catalog when the live model refresh fails', async () => {
78
+ const fetcher = vi.fn(async () => ({
79
+ ok: false,
80
+ status: 401,
81
+ json: async () => ({ data: [] }),
82
+ }));
83
+
84
+ const catalog = await refreshQwenModelCatalog({}, fetcher);
85
+
86
+ expect(catalog).toMatchObject({
87
+ status: 'unavailable',
88
+ message: 'Qwen model refresh failed: HTTP 401',
89
+ });
90
+ });
91
+ });
@@ -0,0 +1,97 @@
1
+ import type {
2
+ IProviderModelCatalog,
3
+ IProviderModelCatalogEntry,
4
+ IProviderProfileConfig,
5
+ } from '@robota-sdk/agent-core';
6
+ import {
7
+ DEFAULT_QWEN_PROVIDER_BASE_URL,
8
+ QWEN_MODEL_LAST_VERIFIED_AT,
9
+ QWEN_MODEL_SOURCE_URL,
10
+ } from './defaults';
11
+
12
+ export interface IQwenModelsResponse {
13
+ data?: Array<{
14
+ id?: string;
15
+ object?: string;
16
+ owned_by?: string;
17
+ }>;
18
+ }
19
+
20
+ export interface IQwenFetchInit {
21
+ headers?: Record<string, string>;
22
+ }
23
+
24
+ export interface IQwenFetchResponse {
25
+ ok: boolean;
26
+ status: number;
27
+ json: () => Promise<IQwenModelsResponse>;
28
+ }
29
+
30
+ export type TQwenFetch = (url: string, init?: IQwenFetchInit) => Promise<IQwenFetchResponse>;
31
+
32
+ export async function refreshQwenModelCatalog(
33
+ profile: IProviderProfileConfig,
34
+ fetcher: TQwenFetch = defaultQwenFetch,
35
+ ): Promise<IProviderModelCatalog> {
36
+ const baseURL = trimTrailingSlash(profile.baseURL ?? DEFAULT_QWEN_PROVIDER_BASE_URL);
37
+ const url = `${baseURL}/models`;
38
+ const response = await fetcher(url, buildFetchInit(profile.apiKey));
39
+
40
+ if (!response.ok) {
41
+ return {
42
+ status: 'unavailable',
43
+ sourceUrl: QWEN_MODEL_SOURCE_URL,
44
+ message: `Qwen model refresh failed: HTTP ${response.status}`,
45
+ };
46
+ }
47
+
48
+ const body = await response.json();
49
+ const entries = (body.data ?? [])
50
+ .map((model) => model.id)
51
+ .filter((id): id is string => typeof id === 'string' && id.length > 0)
52
+ .map(toModelCatalogEntry);
53
+
54
+ return {
55
+ status: 'live',
56
+ sourceUrl: QWEN_MODEL_SOURCE_URL,
57
+ lastVerifiedAt: QWEN_MODEL_LAST_VERIFIED_AT,
58
+ entries,
59
+ message: `Loaded ${entries.length} models from Qwen API`,
60
+ };
61
+ }
62
+
63
+ function buildFetchInit(apiKey: string | undefined): IQwenFetchInit | undefined {
64
+ if (!apiKey) {
65
+ return undefined;
66
+ }
67
+ return {
68
+ headers: {
69
+ Authorization: `Bearer ${apiKey}`,
70
+ },
71
+ };
72
+ }
73
+
74
+ function toModelCatalogEntry(id: string): IProviderModelCatalogEntry {
75
+ return {
76
+ id,
77
+ displayName: id,
78
+ lifecycle: 'active',
79
+ sourceUrl: QWEN_MODEL_SOURCE_URL,
80
+ lastVerifiedAt: QWEN_MODEL_LAST_VERIFIED_AT,
81
+ };
82
+ }
83
+
84
+ function trimTrailingSlash(value: string): string {
85
+ return value.replace(/\/$/, '');
86
+ }
87
+
88
+ async function defaultQwenFetch(url: string, init?: IQwenFetchInit): Promise<IQwenFetchResponse> {
89
+ const response = await fetch(url, {
90
+ ...(init?.headers !== undefined && { headers: init.headers }),
91
+ });
92
+ return {
93
+ ok: response.ok,
94
+ status: response.status,
95
+ json: () => response.json() as Promise<IQwenModelsResponse>,
96
+ };
97
+ }
@@ -0,0 +1,34 @@
1
+ import type { IProviderCapabilities } from '@robota-sdk/agent-core';
2
+ import type { IQwenProviderOptions } from './types';
3
+
4
+ const QWEN_RESPONSES_SOURCE = 'qwen-responses';
5
+ const ENABLE_WEB_SEARCH_REASON = 'Enable builtInWebTools.webSearch or builtInWebTools.webFetch.';
6
+ const ENABLE_WEB_FETCH_REASON = 'Enable builtInWebTools.webFetch.';
7
+
8
+ export function getQwenProviderCapabilities(options: IQwenProviderOptions): IProviderCapabilities {
9
+ const webTools = options.builtInWebTools;
10
+ const webSearchEnabled = webTools?.webSearch === true || webTools?.webFetch === true;
11
+ const webFetchEnabled = webTools?.webFetch === true;
12
+
13
+ return {
14
+ functionCalling: { supported: true },
15
+ nativeWebTools: {
16
+ webSearch: webSearchEnabled
17
+ ? { supported: true, enabled: true, source: QWEN_RESPONSES_SOURCE }
18
+ : {
19
+ supported: true,
20
+ enabled: false,
21
+ source: QWEN_RESPONSES_SOURCE,
22
+ reason: ENABLE_WEB_SEARCH_REASON,
23
+ },
24
+ webFetch: webFetchEnabled
25
+ ? { supported: true, enabled: true, source: QWEN_RESPONSES_SOURCE }
26
+ : {
27
+ supported: true,
28
+ enabled: false,
29
+ source: QWEN_RESPONSES_SOURCE,
30
+ reason: ENABLE_WEB_FETCH_REASON,
31
+ },
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,139 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import {
3
+ createQwenProviderDefinition,
4
+ DEFAULT_QWEN_PROVIDER_API_KEY_ENV,
5
+ DEFAULT_QWEN_PROVIDER_API_KEY_REFERENCE,
6
+ DEFAULT_QWEN_PROVIDER_BASE_URL,
7
+ DEFAULT_QWEN_PROVIDER_MODEL,
8
+ DEFAULT_QWEN_PROVIDER_RESPONSES_BASE_URL,
9
+ QWEN_PROVIDER_BASE_URLS,
10
+ QWEN_PROVIDER_RESPONSES_BASE_URLS,
11
+ QwenProvider,
12
+ } from './index';
13
+
14
+ vi.mock('openai', () => {
15
+ const MockOpenAI = vi.fn().mockImplementation(() => ({
16
+ chat: {
17
+ completions: {
18
+ create: vi.fn(),
19
+ },
20
+ },
21
+ responses: {
22
+ create: vi.fn(),
23
+ },
24
+ }));
25
+ return { default: MockOpenAI };
26
+ });
27
+
28
+ describe('createQwenProviderDefinition', () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ it('exposes Qwen defaults and setup steps through the provider-definition contract', () => {
34
+ const definition = createQwenProviderDefinition();
35
+
36
+ expect(definition.type).toBe('qwen');
37
+ expect(definition.requiresApiKey).toBe(true);
38
+ expect(definition.probeProfile).toBeTypeOf('function');
39
+ expect(definition.defaults).toEqual({
40
+ model: DEFAULT_QWEN_PROVIDER_MODEL,
41
+ apiKey: DEFAULT_QWEN_PROVIDER_API_KEY_REFERENCE,
42
+ baseURL: DEFAULT_QWEN_PROVIDER_BASE_URL,
43
+ });
44
+ expect(definition.setupHelpLinks).toEqual([
45
+ {
46
+ kind: 'api-key',
47
+ label: 'Alibaba Cloud Model Studio API keys',
48
+ url: 'https://modelstudio.console.alibabacloud.com/?tab=api#/api-key',
49
+ sourceUrl: 'https://www.alibabacloud.com/help/en/model-studio/get-api-key',
50
+ lastVerifiedAt: '2026-05-08',
51
+ },
52
+ ]);
53
+ expect(DEFAULT_QWEN_PROVIDER_API_KEY_ENV).toBe('DASHSCOPE_API_KEY');
54
+ expect(QWEN_PROVIDER_BASE_URLS).toMatchObject({
55
+ singapore: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
56
+ usVirginia: 'https://dashscope-us.aliyuncs.com/compatible-mode/v1',
57
+ beijing: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
58
+ hongKong: 'https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1',
59
+ });
60
+ expect(DEFAULT_QWEN_PROVIDER_RESPONSES_BASE_URL).toBe(
61
+ 'https://dashscope-intl.aliyuncs.com/api/v2/apps/protocols/compatible-mode/v1',
62
+ );
63
+ expect(QWEN_PROVIDER_RESPONSES_BASE_URLS).toMatchObject({
64
+ singapore: 'https://dashscope-intl.aliyuncs.com/api/v2/apps/protocols/compatible-mode/v1',
65
+ usVirginia: 'https://dashscope-us.aliyuncs.com/api/v2/apps/protocols/compatible-mode/v1',
66
+ beijing: 'https://dashscope.aliyuncs.com/api/v2/apps/protocols/compatible-mode/v1',
67
+ });
68
+ expect(definition.setupSteps).toEqual([
69
+ {
70
+ key: 'baseURL',
71
+ title: 'Qwen OpenAI-compatible base URL',
72
+ defaultValue: DEFAULT_QWEN_PROVIDER_BASE_URL,
73
+ },
74
+ {
75
+ key: 'model',
76
+ title: 'Qwen model',
77
+ defaultValue: DEFAULT_QWEN_PROVIDER_MODEL,
78
+ },
79
+ {
80
+ key: 'apiKey',
81
+ title: 'Qwen Model Studio API key',
82
+ defaultValue: DEFAULT_QWEN_PROVIDER_API_KEY_REFERENCE,
83
+ masked: true,
84
+ },
85
+ ]);
86
+ });
87
+
88
+ it('creates a QwenProvider from a resolved provider profile', () => {
89
+ const definition = createQwenProviderDefinition();
90
+
91
+ const provider = definition.createProvider({
92
+ name: 'qwen',
93
+ model: 'qwen-plus',
94
+ apiKey: 'dashscope-key',
95
+ baseURL: QWEN_PROVIDER_BASE_URLS.usVirginia,
96
+ timeout: 12_000,
97
+ });
98
+
99
+ expect(provider).toBeInstanceOf(QwenProvider);
100
+ expect(provider.name).toBe('qwen');
101
+ });
102
+
103
+ it('passes Qwen-owned Responses API options from the generic provider options bag', () => {
104
+ const definition = createQwenProviderDefinition();
105
+
106
+ const provider = definition.createProvider({
107
+ name: 'qwen',
108
+ model: 'qwen3.6-plus',
109
+ apiKey: 'dashscope-key',
110
+ options: {
111
+ responsesBaseURL: QWEN_PROVIDER_RESPONSES_BASE_URLS.usVirginia,
112
+ builtInWebTools: {
113
+ webSearch: true,
114
+ webFetch: true,
115
+ enableThinking: true,
116
+ },
117
+ },
118
+ });
119
+
120
+ const options = (provider as unknown as { options: Record<string, unknown> }).options;
121
+ expect(options['responsesBaseURL']).toBe(QWEN_PROVIDER_RESPONSES_BASE_URLS.usVirginia);
122
+ expect(options['builtInWebTools']).toEqual({
123
+ webSearch: true,
124
+ webFetch: true,
125
+ enableThinking: true,
126
+ });
127
+ });
128
+
129
+ it('rejects provider creation without an API key', () => {
130
+ const definition = createQwenProviderDefinition();
131
+
132
+ expect(() =>
133
+ definition.createProvider({
134
+ name: 'qwen',
135
+ model: 'qwen-plus',
136
+ }),
137
+ ).toThrow('Provider qwen requires apiKey');
138
+ });
139
+ });