@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.
- package/LICENSE +21 -0
- package/dist/browser/index.d.ts +1104 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +7 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/loggers/index.cjs +1 -0
- package/dist/loggers/index.d.ts +151 -0
- package/dist/loggers/index.d.ts.map +1 -0
- package/dist/loggers/index.js +2 -0
- package/dist/loggers/index.js.map +1 -0
- package/dist/node/anthropic/index.cjs +1 -0
- package/dist/node/anthropic/index.d.ts +158 -0
- package/dist/node/anthropic/index.d.ts.map +1 -0
- package/dist/node/anthropic/index.js +1 -0
- package/dist/node/anthropic--1vgLC-e.js +5 -0
- package/dist/node/anthropic--1vgLC-e.js.map +1 -0
- package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
- package/dist/node/bytedance/index.cjs +1 -0
- package/dist/node/bytedance/index.d.ts +74 -0
- package/dist/node/bytedance/index.d.ts.map +1 -0
- package/dist/node/bytedance/index.js +1 -0
- package/dist/node/bytedance-C_0sF_pJ.js +2 -0
- package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
- package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
- package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
- package/dist/node/deepseek/index.cjs +1 -0
- package/dist/node/deepseek/index.d.ts +2 -0
- package/dist/node/deepseek/index.js +1 -0
- package/dist/node/deepseek-_8Ixx7rA.js +2 -0
- package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
- package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
- package/dist/node/gemini/index.cjs +1 -0
- package/dist/node/gemini/index.d.ts +173 -0
- package/dist/node/gemini/index.d.ts.map +1 -0
- package/dist/node/gemini/index.js +1 -0
- package/dist/node/gemini-Bh2U87MY.js +4 -0
- package/dist/node/gemini-Bh2U87MY.js.map +1 -0
- package/dist/node/gemini-DSaNCxZj.cjs +3 -0
- package/dist/node/gemma/index.cjs +1 -0
- package/dist/node/gemma/index.d.ts +2 -0
- package/dist/node/gemma/index.js +1 -0
- package/dist/node/gemma-Dp_AfCUR.js +2 -0
- package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
- package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
- package/dist/node/google/index.cjs +1 -0
- package/dist/node/google/index.d.ts +14 -0
- package/dist/node/google/index.d.ts.map +1 -0
- package/dist/node/google/index.js +2 -0
- package/dist/node/google/index.js.map +1 -0
- package/dist/node/index-B6PnlDMd.d.ts +82 -0
- package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
- package/dist/node/index-B7UvPJcI.d.ts +315 -0
- package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
- package/dist/node/index-BLPOTNb5.d.ts +98 -0
- package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
- package/dist/node/index-BqixM_XD.d.ts +231 -0
- package/dist/node/index-BqixM_XD.d.ts.map +1 -0
- package/dist/node/index-C3beaqKO.d.ts +231 -0
- package/dist/node/index-C3beaqKO.d.ts.map +1 -0
- package/dist/node/index-Cp2XRh9G.d.ts +82 -0
- package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
- package/dist/node/index-DSv5xruI.d.ts +98 -0
- package/dist/node/index-DSv5xruI.d.ts.map +1 -0
- package/dist/node/index-w0bV1uaP.d.ts +315 -0
- package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +8 -0
- package/dist/node/index.js +1 -0
- package/dist/node/openai/index.cjs +1 -0
- package/dist/node/openai/index.d.ts +2 -0
- package/dist/node/openai/index.js +1 -0
- package/dist/node/openai-CRQjg4xF.js +2 -0
- package/dist/node/openai-CRQjg4xF.js.map +1 -0
- package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
- package/dist/node/openai-xWC6pY7r.cjs +1 -0
- package/dist/node/qwen/index.cjs +1 -0
- package/dist/node/qwen/index.d.ts +2 -0
- package/dist/node/qwen/index.js +1 -0
- package/dist/node/qwen-ChUZobTL.js +2 -0
- package/dist/node/qwen-ChUZobTL.js.map +1 -0
- package/dist/node/qwen-CjT71vSM.cjs +1 -0
- package/package.json +157 -0
- package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
- package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
- package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
- package/src/anthropic/__tests__/provider.test.ts +1357 -0
- package/src/anthropic/__tests__/response-parser.test.ts +326 -0
- package/src/anthropic/index.ts +22 -0
- package/src/anthropic/message-converter.ts +181 -0
- package/src/anthropic/model-catalog-refresh.ts +128 -0
- package/src/anthropic/parsers/response-parser.ts +184 -0
- package/src/anthropic/provider-definition.ts +93 -0
- package/src/anthropic/provider.ts +290 -0
- package/src/anthropic/streaming-handler.ts +204 -0
- package/src/anthropic/types/api-types.ts +158 -0
- package/src/anthropic/types.ts +79 -0
- package/src/bytedance/http-client.test.ts +288 -0
- package/src/bytedance/http-client.ts +163 -0
- package/src/bytedance/index.ts +2 -0
- package/src/bytedance/provider.spec.ts +320 -0
- package/src/bytedance/provider.ts +171 -0
- package/src/bytedance/status-mapper.test.ts +299 -0
- package/src/bytedance/status-mapper.ts +141 -0
- package/src/bytedance/types.ts +68 -0
- package/src/deepseek/defaults.ts +4 -0
- package/src/deepseek/index.ts +22 -0
- package/src/deepseek/model-catalog-refresh.test.ts +57 -0
- package/src/deepseek/model-catalog-refresh.ts +105 -0
- package/src/deepseek/model-catalog.ts +55 -0
- package/src/deepseek/provider-definition.test.ts +109 -0
- package/src/deepseek/provider-definition.ts +132 -0
- package/src/deepseek/provider.test.ts +324 -0
- package/src/deepseek/provider.ts +298 -0
- package/src/deepseek/types.ts +37 -0
- package/src/gemini/execution-helpers.ts +233 -0
- package/src/gemini/genai-transport.test.ts +208 -0
- package/src/gemini/image-operations.test.ts +448 -0
- package/src/gemini/image-operations.ts +261 -0
- package/src/gemini/index.ts +11 -0
- package/src/gemini/message-converter.test.ts +616 -0
- package/src/gemini/message-converter.ts +140 -0
- package/src/gemini/model-catalog-refresh.test.ts +107 -0
- package/src/gemini/model-catalog-refresh.ts +92 -0
- package/src/gemini/provider-definition.test.ts +70 -0
- package/src/gemini/provider-definition.ts +78 -0
- package/src/gemini/provider-extended.test.ts +898 -0
- package/src/gemini/provider.spec.ts +216 -0
- package/src/gemini/provider.ts +279 -0
- package/src/gemini/request-converter.ts +226 -0
- package/src/gemini/tool-schema-converter.ts +78 -0
- package/src/gemini/types/api-types.ts +235 -0
- package/src/gemini/types.ts +121 -0
- package/src/gemma/index.ts +5 -0
- package/src/gemma/message-factory.ts +38 -0
- package/src/gemma/provider-definition.test.ts +43 -0
- package/src/gemma/provider-definition.ts +84 -0
- package/src/gemma/provider-projection.ts +49 -0
- package/src/gemma/provider.test.ts +628 -0
- package/src/gemma/provider.ts +308 -0
- package/src/gemma/pseudo-command-envelope.ts +58 -0
- package/src/gemma/pseudo-tool-call-projector.ts +243 -0
- package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
- package/src/gemma/pseudo-tool-call-types.ts +31 -0
- package/src/gemma/reasoning-projector.test.ts +52 -0
- package/src/gemma/reasoning-projector.ts +144 -0
- package/src/gemma/streaming-projection.ts +79 -0
- package/src/gemma/tool-call-argument-parser.ts +126 -0
- package/src/gemma/tool-call-projector.test.ts +227 -0
- package/src/gemma/tool-call-projector.ts +264 -0
- package/src/gemma/types.ts +27 -0
- package/src/google/index.ts +11 -0
- package/src/google/provider-compat.test.ts +19 -0
- package/src/google/provider-definition.ts +6 -0
- package/src/google/provider.ts +10 -0
- package/src/google/types.ts +5 -0
- package/src/index.ts +9 -0
- package/src/openai/adapter.test.ts +494 -0
- package/src/openai/adapter.ts +145 -0
- package/src/openai/chat-completions-chat.ts +189 -0
- package/src/openai/executor-integration.test.ts +206 -0
- package/src/openai/index.ts +21 -0
- package/src/openai/interfaces/payload-logger.ts +48 -0
- package/src/openai/loggers/console-payload-logger.test.ts +173 -0
- package/src/openai/loggers/console-payload-logger.ts +94 -0
- package/src/openai/loggers/console.ts +9 -0
- package/src/openai/loggers/file-payload-logger.test.ts +238 -0
- package/src/openai/loggers/file-payload-logger.ts +112 -0
- package/src/openai/loggers/file.ts +9 -0
- package/src/openai/loggers/index.ts +12 -0
- package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
- package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
- package/src/openai/message-converter.ts +22 -0
- package/src/openai/model-catalog-refresh.test.ts +92 -0
- package/src/openai/model-catalog-refresh.ts +115 -0
- package/src/openai/openai-request-format.ts +92 -0
- package/src/openai/parsers/response-parser.test.ts +407 -0
- package/src/openai/parsers/response-parser.ts +47 -0
- package/src/openai/provider-definition.test.ts +75 -0
- package/src/openai/provider-definition.ts +132 -0
- package/src/openai/provider.test.ts +1402 -0
- package/src/openai/provider.ts +237 -0
- package/src/openai/responses-chat.ts +258 -0
- package/src/openai/responses-converter.ts +112 -0
- package/src/openai/responses-parser.ts +285 -0
- package/src/openai/responses-stream-utils.ts +45 -0
- package/src/openai/responses-types.ts +195 -0
- package/src/openai/streaming/stream-assembler.ts +3 -0
- package/src/openai/streaming/stream-handler.test.ts +367 -0
- package/src/openai/streaming/stream-handler.ts +119 -0
- package/src/openai/types/api-types.ts +112 -0
- package/src/openai/types.ts +194 -0
- package/src/qwen/defaults.ts +26 -0
- package/src/qwen/index.ts +5 -0
- package/src/qwen/model-catalog-refresh.test.ts +91 -0
- package/src/qwen/model-catalog-refresh.ts +97 -0
- package/src/qwen/provider-capabilities.ts +34 -0
- package/src/qwen/provider-definition.test.ts +139 -0
- package/src/qwen/provider-definition.ts +173 -0
- package/src/qwen/provider-streaming-assembly.ts +40 -0
- package/src/qwen/provider.test.ts +640 -0
- package/src/qwen/provider.ts +293 -0
- package/src/qwen/responses-chat.ts +194 -0
- package/src/qwen/responses-converter.ts +104 -0
- package/src/qwen/responses-parser.ts +299 -0
- package/src/qwen/responses-stream-utils.ts +38 -0
- package/src/qwen/types.ts +228 -0
- package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
- package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
- package/src/shared/openai-compatible/index.ts +6 -0
- package/src/shared/openai-compatible/message-converter.test.ts +111 -0
- package/src/shared/openai-compatible/message-converter.ts +84 -0
- package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
- package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
- package/src/shared/openai-compatible/response-parser.test.ts +172 -0
- package/src/shared/openai-compatible/response-parser.ts +180 -0
- package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
- package/src/shared/openai-compatible/stream-assembler.ts +248 -0
- package/src/shared/openai-compatible/types.ts +59 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { BytedanceProvider } from './provider';
|
|
3
|
+
|
|
4
|
+
describe('BytedanceProvider', () => {
|
|
5
|
+
const fetchMock = vi.fn();
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
fetchMock.mockReset();
|
|
9
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('maps createVideo response to accepted job shape', async () => {
|
|
13
|
+
fetchMock.mockResolvedValue({
|
|
14
|
+
ok: true,
|
|
15
|
+
text: async () => JSON.stringify({ id: 'task-1', status: 'queued' }),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const provider = new BytedanceProvider({
|
|
19
|
+
apiKey: 'test-key',
|
|
20
|
+
baseUrl: 'https://api.byteplus.test',
|
|
21
|
+
});
|
|
22
|
+
const result = await provider.createVideo({
|
|
23
|
+
prompt: 'test prompt',
|
|
24
|
+
model: 'seedance-2.0',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(result.ok).toBe(true);
|
|
28
|
+
if (!result.ok) {
|
|
29
|
+
throw new Error('Expected successful result');
|
|
30
|
+
}
|
|
31
|
+
expect(result.value.jobId).toBe('task-1');
|
|
32
|
+
expect(result.value.status).toBe('queued');
|
|
33
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
34
|
+
'https://api.byteplus.test/contents/generations/tasks',
|
|
35
|
+
expect.objectContaining({
|
|
36
|
+
method: 'POST',
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('maps createVideo response without status as queued', async () => {
|
|
42
|
+
fetchMock.mockResolvedValue({
|
|
43
|
+
ok: true,
|
|
44
|
+
text: async () => JSON.stringify({ id: 'task-no-status' }),
|
|
45
|
+
});
|
|
46
|
+
const provider = new BytedanceProvider({
|
|
47
|
+
apiKey: 'test-key',
|
|
48
|
+
baseUrl: 'https://api.byteplus.test',
|
|
49
|
+
});
|
|
50
|
+
const result = await provider.createVideo({
|
|
51
|
+
prompt: 'test prompt',
|
|
52
|
+
model: 'seedance-1-5-pro-251215',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result.ok).toBe(true);
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
throw new Error('Expected successful result');
|
|
58
|
+
}
|
|
59
|
+
expect(result.value.status).toBe('queued');
|
|
60
|
+
expect(result.value.jobId).toBe('task-no-status');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('maps getVideoJob success with output uri', async () => {
|
|
64
|
+
fetchMock.mockResolvedValue({
|
|
65
|
+
ok: true,
|
|
66
|
+
text: async () =>
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
id: 'task-2',
|
|
69
|
+
status: 'completed',
|
|
70
|
+
video_url: 'https://cdn.test/video.mp4',
|
|
71
|
+
mime_type: 'video/mp4',
|
|
72
|
+
bytes: 1024,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const provider = new BytedanceProvider({
|
|
77
|
+
apiKey: 'test-key',
|
|
78
|
+
baseUrl: 'https://api.byteplus.test',
|
|
79
|
+
});
|
|
80
|
+
const result = await provider.getVideoJob('job-2');
|
|
81
|
+
|
|
82
|
+
expect(result.ok).toBe(true);
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
throw new Error('Expected successful result');
|
|
85
|
+
}
|
|
86
|
+
expect(result.value.status).toBe('succeeded');
|
|
87
|
+
expect(result.value.output?.uri).toBe('https://cdn.test/video.mp4');
|
|
88
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
89
|
+
'https://api.byteplus.test/contents/generations/tasks/job-2',
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
method: 'GET',
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('maps 404 response to PROVIDER_JOB_NOT_FOUND', async () => {
|
|
97
|
+
fetchMock.mockResolvedValue({
|
|
98
|
+
ok: false,
|
|
99
|
+
status: 404,
|
|
100
|
+
text: async () => JSON.stringify({ message: 'not found' }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const provider = new BytedanceProvider({
|
|
104
|
+
apiKey: 'test-key',
|
|
105
|
+
baseUrl: 'https://api.byteplus.test',
|
|
106
|
+
});
|
|
107
|
+
const result = await provider.getVideoJob('missing-job');
|
|
108
|
+
|
|
109
|
+
expect(result.ok).toBe(false);
|
|
110
|
+
if (result.ok) {
|
|
111
|
+
throw new Error('Expected failed result');
|
|
112
|
+
}
|
|
113
|
+
expect(result.error.code).toBe('PROVIDER_JOB_NOT_FOUND');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects createVideo with empty prompt', async () => {
|
|
117
|
+
const provider = new BytedanceProvider({
|
|
118
|
+
apiKey: 'test-key',
|
|
119
|
+
baseUrl: 'https://api.byteplus.test',
|
|
120
|
+
});
|
|
121
|
+
const result = await provider.createVideo({ prompt: '', model: 'seedance-2.0' });
|
|
122
|
+
expect(result.ok).toBe(false);
|
|
123
|
+
if (!result.ok) {
|
|
124
|
+
expect(result.error.code).toBe('PROVIDER_INVALID_REQUEST');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('rejects createVideo with empty model', async () => {
|
|
129
|
+
const provider = new BytedanceProvider({
|
|
130
|
+
apiKey: 'test-key',
|
|
131
|
+
baseUrl: 'https://api.byteplus.test',
|
|
132
|
+
});
|
|
133
|
+
const result = await provider.createVideo({ prompt: 'test', model: '' });
|
|
134
|
+
expect(result.ok).toBe(false);
|
|
135
|
+
if (!result.ok) {
|
|
136
|
+
expect(result.error.code).toBe('PROVIDER_INVALID_REQUEST');
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('rejects createVideo when seed is provided', async () => {
|
|
141
|
+
const provider = new BytedanceProvider({
|
|
142
|
+
apiKey: 'test-key',
|
|
143
|
+
baseUrl: 'https://api.byteplus.test',
|
|
144
|
+
});
|
|
145
|
+
const result = await provider.createVideo({ prompt: 'test', model: 'seedance-2.0', seed: 42 });
|
|
146
|
+
expect(result.ok).toBe(false);
|
|
147
|
+
if (!result.ok) {
|
|
148
|
+
expect(result.error.code).toBe('PROVIDER_INVALID_REQUEST');
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('rejects createVideo with empty task id in response', async () => {
|
|
153
|
+
fetchMock.mockResolvedValue({
|
|
154
|
+
ok: true,
|
|
155
|
+
text: async () => JSON.stringify({ id: '', status: 'queued' }),
|
|
156
|
+
});
|
|
157
|
+
const provider = new BytedanceProvider({
|
|
158
|
+
apiKey: 'test-key',
|
|
159
|
+
baseUrl: 'https://api.byteplus.test',
|
|
160
|
+
});
|
|
161
|
+
const result = await provider.createVideo({ prompt: 'test', model: 'seedance-2.0' });
|
|
162
|
+
expect(result.ok).toBe(false);
|
|
163
|
+
if (!result.ok) {
|
|
164
|
+
expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('includes image URI content in createVideo payload', async () => {
|
|
169
|
+
fetchMock.mockResolvedValue({
|
|
170
|
+
ok: true,
|
|
171
|
+
text: async () => JSON.stringify({ id: 'task-img', status: 'queued' }),
|
|
172
|
+
});
|
|
173
|
+
const provider = new BytedanceProvider({
|
|
174
|
+
apiKey: 'test-key',
|
|
175
|
+
baseUrl: 'https://api.byteplus.test',
|
|
176
|
+
});
|
|
177
|
+
const result = await provider.createVideo({
|
|
178
|
+
prompt: 'animate this',
|
|
179
|
+
model: 'seedance-2.0',
|
|
180
|
+
inputImages: [{ kind: 'uri', uri: 'https://example.com/img.png' }],
|
|
181
|
+
});
|
|
182
|
+
expect(result.ok).toBe(true);
|
|
183
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
184
|
+
expect(body.content).toHaveLength(2);
|
|
185
|
+
expect(body.content[1].type).toBe('image_url');
|
|
186
|
+
expect(body.content[1].image_url.url).toBe('https://example.com/img.png');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('includes inline base64 image content in createVideo payload', async () => {
|
|
190
|
+
fetchMock.mockResolvedValue({
|
|
191
|
+
ok: true,
|
|
192
|
+
text: async () => JSON.stringify({ id: 'task-b64', status: 'queued' }),
|
|
193
|
+
});
|
|
194
|
+
const provider = new BytedanceProvider({
|
|
195
|
+
apiKey: 'test-key',
|
|
196
|
+
baseUrl: 'https://api.byteplus.test',
|
|
197
|
+
});
|
|
198
|
+
const result = await provider.createVideo({
|
|
199
|
+
prompt: 'animate this',
|
|
200
|
+
model: 'seedance-2.0',
|
|
201
|
+
inputImages: [{ kind: 'inline', data: 'aGVsbG8=', mimeType: 'image/png' }],
|
|
202
|
+
});
|
|
203
|
+
expect(result.ok).toBe(true);
|
|
204
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
205
|
+
expect(body.content[1].image_url.url).toBe('data:image/png;base64,aGVsbG8=');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('rejects createVideo with empty image URI', async () => {
|
|
209
|
+
const provider = new BytedanceProvider({
|
|
210
|
+
apiKey: 'test-key',
|
|
211
|
+
baseUrl: 'https://api.byteplus.test',
|
|
212
|
+
});
|
|
213
|
+
const result = await provider.createVideo({
|
|
214
|
+
prompt: 'test',
|
|
215
|
+
model: 'seedance-2.0',
|
|
216
|
+
inputImages: [{ kind: 'uri', uri: '' }],
|
|
217
|
+
});
|
|
218
|
+
expect(result.ok).toBe(false);
|
|
219
|
+
if (!result.ok) {
|
|
220
|
+
expect(result.error.code).toBe('PROVIDER_INVALID_REQUEST');
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('rejects createVideo with empty inline image data', async () => {
|
|
225
|
+
const provider = new BytedanceProvider({
|
|
226
|
+
apiKey: 'test-key',
|
|
227
|
+
baseUrl: 'https://api.byteplus.test',
|
|
228
|
+
});
|
|
229
|
+
const result = await provider.createVideo({
|
|
230
|
+
prompt: 'test',
|
|
231
|
+
model: 'seedance-2.0',
|
|
232
|
+
inputImages: [{ kind: 'inline', data: '', mimeType: 'image/png' }],
|
|
233
|
+
});
|
|
234
|
+
expect(result.ok).toBe(false);
|
|
235
|
+
if (!result.ok) {
|
|
236
|
+
expect(result.error.code).toBe('PROVIDER_INVALID_REQUEST');
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('rejects createVideo with empty inline image mimeType', async () => {
|
|
241
|
+
const provider = new BytedanceProvider({
|
|
242
|
+
apiKey: 'test-key',
|
|
243
|
+
baseUrl: 'https://api.byteplus.test',
|
|
244
|
+
});
|
|
245
|
+
const result = await provider.createVideo({
|
|
246
|
+
prompt: 'test',
|
|
247
|
+
model: 'seedance-2.0',
|
|
248
|
+
inputImages: [{ kind: 'inline', data: 'aGVsbG8=', mimeType: '' }],
|
|
249
|
+
});
|
|
250
|
+
expect(result.ok).toBe(false);
|
|
251
|
+
if (!result.ok) {
|
|
252
|
+
expect(result.error.code).toBe('PROVIDER_INVALID_REQUEST');
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('rejects getVideoJob with empty jobId', async () => {
|
|
257
|
+
const provider = new BytedanceProvider({
|
|
258
|
+
apiKey: 'test-key',
|
|
259
|
+
baseUrl: 'https://api.byteplus.test',
|
|
260
|
+
});
|
|
261
|
+
const result = await provider.getVideoJob('');
|
|
262
|
+
expect(result.ok).toBe(false);
|
|
263
|
+
if (!result.ok) {
|
|
264
|
+
expect(result.error.code).toBe('PROVIDER_INVALID_REQUEST');
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('rejects cancelVideoJob with empty jobId', async () => {
|
|
269
|
+
const provider = new BytedanceProvider({
|
|
270
|
+
apiKey: 'test-key',
|
|
271
|
+
baseUrl: 'https://api.byteplus.test',
|
|
272
|
+
});
|
|
273
|
+
const result = await provider.cancelVideoJob('');
|
|
274
|
+
expect(result.ok).toBe(false);
|
|
275
|
+
if (!result.ok) {
|
|
276
|
+
expect(result.error.code).toBe('PROVIDER_INVALID_REQUEST');
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('uses POST method for cancelVideoJob when configured', async () => {
|
|
281
|
+
fetchMock.mockResolvedValue({
|
|
282
|
+
ok: true,
|
|
283
|
+
text: async () => JSON.stringify({ id: 'task-cancel', status: 'cancelled' }),
|
|
284
|
+
});
|
|
285
|
+
const provider = new BytedanceProvider({
|
|
286
|
+
apiKey: 'test-key',
|
|
287
|
+
baseUrl: 'https://api.byteplus.test',
|
|
288
|
+
cancelVideoTaskMethod: 'POST',
|
|
289
|
+
});
|
|
290
|
+
await provider.cancelVideoJob('task-cancel');
|
|
291
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
292
|
+
expect.any(String),
|
|
293
|
+
expect.objectContaining({ method: 'POST' }),
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('uses DELETE by default for cancelVideoJob', async () => {
|
|
298
|
+
fetchMock.mockResolvedValue({
|
|
299
|
+
ok: true,
|
|
300
|
+
text: async () =>
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
id: 'task-3',
|
|
303
|
+
status: 'cancelled',
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
const provider = new BytedanceProvider({
|
|
307
|
+
apiKey: 'test-key',
|
|
308
|
+
baseUrl: 'https://api.byteplus.test',
|
|
309
|
+
});
|
|
310
|
+
const result = await provider.cancelVideoJob('task-3');
|
|
311
|
+
|
|
312
|
+
expect(result.ok).toBe(true);
|
|
313
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
314
|
+
'https://api.byteplus.test/contents/generations/tasks/task-3',
|
|
315
|
+
expect.objectContaining({
|
|
316
|
+
method: 'DELETE',
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IInlineImageInputSource,
|
|
3
|
+
IUriImageInputSource,
|
|
4
|
+
TProviderMediaResult,
|
|
5
|
+
IVideoGenerationProvider,
|
|
6
|
+
IVideoGenerationRequest,
|
|
7
|
+
IVideoJobAccepted,
|
|
8
|
+
IVideoJobSnapshot,
|
|
9
|
+
} from '@robota-sdk/agent-core';
|
|
10
|
+
import type {
|
|
11
|
+
IBytedanceCreateVideoTaskRequest,
|
|
12
|
+
IBytedanceCreateVideoTaskResponse,
|
|
13
|
+
IBytedanceProviderOptions,
|
|
14
|
+
IBytedanceVideoTaskResponse,
|
|
15
|
+
TBytedanceTaskContent,
|
|
16
|
+
} from './types';
|
|
17
|
+
import { requestJson } from './http-client';
|
|
18
|
+
import { mapVideoJobSnapshot, mapInitialStatus, toIsoTimestamp } from './status-mapper';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CREATE_VIDEO_PATH = '/contents/generations/tasks';
|
|
21
|
+
const DEFAULT_GET_VIDEO_TASK_PATH_TEMPLATE = '/contents/generations/tasks/{taskId}';
|
|
22
|
+
const DEFAULT_CANCEL_VIDEO_TASK_PATH_TEMPLATE = '/contents/generations/tasks/{taskId}';
|
|
23
|
+
|
|
24
|
+
export class BytedanceProvider implements IVideoGenerationProvider {
|
|
25
|
+
private readonly options: IBytedanceProviderOptions;
|
|
26
|
+
|
|
27
|
+
public constructor(options: IBytedanceProviderOptions) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async createVideo(
|
|
32
|
+
request: IVideoGenerationRequest,
|
|
33
|
+
): Promise<TProviderMediaResult<IVideoJobAccepted>> {
|
|
34
|
+
if (request.prompt.trim().length === 0) {
|
|
35
|
+
return buildInvalidRequestError('Video generation requires non-empty prompt.');
|
|
36
|
+
}
|
|
37
|
+
if (request.model.trim().length === 0) {
|
|
38
|
+
return buildInvalidRequestError('Video generation requires non-empty model.');
|
|
39
|
+
}
|
|
40
|
+
if (typeof request.seed === 'number') {
|
|
41
|
+
return buildInvalidRequestError(
|
|
42
|
+
'ModelArk Seedance provider does not support seed field in current contract.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const contentResult = this.buildContentPayload(request.prompt, request.inputImages);
|
|
47
|
+
if (!contentResult.ok) {
|
|
48
|
+
return contentResult;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const payload: IBytedanceCreateVideoTaskRequest = {
|
|
52
|
+
model: request.model.trim(),
|
|
53
|
+
content: contentResult.value,
|
|
54
|
+
duration: request.durationSeconds,
|
|
55
|
+
ratio: request.aspectRatio,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const responseResult = await requestJson<IBytedanceCreateVideoTaskResponse>(this.options, {
|
|
59
|
+
path: this.options.createVideoPath ?? DEFAULT_CREATE_VIDEO_PATH,
|
|
60
|
+
method: 'POST',
|
|
61
|
+
body: JSON.stringify(payload),
|
|
62
|
+
});
|
|
63
|
+
if (!responseResult.ok) {
|
|
64
|
+
return responseResult;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (responseResult.value.id.trim().length === 0) {
|
|
68
|
+
return buildUpstreamError('Bytedance createVideo response is missing task id.');
|
|
69
|
+
}
|
|
70
|
+
const mappedStatus = mapInitialStatus(responseResult.value.status);
|
|
71
|
+
if (!mappedStatus.ok) {
|
|
72
|
+
return mappedStatus;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
value: {
|
|
77
|
+
jobId: responseResult.value.id,
|
|
78
|
+
status: mappedStatus.value,
|
|
79
|
+
createdAt: toIsoTimestamp(responseResult.value.created_at),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public async getVideoJob(jobId: string): Promise<TProviderMediaResult<IVideoJobSnapshot>> {
|
|
85
|
+
if (jobId.trim().length === 0) {
|
|
86
|
+
return buildInvalidRequestError('Video job lookup requires non-empty jobId.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const responseResult = await requestJson<IBytedanceVideoTaskResponse>(this.options, {
|
|
90
|
+
path: buildPath(
|
|
91
|
+
this.options.getVideoTaskPathTemplate ?? DEFAULT_GET_VIDEO_TASK_PATH_TEMPLATE,
|
|
92
|
+
jobId,
|
|
93
|
+
),
|
|
94
|
+
method: 'GET',
|
|
95
|
+
});
|
|
96
|
+
if (!responseResult.ok) {
|
|
97
|
+
return responseResult;
|
|
98
|
+
}
|
|
99
|
+
return mapVideoJobSnapshot(responseResult.value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async cancelVideoJob(jobId: string): Promise<TProviderMediaResult<IVideoJobSnapshot>> {
|
|
103
|
+
if (jobId.trim().length === 0) {
|
|
104
|
+
return buildInvalidRequestError('Video job cancellation requires non-empty jobId.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const cancelMethod = this.options.cancelVideoTaskMethod ?? 'DELETE';
|
|
108
|
+
const responseResult = await requestJson<IBytedanceVideoTaskResponse>(this.options, {
|
|
109
|
+
path: buildPath(
|
|
110
|
+
this.options.cancelVideoTaskPathTemplate ?? DEFAULT_CANCEL_VIDEO_TASK_PATH_TEMPLATE,
|
|
111
|
+
jobId,
|
|
112
|
+
),
|
|
113
|
+
method: cancelMethod,
|
|
114
|
+
});
|
|
115
|
+
if (!responseResult.ok) {
|
|
116
|
+
return responseResult;
|
|
117
|
+
}
|
|
118
|
+
return mapVideoJobSnapshot(responseResult.value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private buildContentPayload(
|
|
122
|
+
prompt: string,
|
|
123
|
+
inputImages: IVideoGenerationRequest['inputImages'],
|
|
124
|
+
): TProviderMediaResult<TBytedanceTaskContent[]> {
|
|
125
|
+
const normalizedPrompt = prompt.trim();
|
|
126
|
+
if (normalizedPrompt.length === 0) {
|
|
127
|
+
return buildInvalidRequestError('Video generation requires non-empty prompt.');
|
|
128
|
+
}
|
|
129
|
+
const content: TBytedanceTaskContent[] = [{ type: 'text', text: normalizedPrompt }];
|
|
130
|
+
if (Array.isArray(inputImages)) {
|
|
131
|
+
for (const image of inputImages) {
|
|
132
|
+
const imageUrlResult = toContentImageUrl(image);
|
|
133
|
+
if (!imageUrlResult.ok) {
|
|
134
|
+
return imageUrlResult;
|
|
135
|
+
}
|
|
136
|
+
content.push({ type: 'image_url', image_url: { url: imageUrlResult.value } });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, value: content };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildPath(template: string, taskId: string): string {
|
|
144
|
+
return template.replace('{taskId}', encodeURIComponent(taskId));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildInvalidRequestError(message: string): TProviderMediaResult<never> {
|
|
148
|
+
return { ok: false, error: { code: 'PROVIDER_INVALID_REQUEST', message } };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildUpstreamError(message: string): TProviderMediaResult<never> {
|
|
152
|
+
return { ok: false, error: { code: 'PROVIDER_UPSTREAM_ERROR', message } };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toContentImageUrl(
|
|
156
|
+
image: IInlineImageInputSource | IUriImageInputSource,
|
|
157
|
+
): TProviderMediaResult<string> {
|
|
158
|
+
if (image.kind === 'uri') {
|
|
159
|
+
if (image.uri.trim().length === 0) {
|
|
160
|
+
return buildInvalidRequestError('Image uri must be non-empty.');
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, value: image.uri };
|
|
163
|
+
}
|
|
164
|
+
if (image.data.trim().length === 0) {
|
|
165
|
+
return buildInvalidRequestError('Inline image data must be non-empty.');
|
|
166
|
+
}
|
|
167
|
+
if (image.mimeType.trim().length === 0) {
|
|
168
|
+
return buildInvalidRequestError('Inline image mimeType must be non-empty.');
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, value: `data:${image.mimeType};base64,${image.data}` };
|
|
171
|
+
}
|