@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,288 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { requestJson } from './http-client';
|
|
3
|
+
import type { IBytedanceProviderOptions } from './types';
|
|
4
|
+
|
|
5
|
+
const BASE_OPTIONS: IBytedanceProviderOptions = {
|
|
6
|
+
apiKey: 'test-api-key',
|
|
7
|
+
baseUrl: 'https://api.byteplus.test',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe('requestJson', () => {
|
|
11
|
+
const fetchMock = vi.fn();
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
fetchMock.mockReset();
|
|
15
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.unstubAllGlobals();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('sends GET request with Authorization header', async () => {
|
|
23
|
+
fetchMock.mockResolvedValue({
|
|
24
|
+
ok: true,
|
|
25
|
+
text: async () => JSON.stringify({ id: '123' }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await requestJson(BASE_OPTIONS, { path: '/tasks/123', method: 'GET' });
|
|
29
|
+
|
|
30
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
31
|
+
'https://api.byteplus.test/tasks/123',
|
|
32
|
+
expect.objectContaining({
|
|
33
|
+
method: 'GET',
|
|
34
|
+
headers: expect.objectContaining({
|
|
35
|
+
Authorization: 'Bearer test-api-key',
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('sends POST request with body', async () => {
|
|
43
|
+
fetchMock.mockResolvedValue({
|
|
44
|
+
ok: true,
|
|
45
|
+
text: async () => JSON.stringify({ id: 'new-task' }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const body = JSON.stringify({ model: 'seedance' });
|
|
49
|
+
await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'POST', body });
|
|
50
|
+
|
|
51
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
52
|
+
'https://api.byteplus.test/tasks',
|
|
53
|
+
expect.objectContaining({
|
|
54
|
+
method: 'POST',
|
|
55
|
+
body,
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns parsed JSON on success', async () => {
|
|
61
|
+
fetchMock.mockResolvedValue({
|
|
62
|
+
ok: true,
|
|
63
|
+
text: async () => JSON.stringify({ id: 'task-1', status: 'queued' }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const result = await requestJson<{ id: string; status: string }>(BASE_OPTIONS, {
|
|
67
|
+
path: '/tasks',
|
|
68
|
+
method: 'GET',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result.ok).toBe(true);
|
|
72
|
+
if (result.ok) {
|
|
73
|
+
expect(result.value).toEqual({ id: 'task-1', status: 'queued' });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('includes custom default headers', async () => {
|
|
78
|
+
fetchMock.mockResolvedValue({
|
|
79
|
+
ok: true,
|
|
80
|
+
text: async () => JSON.stringify({}),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const options: IBytedanceProviderOptions = {
|
|
84
|
+
...BASE_OPTIONS,
|
|
85
|
+
defaultHeaders: { 'X-Custom': 'value' },
|
|
86
|
+
};
|
|
87
|
+
await requestJson(options, { path: '/tasks', method: 'GET' });
|
|
88
|
+
|
|
89
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
90
|
+
expect.any(String),
|
|
91
|
+
expect.objectContaining({
|
|
92
|
+
headers: expect.objectContaining({
|
|
93
|
+
'X-Custom': 'value',
|
|
94
|
+
}),
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('normalizes base URL trailing slash', async () => {
|
|
100
|
+
fetchMock.mockResolvedValue({
|
|
101
|
+
ok: true,
|
|
102
|
+
text: async () => JSON.stringify({}),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await requestJson(
|
|
106
|
+
{ ...BASE_OPTIONS, baseUrl: 'https://api.test/' },
|
|
107
|
+
{ path: '/path', method: 'GET' },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(fetchMock).toHaveBeenCalledWith('https://api.test/path', expect.any(Object));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('normalizes path without leading slash', async () => {
|
|
114
|
+
fetchMock.mockResolvedValue({
|
|
115
|
+
ok: true,
|
|
116
|
+
text: async () => JSON.stringify({}),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await requestJson(BASE_OPTIONS, { path: 'tasks', method: 'GET' });
|
|
120
|
+
|
|
121
|
+
expect(fetchMock).toHaveBeenCalledWith('https://api.byteplus.test/tasks', expect.any(Object));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('HTTP error mapping', () => {
|
|
125
|
+
it('maps 401 to PROVIDER_AUTH_ERROR', async () => {
|
|
126
|
+
fetchMock.mockResolvedValue({
|
|
127
|
+
ok: false,
|
|
128
|
+
status: 401,
|
|
129
|
+
text: async () => JSON.stringify({ message: 'Unauthorized' }),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'GET' });
|
|
133
|
+
expect(result.ok).toBe(false);
|
|
134
|
+
if (!result.ok) {
|
|
135
|
+
expect(result.error.code).toBe('PROVIDER_AUTH_ERROR');
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('maps 403 to PROVIDER_AUTH_ERROR', async () => {
|
|
140
|
+
fetchMock.mockResolvedValue({
|
|
141
|
+
ok: false,
|
|
142
|
+
status: 403,
|
|
143
|
+
text: async () => JSON.stringify({ message: 'Forbidden' }),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'GET' });
|
|
147
|
+
expect(result.ok).toBe(false);
|
|
148
|
+
if (!result.ok) {
|
|
149
|
+
expect(result.error.code).toBe('PROVIDER_AUTH_ERROR');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('maps 404 to PROVIDER_JOB_NOT_FOUND', async () => {
|
|
154
|
+
fetchMock.mockResolvedValue({
|
|
155
|
+
ok: false,
|
|
156
|
+
status: 404,
|
|
157
|
+
text: async () => JSON.stringify({ message: 'Not found' }),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks/missing', method: 'GET' });
|
|
161
|
+
expect(result.ok).toBe(false);
|
|
162
|
+
if (!result.ok) {
|
|
163
|
+
expect(result.error.code).toBe('PROVIDER_JOB_NOT_FOUND');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('maps 409 to PROVIDER_JOB_NOT_CANCELLABLE', async () => {
|
|
168
|
+
fetchMock.mockResolvedValue({
|
|
169
|
+
ok: false,
|
|
170
|
+
status: 409,
|
|
171
|
+
text: async () => JSON.stringify({ message: 'Conflict' }),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks/1', method: 'DELETE' });
|
|
175
|
+
expect(result.ok).toBe(false);
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
expect(result.error.code).toBe('PROVIDER_JOB_NOT_CANCELLABLE');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('maps 429 to PROVIDER_RATE_LIMITED', async () => {
|
|
182
|
+
fetchMock.mockResolvedValue({
|
|
183
|
+
ok: false,
|
|
184
|
+
status: 429,
|
|
185
|
+
text: async () => JSON.stringify({ message: 'Rate limited' }),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'POST' });
|
|
189
|
+
expect(result.ok).toBe(false);
|
|
190
|
+
if (!result.ok) {
|
|
191
|
+
expect(result.error.code).toBe('PROVIDER_RATE_LIMITED');
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('maps 4xx (not 401/403/404/409/429) to PROVIDER_INVALID_REQUEST', async () => {
|
|
196
|
+
fetchMock.mockResolvedValue({
|
|
197
|
+
ok: false,
|
|
198
|
+
status: 422,
|
|
199
|
+
text: async () => JSON.stringify({ message: 'Unprocessable' }),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'POST' });
|
|
203
|
+
expect(result.ok).toBe(false);
|
|
204
|
+
if (!result.ok) {
|
|
205
|
+
expect(result.error.code).toBe('PROVIDER_INVALID_REQUEST');
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('maps 5xx to PROVIDER_UPSTREAM_ERROR', async () => {
|
|
210
|
+
fetchMock.mockResolvedValue({
|
|
211
|
+
ok: false,
|
|
212
|
+
status: 500,
|
|
213
|
+
text: async () => JSON.stringify({ message: 'Internal error' }),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'GET' });
|
|
217
|
+
expect(result.ok).toBe(false);
|
|
218
|
+
if (!result.ok) {
|
|
219
|
+
expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('handles non-JSON error response body', async () => {
|
|
224
|
+
fetchMock.mockResolvedValue({
|
|
225
|
+
ok: false,
|
|
226
|
+
status: 500,
|
|
227
|
+
text: async () => 'plain text error',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'GET' });
|
|
231
|
+
expect(result.ok).toBe(false);
|
|
232
|
+
if (!result.ok) {
|
|
233
|
+
expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
|
|
234
|
+
expect(result.error.message).toBe('plain text error');
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('returns PROVIDER_UPSTREAM_ERROR for invalid JSON success response', async () => {
|
|
240
|
+
fetchMock.mockResolvedValue({
|
|
241
|
+
ok: true,
|
|
242
|
+
text: async () => 'not-json',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'GET' });
|
|
246
|
+
expect(result.ok).toBe(false);
|
|
247
|
+
if (!result.ok) {
|
|
248
|
+
expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
|
|
249
|
+
expect(result.error.message).toContain('not valid JSON');
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('returns PROVIDER_TIMEOUT on AbortError', async () => {
|
|
254
|
+
const abortError = new Error('The operation was aborted');
|
|
255
|
+
abortError.name = 'AbortError';
|
|
256
|
+
fetchMock.mockRejectedValue(abortError);
|
|
257
|
+
|
|
258
|
+
const result = await requestJson(
|
|
259
|
+
{ ...BASE_OPTIONS, timeoutMs: 1 },
|
|
260
|
+
{ path: '/tasks', method: 'GET' },
|
|
261
|
+
);
|
|
262
|
+
expect(result.ok).toBe(false);
|
|
263
|
+
if (!result.ok) {
|
|
264
|
+
expect(result.error.code).toBe('PROVIDER_TIMEOUT');
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('returns PROVIDER_UPSTREAM_ERROR on network error', async () => {
|
|
269
|
+
fetchMock.mockRejectedValue(new Error('Network failure'));
|
|
270
|
+
|
|
271
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'GET' });
|
|
272
|
+
expect(result.ok).toBe(false);
|
|
273
|
+
if (!result.ok) {
|
|
274
|
+
expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
|
|
275
|
+
expect(result.error.message).toBe('Network failure');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('returns PROVIDER_UPSTREAM_ERROR for non-Error thrown value', async () => {
|
|
280
|
+
fetchMock.mockRejectedValue('string error');
|
|
281
|
+
|
|
282
|
+
const result = await requestJson(BASE_OPTIONS, { path: '/tasks', method: 'GET' });
|
|
283
|
+
expect(result.ok).toBe(false);
|
|
284
|
+
if (!result.ok) {
|
|
285
|
+
expect(result.error.code).toBe('PROVIDER_UPSTREAM_ERROR');
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { TProviderMediaResult } from '@robota-sdk/agent-core';
|
|
2
|
+
import type { IBytedanceApiErrorResponse, IBytedanceProviderOptions } from './types';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
5
|
+
|
|
6
|
+
/** HTTP status codes used by the Bytedance API error mapper. */
|
|
7
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
8
|
+
const HTTP_FORBIDDEN = 403;
|
|
9
|
+
const HTTP_NOT_FOUND = 404;
|
|
10
|
+
const HTTP_CONFLICT = 409;
|
|
11
|
+
const HTTP_TOO_MANY_REQUESTS = 429;
|
|
12
|
+
const HTTP_BAD_REQUEST = 400;
|
|
13
|
+
const HTTP_INTERNAL_ERROR = 500;
|
|
14
|
+
|
|
15
|
+
/** Sends an HTTP request to the Bytedance API and returns the parsed JSON response. */
|
|
16
|
+
export async function requestJson<TResponse>(
|
|
17
|
+
options: IBytedanceProviderOptions,
|
|
18
|
+
request: {
|
|
19
|
+
path: string;
|
|
20
|
+
method: 'GET' | 'POST' | 'DELETE';
|
|
21
|
+
body?: string;
|
|
22
|
+
},
|
|
23
|
+
): Promise<TProviderMediaResult<TResponse>> {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timeoutHandle = setTimeout(
|
|
26
|
+
() => controller.abort(),
|
|
27
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
28
|
+
);
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(buildUrl(options.baseUrl, request.path), {
|
|
31
|
+
method: request.method,
|
|
32
|
+
headers: {
|
|
33
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
...(options.defaultHeaders ?? {}),
|
|
36
|
+
},
|
|
37
|
+
body: request.body,
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
});
|
|
40
|
+
const responseText = await response.text();
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
return mapHttpError(response.status, responseText);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsedResult = parseJsonRecord(responseText);
|
|
46
|
+
if (!parsedResult.ok) {
|
|
47
|
+
return parsedResult;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
value: parsedResult.value as TResponse,
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: { code: 'PROVIDER_TIMEOUT', message: 'Bytedance media request timed out.' },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: {
|
|
63
|
+
code: 'PROVIDER_UPSTREAM_ERROR',
|
|
64
|
+
message: error instanceof Error ? error.message : 'Bytedance media request failed.',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timeoutHandle);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildUrl(baseUrl: string, path: string): string {
|
|
73
|
+
const sanitizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
74
|
+
const sanitizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
75
|
+
return `${sanitizedBaseUrl}${sanitizedPath}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseJsonRecord(
|
|
79
|
+
responseText: string,
|
|
80
|
+
): TProviderMediaResult<Record<string, string | number | boolean | undefined>> {
|
|
81
|
+
try {
|
|
82
|
+
const parsedValue = JSON.parse(responseText) as Record<
|
|
83
|
+
string,
|
|
84
|
+
string | number | boolean | undefined
|
|
85
|
+
>;
|
|
86
|
+
return { ok: true, value: parsedValue };
|
|
87
|
+
} catch {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: { code: 'PROVIDER_UPSTREAM_ERROR', message: 'Bytedance response is not valid JSON.' },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseErrorResponse(responseText: string): IBytedanceApiErrorResponse {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(responseText) as IBytedanceApiErrorResponse;
|
|
98
|
+
} catch {
|
|
99
|
+
return { message: responseText };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function mapHttpError(statusCode: number, responseText: string): TProviderMediaResult<never> {
|
|
104
|
+
const parsedError = parseErrorResponse(responseText);
|
|
105
|
+
if (statusCode === HTTP_UNAUTHORIZED || statusCode === HTTP_FORBIDDEN) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: {
|
|
109
|
+
code: 'PROVIDER_AUTH_ERROR',
|
|
110
|
+
message: parsedError.message ?? 'Bytedance authentication failed.',
|
|
111
|
+
details: parsedError.details,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (statusCode === HTTP_NOT_FOUND) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: {
|
|
119
|
+
code: 'PROVIDER_JOB_NOT_FOUND',
|
|
120
|
+
message: parsedError.message ?? 'Bytedance video job was not found.',
|
|
121
|
+
details: parsedError.details,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (statusCode === HTTP_CONFLICT) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: {
|
|
129
|
+
code: 'PROVIDER_JOB_NOT_CANCELLABLE',
|
|
130
|
+
message: parsedError.message ?? 'Bytedance video job cannot be cancelled in current state.',
|
|
131
|
+
details: parsedError.details,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (statusCode === HTTP_TOO_MANY_REQUESTS) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
error: {
|
|
139
|
+
code: 'PROVIDER_RATE_LIMITED',
|
|
140
|
+
message: parsedError.message ?? 'Bytedance rate limit exceeded.',
|
|
141
|
+
details: parsedError.details,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (statusCode >= HTTP_BAD_REQUEST && statusCode < HTTP_INTERNAL_ERROR) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: {
|
|
149
|
+
code: 'PROVIDER_INVALID_REQUEST',
|
|
150
|
+
message: parsedError.message ?? 'Bytedance rejected request payload.',
|
|
151
|
+
details: parsedError.details,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
error: {
|
|
158
|
+
code: 'PROVIDER_UPSTREAM_ERROR',
|
|
159
|
+
message: parsedError.message ?? 'Bytedance upstream request failed.',
|
|
160
|
+
details: parsedError.details,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|