@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 { describe, expect, it } from 'vitest';
2
+ import {
3
+ mapVideoStatus,
4
+ mapInitialStatus,
5
+ mapVideoJobSnapshot,
6
+ toIsoTimestamp,
7
+ } from './status-mapper';
8
+ import type { IBytedanceVideoTaskResponse } from './types';
9
+
10
+ describe('mapVideoStatus', () => {
11
+ it.each([
12
+ ['queued', 'queued'],
13
+ ['pending', 'queued'],
14
+ ['submitted', 'queued'],
15
+ ['running', 'running'],
16
+ ['processing', 'running'],
17
+ ['in_progress', 'running'],
18
+ ['succeeded', 'succeeded'],
19
+ ['success', 'succeeded'],
20
+ ['completed', 'succeeded'],
21
+ ['failed', 'failed'],
22
+ ['error', 'failed'],
23
+ ['cancelled', 'cancelled'],
24
+ ['canceled', 'cancelled'],
25
+ ])('maps "%s" to "%s"', (input, expected) => {
26
+ const result = mapVideoStatus(input);
27
+ expect(result.ok).toBe(true);
28
+ if (result.ok) {
29
+ expect(result.value).toBe(expected);
30
+ }
31
+ });
32
+
33
+ it('trims whitespace and normalizes case', () => {
34
+ const result = mapVideoStatus(' QUEUED ');
35
+ expect(result.ok).toBe(true);
36
+ if (result.ok) {
37
+ expect(result.value).toBe('queued');
38
+ }
39
+ });
40
+
41
+ it('returns PROVIDER_UPSTREAM_ERROR for unknown status', () => {
42
+ const result = mapVideoStatus('unknown_state');
43
+ expect(result.ok).toBe(false);
44
+ if (!result.ok) {
45
+ expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
46
+ expect(result.error.message).toContain('unknown_state');
47
+ }
48
+ });
49
+ });
50
+
51
+ describe('mapInitialStatus', () => {
52
+ it('returns queued when status is undefined', () => {
53
+ const result = mapInitialStatus(undefined);
54
+ expect(result.ok).toBe(true);
55
+ if (result.ok) {
56
+ expect(result.value).toBe('queued');
57
+ }
58
+ });
59
+
60
+ it('returns queued when status is empty string', () => {
61
+ const result = mapInitialStatus('');
62
+ expect(result.ok).toBe(true);
63
+ if (result.ok) {
64
+ expect(result.value).toBe('queued');
65
+ }
66
+ });
67
+
68
+ it('returns queued when status is whitespace-only', () => {
69
+ const result = mapInitialStatus(' ');
70
+ expect(result.ok).toBe(true);
71
+ if (result.ok) {
72
+ expect(result.value).toBe('queued');
73
+ }
74
+ });
75
+
76
+ it.each([
77
+ ['queued', 'queued'],
78
+ ['pending', 'queued'],
79
+ ['submitted', 'queued'],
80
+ ['running', 'running'],
81
+ ['processing', 'running'],
82
+ ['in_progress', 'running'],
83
+ ])('maps "%s" to "%s"', (input, expected) => {
84
+ const result = mapInitialStatus(input);
85
+ expect(result.ok).toBe(true);
86
+ if (result.ok) {
87
+ expect(result.value).toBe(expected);
88
+ }
89
+ });
90
+
91
+ it('returns error for status that is not queued or running', () => {
92
+ const result = mapInitialStatus('completed');
93
+ expect(result.ok).toBe(false);
94
+ if (!result.ok) {
95
+ expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
96
+ }
97
+ });
98
+ });
99
+
100
+ describe('toIsoTimestamp', () => {
101
+ it('converts epoch seconds to ISO string', () => {
102
+ const epochSeconds = 1700000000;
103
+ const result = toIsoTimestamp(epochSeconds);
104
+ expect(result).toBe(new Date(epochSeconds * 1000).toISOString());
105
+ });
106
+
107
+ it('converts epoch milliseconds to ISO string', () => {
108
+ const epochMs = 1700000000000;
109
+ const result = toIsoTimestamp(epochMs);
110
+ expect(result).toBe(new Date(epochMs).toISOString());
111
+ });
112
+
113
+ it('converts numeric string to ISO string', () => {
114
+ const result = toIsoTimestamp('1700000000');
115
+ expect(result).toBe(new Date(1700000000 * 1000).toISOString());
116
+ });
117
+
118
+ it('converts ISO string to ISO string', () => {
119
+ const isoString = '2024-01-15T12:00:00.000Z';
120
+ const result = toIsoTimestamp(isoString);
121
+ expect(result).toBe(new Date(isoString).toISOString());
122
+ });
123
+
124
+ it('returns current time for undefined', () => {
125
+ const before = Date.now();
126
+ const result = toIsoTimestamp(undefined);
127
+ const after = Date.now();
128
+ const resultMs = new Date(result).getTime();
129
+ expect(resultMs).toBeGreaterThanOrEqual(before);
130
+ expect(resultMs).toBeLessThanOrEqual(after);
131
+ });
132
+
133
+ it('returns current time for non-parsable string', () => {
134
+ const before = Date.now();
135
+ const result = toIsoTimestamp('not-a-date');
136
+ const after = Date.now();
137
+ const resultMs = new Date(result).getTime();
138
+ expect(resultMs).toBeGreaterThanOrEqual(before);
139
+ expect(resultMs).toBeLessThanOrEqual(after);
140
+ });
141
+
142
+ it('returns current time for NaN number', () => {
143
+ const before = Date.now();
144
+ const result = toIsoTimestamp(NaN);
145
+ const after = Date.now();
146
+ const resultMs = new Date(result).getTime();
147
+ expect(resultMs).toBeGreaterThanOrEqual(before);
148
+ expect(resultMs).toBeLessThanOrEqual(after);
149
+ });
150
+ });
151
+
152
+ describe('mapVideoJobSnapshot', () => {
153
+ it('maps a succeeded response with video output', () => {
154
+ const response: IBytedanceVideoTaskResponse = {
155
+ id: 'task-1',
156
+ status: 'completed',
157
+ video_url: 'https://cdn.test/video.mp4',
158
+ mime_type: 'video/mp4',
159
+ bytes: 2048,
160
+ created_at: 1700000000,
161
+ };
162
+ const result = mapVideoJobSnapshot(response);
163
+ expect(result.ok).toBe(true);
164
+ if (result.ok) {
165
+ expect(result.value.jobId).toBe('task-1');
166
+ expect(result.value.status).toBe('succeeded');
167
+ expect(result.value.output).toEqual({
168
+ kind: 'uri',
169
+ uri: 'https://cdn.test/video.mp4',
170
+ mimeType: 'video/mp4',
171
+ bytes: 2048,
172
+ });
173
+ expect(result.value.error).toBeUndefined();
174
+ }
175
+ });
176
+
177
+ it('maps a failed response with error message', () => {
178
+ const response: IBytedanceVideoTaskResponse = {
179
+ id: 'task-2',
180
+ status: 'failed',
181
+ error_message: 'Content policy violation',
182
+ created_at: 1700000000,
183
+ };
184
+ const result = mapVideoJobSnapshot(response);
185
+ expect(result.ok).toBe(true);
186
+ if (result.ok) {
187
+ expect(result.value.status).toBe('failed');
188
+ expect(result.value.error?.code).toBe('PROVIDER_UPSTREAM_ERROR');
189
+ expect(result.value.error?.message).toBe('Content policy violation');
190
+ expect(result.value.output).toBeUndefined();
191
+ }
192
+ });
193
+
194
+ it('returns error when task id is empty', () => {
195
+ const response: IBytedanceVideoTaskResponse = {
196
+ id: '',
197
+ status: 'completed',
198
+ created_at: 1700000000,
199
+ };
200
+ const result = mapVideoJobSnapshot(response);
201
+ expect(result.ok).toBe(false);
202
+ if (!result.ok) {
203
+ expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
204
+ expect(result.error.message).toContain('missing task id');
205
+ }
206
+ });
207
+
208
+ it('returns error when task id is whitespace-only', () => {
209
+ const response: IBytedanceVideoTaskResponse = {
210
+ id: ' ',
211
+ status: 'completed',
212
+ created_at: 1700000000,
213
+ };
214
+ const result = mapVideoJobSnapshot(response);
215
+ expect(result.ok).toBe(false);
216
+ });
217
+
218
+ it('returns error for unknown status', () => {
219
+ const response: IBytedanceVideoTaskResponse = {
220
+ id: 'task-3',
221
+ status: 'bizarre_state',
222
+ created_at: 1700000000,
223
+ };
224
+ const result = mapVideoJobSnapshot(response);
225
+ expect(result.ok).toBe(false);
226
+ if (!result.ok) {
227
+ expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
228
+ }
229
+ });
230
+
231
+ it('prefers updated_at over created_at for timestamp', () => {
232
+ const response: IBytedanceVideoTaskResponse = {
233
+ id: 'task-4',
234
+ status: 'queued',
235
+ created_at: 1700000000,
236
+ updated_at: 1700001000,
237
+ };
238
+ const result = mapVideoJobSnapshot(response);
239
+ expect(result.ok).toBe(true);
240
+ if (result.ok) {
241
+ expect(result.value.updatedAt).toBe(new Date(1700001000 * 1000).toISOString());
242
+ }
243
+ });
244
+
245
+ it('falls back to created_at when updated_at is absent', () => {
246
+ const response: IBytedanceVideoTaskResponse = {
247
+ id: 'task-5',
248
+ status: 'queued',
249
+ created_at: 1700000000,
250
+ };
251
+ const result = mapVideoJobSnapshot(response);
252
+ expect(result.ok).toBe(true);
253
+ if (result.ok) {
254
+ expect(result.value.updatedAt).toBe(new Date(1700000000 * 1000).toISOString());
255
+ }
256
+ });
257
+
258
+ it('resolves video url from content.video_url when direct url is absent', () => {
259
+ const response: IBytedanceVideoTaskResponse = {
260
+ id: 'task-6',
261
+ status: 'succeeded',
262
+ content: { video_url: 'https://cdn.test/content-video.mp4' },
263
+ created_at: 1700000000,
264
+ };
265
+ const result = mapVideoJobSnapshot(response);
266
+ expect(result.ok).toBe(true);
267
+ if (result.ok) {
268
+ expect(result.value.output?.uri).toBe('https://cdn.test/content-video.mp4');
269
+ }
270
+ });
271
+
272
+ it('prefers direct video_url over content.video_url', () => {
273
+ const response: IBytedanceVideoTaskResponse = {
274
+ id: 'task-7',
275
+ status: 'succeeded',
276
+ video_url: 'https://cdn.test/direct.mp4',
277
+ content: { video_url: 'https://cdn.test/content.mp4' },
278
+ created_at: 1700000000,
279
+ };
280
+ const result = mapVideoJobSnapshot(response);
281
+ expect(result.ok).toBe(true);
282
+ if (result.ok) {
283
+ expect(result.value.output?.uri).toBe('https://cdn.test/direct.mp4');
284
+ }
285
+ });
286
+
287
+ it('returns undefined output when no video url is present', () => {
288
+ const response: IBytedanceVideoTaskResponse = {
289
+ id: 'task-8',
290
+ status: 'queued',
291
+ created_at: 1700000000,
292
+ };
293
+ const result = mapVideoJobSnapshot(response);
294
+ expect(result.ok).toBe(true);
295
+ if (result.ok) {
296
+ expect(result.value.output).toBeUndefined();
297
+ }
298
+ });
299
+ });
@@ -0,0 +1,141 @@
1
+ import type { TProviderMediaResult, IVideoJobSnapshot } from '@robota-sdk/agent-core';
2
+ import type { IBytedanceVideoTaskResponse } from './types';
3
+
4
+ /** Threshold (in milliseconds) above which a numeric timestamp is treated as milliseconds rather than seconds. */
5
+ const MILLISECOND_EPOCH_THRESHOLD = 1_000_000_000_000;
6
+
7
+ /** Conversion factor from seconds to milliseconds. */
8
+ const MS_PER_SECOND = 1000;
9
+
10
+ /** Maps a Bytedance video task response into a normalized job snapshot. */
11
+ export function mapVideoJobSnapshot(
12
+ response: IBytedanceVideoTaskResponse,
13
+ ): TProviderMediaResult<IVideoJobSnapshot> {
14
+ if (response.id.trim().length === 0) {
15
+ return {
16
+ ok: false,
17
+ error: {
18
+ code: 'PROVIDER_UPSTREAM_ERROR',
19
+ message: 'Bytedance video job response is missing task id.',
20
+ },
21
+ };
22
+ }
23
+ const normalizedStatusResult = mapVideoStatus(response.status);
24
+ if (!normalizedStatusResult.ok) {
25
+ return normalizedStatusResult;
26
+ }
27
+ return {
28
+ ok: true,
29
+ value: {
30
+ jobId: response.id,
31
+ status: normalizedStatusResult.value,
32
+ output: mapOutput(response),
33
+ error:
34
+ normalizedStatusResult.value === 'failed' && response.error_message
35
+ ? { code: 'PROVIDER_UPSTREAM_ERROR', message: response.error_message }
36
+ : undefined,
37
+ updatedAt: toIsoTimestamp(response.updated_at ?? response.created_at),
38
+ },
39
+ };
40
+ }
41
+
42
+ /** Maps a Bytedance status string to a normalized video job status. */
43
+ export function mapVideoStatus(status: string): TProviderMediaResult<IVideoJobSnapshot['status']> {
44
+ const normalized = status.trim().toLowerCase();
45
+ if (normalized === 'queued' || normalized === 'pending' || normalized === 'submitted') {
46
+ return { ok: true, value: 'queued' };
47
+ }
48
+ if (normalized === 'running' || normalized === 'processing' || normalized === 'in_progress') {
49
+ return { ok: true, value: 'running' };
50
+ }
51
+ if (normalized === 'succeeded' || normalized === 'success' || normalized === 'completed') {
52
+ return { ok: true, value: 'succeeded' };
53
+ }
54
+ if (normalized === 'failed' || normalized === 'error') {
55
+ return { ok: true, value: 'failed' };
56
+ }
57
+ if (normalized === 'cancelled' || normalized === 'canceled') {
58
+ return { ok: true, value: 'cancelled' };
59
+ }
60
+ return {
61
+ ok: false,
62
+ error: {
63
+ code: 'PROVIDER_UPSTREAM_ERROR',
64
+ message: `Unexpected video job status from Bytedance: ${status}`,
65
+ },
66
+ };
67
+ }
68
+
69
+ /** Maps the initial createVideo status to queued or running. */
70
+ export function mapInitialStatus(
71
+ statusValue: string | undefined,
72
+ ): TProviderMediaResult<'queued' | 'running'> {
73
+ if (typeof statusValue !== 'string' || statusValue.trim().length === 0) {
74
+ return { ok: true, value: 'queued' };
75
+ }
76
+ const normalizedStatus = statusValue.trim().toLowerCase();
77
+ if (
78
+ normalizedStatus === 'queued' ||
79
+ normalizedStatus === 'pending' ||
80
+ normalizedStatus === 'submitted'
81
+ ) {
82
+ return { ok: true, value: 'queued' };
83
+ }
84
+ if (
85
+ normalizedStatus === 'running' ||
86
+ normalizedStatus === 'processing' ||
87
+ normalizedStatus === 'in_progress'
88
+ ) {
89
+ return { ok: true, value: 'running' };
90
+ }
91
+ return {
92
+ ok: false,
93
+ error: {
94
+ code: 'PROVIDER_UPSTREAM_ERROR',
95
+ message: `Unexpected createVideo status from Bytedance: ${statusValue}`,
96
+ },
97
+ };
98
+ }
99
+
100
+ /** Converts a numeric or string timestamp to ISO 8601 format. */
101
+ export function toIsoTimestamp(value: string | number | undefined): string {
102
+ if (typeof value === 'number' && Number.isFinite(value)) {
103
+ const numericTimestamp = value > MILLISECOND_EPOCH_THRESHOLD ? value : value * MS_PER_SECOND;
104
+ const date = new Date(numericTimestamp);
105
+ if (!Number.isNaN(date.getTime())) {
106
+ return date.toISOString();
107
+ }
108
+ }
109
+ if (typeof value === 'string' && value.trim().length > 0) {
110
+ const maybeNumeric = Number(value);
111
+ if (Number.isFinite(maybeNumeric)) {
112
+ return toIsoTimestamp(maybeNumeric);
113
+ }
114
+ const parsedDate = new Date(value);
115
+ if (!Number.isNaN(parsedDate.getTime())) {
116
+ return parsedDate.toISOString();
117
+ }
118
+ }
119
+ return new Date().toISOString();
120
+ }
121
+
122
+ function mapOutput(response: IBytedanceVideoTaskResponse): IVideoJobSnapshot['output'] {
123
+ const directVideoUrl =
124
+ typeof response.video_url === 'string' && response.video_url.trim().length > 0
125
+ ? response.video_url
126
+ : undefined;
127
+ const contentVideoUrl =
128
+ typeof response.content?.video_url === 'string' && response.content.video_url.trim().length > 0
129
+ ? response.content.video_url
130
+ : undefined;
131
+ const resolvedVideoUrl = directVideoUrl ?? contentVideoUrl;
132
+ if (typeof resolvedVideoUrl !== 'string') {
133
+ return undefined;
134
+ }
135
+ return {
136
+ kind: 'uri',
137
+ uri: resolvedVideoUrl,
138
+ mimeType: response.mime_type,
139
+ bytes: response.bytes,
140
+ };
141
+ }
@@ -0,0 +1,68 @@
1
+ import type { TUniversalValue } from '@robota-sdk/agent-core';
2
+
3
+ export interface IBytedanceProviderOptions {
4
+ apiKey: string;
5
+ baseUrl: string;
6
+ createVideoPath?: string;
7
+ getVideoTaskPathTemplate?: string;
8
+ cancelVideoTaskPathTemplate?: string;
9
+ cancelVideoTaskMethod?: 'POST' | 'DELETE';
10
+ timeoutMs?: number;
11
+ defaultHeaders?: Record<string, string>;
12
+ }
13
+
14
+ export interface IBytedanceTaskContentText {
15
+ type: 'text';
16
+ text: string;
17
+ }
18
+
19
+ export interface IBytedanceTaskContentImageUrl {
20
+ type: 'image_url';
21
+ image_url: {
22
+ url: string;
23
+ };
24
+ }
25
+
26
+ export type TBytedanceTaskContent = IBytedanceTaskContentText | IBytedanceTaskContentImageUrl;
27
+
28
+ export interface IBytedanceCreateVideoTaskRequest {
29
+ model: string;
30
+ content: TBytedanceTaskContent[];
31
+ generate_audio?: boolean;
32
+ ratio?: string;
33
+ duration?: number;
34
+ watermark?: boolean;
35
+ }
36
+
37
+ export interface IBytedanceCreateVideoTaskResponse {
38
+ id: string;
39
+ status?: string;
40
+ created_at?: string | number;
41
+ }
42
+
43
+ export interface IBytedanceTaskContentVideoUrl {
44
+ type: 'video_url';
45
+ video_url: {
46
+ url: string;
47
+ };
48
+ }
49
+
50
+ export interface IBytedanceVideoTaskResponse {
51
+ id: string;
52
+ status: string;
53
+ video_url?: string;
54
+ content?: {
55
+ video_url?: string;
56
+ };
57
+ mime_type?: string;
58
+ bytes?: number;
59
+ error_message?: string;
60
+ created_at?: string | number;
61
+ updated_at?: string | number;
62
+ }
63
+
64
+ export interface IBytedanceApiErrorResponse {
65
+ code?: string;
66
+ message?: string;
67
+ details?: Record<string, TUniversalValue>;
68
+ }
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_DEEPSEEK_PROVIDER_MODEL = 'deepseek-v4-flash';
2
+ export const DEFAULT_DEEPSEEK_PROVIDER_API_KEY_ENV = 'DEEPSEEK_API_KEY';
3
+ export const DEFAULT_DEEPSEEK_PROVIDER_API_KEY_REFERENCE = `$ENV:${DEFAULT_DEEPSEEK_PROVIDER_API_KEY_ENV}`;
4
+ export const DEFAULT_DEEPSEEK_PROVIDER_BASE_URL = 'https://api.deepseek.com';
@@ -0,0 +1,22 @@
1
+ export { DeepSeekProvider } from './provider';
2
+ export { createDeepSeekProviderDefinition } from './provider-definition';
3
+ export {
4
+ DEFAULT_DEEPSEEK_PROVIDER_API_KEY_ENV,
5
+ DEFAULT_DEEPSEEK_PROVIDER_API_KEY_REFERENCE,
6
+ DEFAULT_DEEPSEEK_PROVIDER_BASE_URL,
7
+ DEFAULT_DEEPSEEK_PROVIDER_MODEL,
8
+ } from './defaults';
9
+ export { refreshDeepSeekModelCatalog } from './model-catalog-refresh';
10
+ export {
11
+ DEEPSEEK_DEPRECATED_ALIAS_RETIREMENT_DATE,
12
+ DEEPSEEK_MODEL_CATALOG_SOURCE_URL,
13
+ DEEPSEEK_MODEL_LAST_VERIFIED_AT,
14
+ DEEPSEEK_MODEL_LIST_SOURCE_URL,
15
+ } from './model-catalog';
16
+ export type {
17
+ IDeepSeekProviderOptions,
18
+ IDeepSeekThinkingConfig,
19
+ TDeepSeekProviderOptionValue,
20
+ TDeepSeekReasoningEffort,
21
+ TDeepSeekThinkingMode,
22
+ } from './types';
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { refreshDeepSeekModelCatalog } from './model-catalog-refresh';
3
+
4
+ describe('refreshDeepSeekModelCatalog', () => {
5
+ it('maps the live DeepSeek /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: 'deepseek-v4-flash', object: 'model', owned_by: 'deepseek' },
12
+ { id: 'deepseek-v4-pro', object: 'model', owned_by: 'deepseek' },
13
+ { id: 'custom-deepseek', object: 'model', owned_by: 'deepseek' },
14
+ ],
15
+ }),
16
+ }));
17
+
18
+ const catalog = await refreshDeepSeekModelCatalog(
19
+ {
20
+ baseURL: 'https://api.deepseek.com/',
21
+ apiKey: 'deepseek-key',
22
+ },
23
+ fetcher,
24
+ );
25
+
26
+ expect(fetcher).toHaveBeenCalledWith('https://api.deepseek.com/models', {
27
+ headers: {
28
+ Authorization: 'Bearer deepseek-key',
29
+ },
30
+ });
31
+ expect(catalog.status).toBe('live');
32
+ expect(catalog.entries?.map((entry) => entry.id)).toEqual([
33
+ 'deepseek-v4-flash',
34
+ 'deepseek-v4-pro',
35
+ 'custom-deepseek',
36
+ ]);
37
+ expect(catalog.entries?.[0]).toMatchObject({
38
+ displayName: 'DeepSeek V4 Flash',
39
+ capabilities: ['tools', 'reasoning', 'json_schema', 'streaming'],
40
+ });
41
+ });
42
+
43
+ it('returns an unavailable catalog when the live model refresh fails', async () => {
44
+ const fetcher = vi.fn(async () => ({
45
+ ok: false,
46
+ status: 401,
47
+ json: async () => ({ data: [] }),
48
+ }));
49
+
50
+ const catalog = await refreshDeepSeekModelCatalog({}, fetcher);
51
+
52
+ expect(catalog).toMatchObject({
53
+ status: 'unavailable',
54
+ message: 'DeepSeek model refresh failed: HTTP 401',
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,105 @@
1
+ import type {
2
+ IProviderModelCatalog,
3
+ IProviderModelCatalogEntry,
4
+ IProviderProfileConfig,
5
+ } from '@robota-sdk/agent-core';
6
+ import { DEFAULT_DEEPSEEK_PROVIDER_BASE_URL } from './defaults';
7
+ import {
8
+ DEEPSEEK_MODEL_CATALOG_SOURCE_URL,
9
+ DEEPSEEK_MODEL_LAST_VERIFIED_AT,
10
+ getDeepSeekFallbackModelCatalogEntry,
11
+ } from './model-catalog';
12
+
13
+ export interface IDeepSeekModelsResponse {
14
+ data?: Array<{
15
+ id?: string;
16
+ object?: string;
17
+ owned_by?: string;
18
+ }>;
19
+ }
20
+
21
+ export interface IDeepSeekFetchInit {
22
+ headers?: Record<string, string>;
23
+ }
24
+
25
+ export interface IDeepSeekFetchResponse {
26
+ ok: boolean;
27
+ status: number;
28
+ json: () => Promise<IDeepSeekModelsResponse>;
29
+ }
30
+
31
+ export type TDeepSeekFetch = (
32
+ url: string,
33
+ init?: IDeepSeekFetchInit,
34
+ ) => Promise<IDeepSeekFetchResponse>;
35
+
36
+ export async function refreshDeepSeekModelCatalog(
37
+ profile: IProviderProfileConfig,
38
+ fetcher: TDeepSeekFetch = defaultDeepSeekFetch,
39
+ ): Promise<IProviderModelCatalog> {
40
+ const baseURL = trimTrailingSlash(profile.baseURL ?? DEFAULT_DEEPSEEK_PROVIDER_BASE_URL);
41
+ const url = `${baseURL}/models`;
42
+ const response = await fetcher(url, buildFetchInit(profile.apiKey));
43
+
44
+ if (!response.ok) {
45
+ return {
46
+ status: 'unavailable',
47
+ sourceUrl: DEEPSEEK_MODEL_CATALOG_SOURCE_URL,
48
+ message: `DeepSeek model refresh failed: HTTP ${response.status}`,
49
+ };
50
+ }
51
+
52
+ const body = await response.json();
53
+ const entries = (body.data ?? [])
54
+ .map((model) => model.id)
55
+ .filter((id): id is string => typeof id === 'string' && id.length > 0)
56
+ .map(toModelCatalogEntry);
57
+
58
+ return {
59
+ status: 'live',
60
+ sourceUrl: DEEPSEEK_MODEL_CATALOG_SOURCE_URL,
61
+ lastVerifiedAt: DEEPSEEK_MODEL_LAST_VERIFIED_AT,
62
+ entries,
63
+ };
64
+ }
65
+
66
+ function buildFetchInit(apiKey: string | undefined): IDeepSeekFetchInit | undefined {
67
+ if (!apiKey) {
68
+ return undefined;
69
+ }
70
+ return {
71
+ headers: {
72
+ Authorization: `Bearer ${apiKey}`,
73
+ },
74
+ };
75
+ }
76
+
77
+ function toModelCatalogEntry(id: string): IProviderModelCatalogEntry {
78
+ return (
79
+ getDeepSeekFallbackModelCatalogEntry(id) ?? {
80
+ id,
81
+ displayName: id,
82
+ lifecycle: 'active',
83
+ sourceUrl: DEEPSEEK_MODEL_CATALOG_SOURCE_URL,
84
+ lastVerifiedAt: DEEPSEEK_MODEL_LAST_VERIFIED_AT,
85
+ }
86
+ );
87
+ }
88
+
89
+ function trimTrailingSlash(value: string): string {
90
+ return value.replace(/\/$/, '');
91
+ }
92
+
93
+ async function defaultDeepSeekFetch(
94
+ url: string,
95
+ init?: IDeepSeekFetchInit,
96
+ ): Promise<IDeepSeekFetchResponse> {
97
+ const response = await fetch(url, {
98
+ ...(init?.headers !== undefined && { headers: init.headers }),
99
+ });
100
+ return {
101
+ ok: response.ok,
102
+ status: response.status,
103
+ json: () => response.json() as Promise<IDeepSeekModelsResponse>,
104
+ };
105
+ }