@acedatacloud/sdk 2026.716.0 → 2026.718.0
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/README.md +2 -2
- package/dist/index.d.mts +44 -23
- package/dist/index.d.ts +44 -23
- package/dist/index.js +295 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +295 -57
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +7 -1
- package/src/resources/kling.ts +218 -39
- package/src/runtime/transport.ts +148 -51
package/src/resources/kling.ts
CHANGED
|
@@ -2,41 +2,200 @@
|
|
|
2
2
|
|
|
3
3
|
import { Transport } from '../runtime/transport';
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
export const KLING_MODELS = [
|
|
6
|
+
'kling-v1',
|
|
7
|
+
'kling-v1-6',
|
|
8
|
+
'kling-v2-master',
|
|
9
|
+
'kling-v2-1-master',
|
|
10
|
+
'kling-v2-5-turbo',
|
|
11
|
+
'kling-v2-6',
|
|
12
|
+
'kling-v3',
|
|
13
|
+
'kling-v3-omni',
|
|
14
|
+
'kling-o1',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export type KlingModel = (typeof KLING_MODELS)[number];
|
|
18
|
+
|
|
19
|
+
export interface KlingCameraControl {
|
|
20
|
+
type: 'simple' | 'down_back' | 'forward_up' | 'left_turn_forward' | 'right_turn_forward';
|
|
21
|
+
config?: {
|
|
22
|
+
horizontal?: number;
|
|
23
|
+
vertical?: number;
|
|
24
|
+
pan?: number;
|
|
25
|
+
tilt?: number;
|
|
26
|
+
roll?: number;
|
|
27
|
+
zoom?: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface KlingReferenceImage {
|
|
32
|
+
imageUrl: string;
|
|
33
|
+
type?: 'first_frame' | 'end_frame';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface KlingReferenceVideo {
|
|
37
|
+
videoUrl: string;
|
|
38
|
+
referType?: 'base' | 'feature';
|
|
39
|
+
keepOriginalSound?: 'yes' | 'no';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface KlingGenerateOptions {
|
|
43
|
+
action: 'text2video' | 'image2video' | 'extend';
|
|
44
|
+
mode?: 'std' | 'pro' | '4k';
|
|
45
|
+
model: KlingModel;
|
|
46
|
+
prompt?: string;
|
|
47
|
+
duration?: number;
|
|
48
|
+
generateAudio?: boolean;
|
|
49
|
+
videoId?: string;
|
|
50
|
+
cfgScale?: number;
|
|
51
|
+
aspectRatio?: '16:9' | '9:16' | '1:1';
|
|
52
|
+
callbackUrl?: string;
|
|
53
|
+
async?: boolean;
|
|
54
|
+
timeout?: number;
|
|
55
|
+
endImageUrl?: string;
|
|
56
|
+
cameraControl?: KlingCameraControl;
|
|
57
|
+
imageList?: KlingReferenceImage[];
|
|
58
|
+
videoList?: KlingReferenceVideo[];
|
|
59
|
+
negativePrompt?: string;
|
|
60
|
+
startImageUrl?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isHttpUrl(value: string): boolean {
|
|
64
|
+
try {
|
|
65
|
+
const url = new URL(value);
|
|
66
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateGenerateOptions(opts: KlingGenerateOptions): void {
|
|
73
|
+
if (!KLING_MODELS.includes(opts.model)) {
|
|
74
|
+
throw new Error(`model must be one of: ${KLING_MODELS.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
const isV3 = opts.model === 'kling-v3' || opts.model === 'kling-v3-omni';
|
|
77
|
+
const hasReferences = Boolean(opts.imageList?.length || opts.videoList?.length);
|
|
78
|
+
|
|
79
|
+
if (opts.imageList !== undefined && opts.imageList.length === 0) {
|
|
80
|
+
throw new Error('imageList must be non-empty or omitted');
|
|
81
|
+
}
|
|
82
|
+
if (opts.videoList !== undefined && opts.videoList.length === 0) {
|
|
83
|
+
throw new Error('videoList must be non-empty or omitted');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if ((opts.action === 'text2video' || opts.action === 'image2video') && !opts.prompt) {
|
|
87
|
+
throw new Error('prompt is required for text2video and image2video');
|
|
88
|
+
}
|
|
89
|
+
if (opts.action === 'image2video' && !opts.startImageUrl) {
|
|
90
|
+
throw new Error('startImageUrl is required for image2video');
|
|
91
|
+
}
|
|
92
|
+
if (opts.action === 'extend' && !opts.videoId) {
|
|
93
|
+
throw new Error('videoId is required for extend');
|
|
94
|
+
}
|
|
95
|
+
if (opts.endImageUrl && !opts.startImageUrl) {
|
|
96
|
+
throw new Error('startImageUrl is required with endImageUrl');
|
|
97
|
+
}
|
|
98
|
+
if (opts.startImageUrl && !isHttpUrl(opts.startImageUrl)) {
|
|
99
|
+
throw new Error('startImageUrl must be an HTTP URL');
|
|
100
|
+
}
|
|
101
|
+
if (opts.endImageUrl && !isHttpUrl(opts.endImageUrl)) {
|
|
102
|
+
throw new Error('endImageUrl must be an HTTP URL');
|
|
103
|
+
}
|
|
104
|
+
if (opts.callbackUrl && !isHttpUrl(opts.callbackUrl)) {
|
|
105
|
+
throw new Error('callbackUrl must be an HTTP URL');
|
|
106
|
+
}
|
|
107
|
+
if (opts.cfgScale !== undefined && (opts.cfgScale < 0 || opts.cfgScale > 1)) {
|
|
108
|
+
throw new Error('cfgScale must be between 0 and 1');
|
|
109
|
+
}
|
|
110
|
+
if (opts.duration !== undefined && !Number.isInteger(opts.duration)) {
|
|
111
|
+
throw new Error('duration must be an integer');
|
|
112
|
+
}
|
|
113
|
+
if (isV3 && opts.duration !== undefined && (opts.duration < 3 || opts.duration > 15)) {
|
|
114
|
+
throw new Error('Kling V3 duration must be between 3 and 15 seconds');
|
|
115
|
+
}
|
|
116
|
+
if (!isV3 && opts.model !== 'kling-o1' && opts.duration !== undefined && ![5, 10].includes(opts.duration)) {
|
|
117
|
+
throw new Error('This Kling model supports only 5- or 10-second generation');
|
|
118
|
+
}
|
|
119
|
+
if (opts.model === 'kling-o1' && opts.duration !== undefined && opts.duration !== 5) {
|
|
120
|
+
throw new Error('kling-o1 supports only 5-second generation');
|
|
121
|
+
}
|
|
122
|
+
if (opts.model === 'kling-o1' && opts.mode !== undefined && !['std', 'pro'].includes(opts.mode)) {
|
|
123
|
+
throw new Error('kling-o1 supports only std and pro modes');
|
|
124
|
+
}
|
|
125
|
+
if (opts.mode === '4k' && !isV3) {
|
|
126
|
+
throw new Error('4k mode requires kling-v3 or kling-v3-omni');
|
|
127
|
+
}
|
|
128
|
+
if (opts.action === 'extend' && !['kling-v1', 'kling-v1-6', 'kling-v2-5-turbo'].includes(opts.model)) {
|
|
129
|
+
throw new Error('extend requires kling-v1, kling-v1-6, or kling-v2-5-turbo');
|
|
130
|
+
}
|
|
131
|
+
if (opts.action === 'extend' && hasReferences) {
|
|
132
|
+
throw new Error('imageList and videoList are not supported with extend');
|
|
133
|
+
}
|
|
134
|
+
if (hasReferences && opts.model !== 'kling-o1' && opts.model !== 'kling-v3-omni') {
|
|
135
|
+
throw new Error('Omni references require kling-o1 or kling-v3-omni');
|
|
136
|
+
}
|
|
137
|
+
if (hasReferences && opts.mode === '4k') {
|
|
138
|
+
throw new Error('4k cannot be combined with Omni references');
|
|
139
|
+
}
|
|
140
|
+
if ((opts.model === 'kling-o1' || hasReferences) && (opts.negativePrompt !== undefined || opts.cameraControl !== undefined || opts.cfgScale !== undefined)) {
|
|
141
|
+
throw new Error('Kling O1 and Omni references do not support negativePrompt, cameraControl, or cfgScale');
|
|
142
|
+
}
|
|
143
|
+
if (opts.model === 'kling-o1' && opts.generateAudio) {
|
|
144
|
+
throw new Error('kling-o1 does not support generateAudio');
|
|
145
|
+
}
|
|
146
|
+
if (opts.generateAudio && !isV3 && opts.model !== 'kling-v2-6') {
|
|
147
|
+
throw new Error('generateAudio requires a V3 model or kling-v2-6 pro mode');
|
|
148
|
+
}
|
|
149
|
+
if (opts.generateAudio && opts.model === 'kling-v2-6' && opts.mode !== 'pro') {
|
|
150
|
+
throw new Error('kling-v2-6 supports generateAudio only in pro mode');
|
|
151
|
+
}
|
|
152
|
+
if (opts.generateAudio && opts.videoList?.length) {
|
|
153
|
+
throw new Error('generateAudio cannot be used with videoList');
|
|
154
|
+
}
|
|
155
|
+
if (opts.videoList && (opts.videoList.length === 0 || opts.videoList.length > 1)) {
|
|
156
|
+
throw new Error('videoList must contain exactly one reference video');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const image of opts.imageList ?? []) {
|
|
160
|
+
if (!isHttpUrl(image.imageUrl)) throw new Error('Every reference image requires an HTTP imageUrl');
|
|
161
|
+
if (image.type !== undefined && !['first_frame', 'end_frame'].includes(image.type)) {
|
|
162
|
+
throw new Error('Reference image type must be first_frame or end_frame');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const video of opts.videoList ?? []) {
|
|
166
|
+
if (!isHttpUrl(video.videoUrl)) throw new Error('Every reference video requires an HTTP videoUrl');
|
|
167
|
+
if (video.referType !== undefined && !['base', 'feature'].includes(video.referType)) {
|
|
168
|
+
throw new Error('Reference video referType must be base or feature');
|
|
169
|
+
}
|
|
170
|
+
if (video.keepOriginalSound !== undefined && !['yes', 'no'].includes(video.keepOriginalSound)) {
|
|
171
|
+
throw new Error('Reference video keepOriginalSound must be yes or no');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const firstFrames = Number(Boolean(opts.startImageUrl)) + (opts.imageList ?? []).filter((item) => item.type === 'first_frame').length;
|
|
176
|
+
const endFrames = Number(Boolean(opts.endImageUrl)) + (opts.imageList ?? []).filter((item) => item.type === 'end_frame').length;
|
|
177
|
+
if (firstFrames > 1 || endFrames > 1) {
|
|
178
|
+
throw new Error('Only one first frame and one end frame are allowed');
|
|
179
|
+
}
|
|
180
|
+
if (endFrames > 0 && firstFrames === 0) {
|
|
181
|
+
throw new Error('A first frame is required with an end frame');
|
|
182
|
+
}
|
|
183
|
+
if ((opts.videoList?.[0]?.referType ?? 'base') === 'base' && opts.videoList?.length && (firstFrames > 0 || endFrames > 0)) {
|
|
184
|
+
throw new Error('A base reference video cannot be combined with first or end frames');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const imageCount = (opts.imageList?.length ?? 0) + Number(Boolean(opts.startImageUrl)) + Number(Boolean(opts.endImageUrl));
|
|
188
|
+
const imageLimit = opts.videoList?.length ? 4 : 7;
|
|
189
|
+
if (imageCount > imageLimit) {
|
|
190
|
+
throw new Error(`Reference images cannot exceed ${imageLimit} for this request`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
16
193
|
|
|
17
194
|
export class Kling {
|
|
18
195
|
constructor(private transport: Transport) {}
|
|
19
196
|
|
|
20
|
-
async generate(opts: {
|
|
21
|
-
|
|
22
|
-
mode?: 'std' | 'pro' | '4k';
|
|
23
|
-
model?: KlingModel;
|
|
24
|
-
prompt?: string;
|
|
25
|
-
duration?: 5 | 10;
|
|
26
|
-
generateAudio?: boolean;
|
|
27
|
-
videoId?: string;
|
|
28
|
-
cfgScale?: number;
|
|
29
|
-
aspectRatio?: '16:9' | '9:16' | '1:1';
|
|
30
|
-
callbackUrl?: string;
|
|
31
|
-
async?: boolean;
|
|
32
|
-
endImageUrl?: string;
|
|
33
|
-
cameraControl?: string;
|
|
34
|
-
elementList?: unknown[];
|
|
35
|
-
videoList?: unknown[];
|
|
36
|
-
negativePrompt?: string;
|
|
37
|
-
startImageUrl?: string;
|
|
38
|
-
[key: string]: unknown;
|
|
39
|
-
}): Promise<Record<string, unknown>> {
|
|
197
|
+
async generate(opts: KlingGenerateOptions): Promise<Record<string, unknown>> {
|
|
198
|
+
validateGenerateOptions(opts);
|
|
40
199
|
const {
|
|
41
200
|
action,
|
|
42
201
|
mode,
|
|
@@ -48,15 +207,16 @@ export class Kling {
|
|
|
48
207
|
cfgScale,
|
|
49
208
|
aspectRatio,
|
|
50
209
|
callbackUrl,
|
|
210
|
+
async: asyncMode,
|
|
211
|
+
timeout,
|
|
51
212
|
endImageUrl,
|
|
52
213
|
cameraControl,
|
|
53
|
-
|
|
214
|
+
imageList,
|
|
54
215
|
videoList,
|
|
55
216
|
negativePrompt,
|
|
56
217
|
startImageUrl,
|
|
57
|
-
...rest
|
|
58
218
|
} = opts;
|
|
59
|
-
const body: Record<string, unknown> = { action
|
|
219
|
+
const body: Record<string, unknown> = { action };
|
|
60
220
|
if (mode !== undefined) body.mode = mode;
|
|
61
221
|
if (model !== undefined) body.model = model;
|
|
62
222
|
if (prompt !== undefined) body.prompt = prompt;
|
|
@@ -66,10 +226,20 @@ export class Kling {
|
|
|
66
226
|
if (cfgScale !== undefined) body.cfg_scale = cfgScale;
|
|
67
227
|
if (aspectRatio !== undefined) body.aspect_ratio = aspectRatio;
|
|
68
228
|
if (callbackUrl !== undefined) body.callback_url = callbackUrl;
|
|
229
|
+
if (asyncMode !== undefined) body.async = asyncMode;
|
|
230
|
+
if (timeout !== undefined) body.timeout = timeout;
|
|
69
231
|
if (endImageUrl !== undefined) body.end_image_url = endImageUrl;
|
|
70
232
|
if (cameraControl !== undefined) body.camera_control = cameraControl;
|
|
71
|
-
if (
|
|
72
|
-
|
|
233
|
+
if (imageList !== undefined) {
|
|
234
|
+
body.image_list = imageList.map(({ imageUrl, type }) => ({ image_url: imageUrl, ...(type ? { type } : {}) }));
|
|
235
|
+
}
|
|
236
|
+
if (videoList !== undefined) {
|
|
237
|
+
body.video_list = videoList.map(({ videoUrl, referType, keepOriginalSound }) => ({
|
|
238
|
+
video_url: videoUrl,
|
|
239
|
+
...(referType ? { refer_type: referType } : {}),
|
|
240
|
+
...(keepOriginalSound ? { keep_original_sound: keepOriginalSound } : {}),
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
73
243
|
if (negativePrompt !== undefined) body.negative_prompt = negativePrompt;
|
|
74
244
|
if (startImageUrl !== undefined) body.start_image_url = startImageUrl;
|
|
75
245
|
return this.transport.request('POST', '/kling/videos', { json: body });
|
|
@@ -84,19 +254,28 @@ export class Kling {
|
|
|
84
254
|
prompt?: string;
|
|
85
255
|
callbackUrl?: string;
|
|
86
256
|
async?: boolean;
|
|
87
|
-
[key: string]: unknown;
|
|
88
257
|
}): Promise<Record<string, unknown>> {
|
|
89
|
-
const { mode, imageUrl, videoUrl, characterOrientation, keepOriginalSound, prompt, callbackUrl
|
|
258
|
+
const { mode, imageUrl, videoUrl, characterOrientation, keepOriginalSound, prompt, callbackUrl } = opts;
|
|
259
|
+
if (!['std', 'pro'].includes(mode)) throw new Error('mode must be std or pro');
|
|
260
|
+
if (!isHttpUrl(imageUrl)) throw new Error('imageUrl must be an HTTP URL');
|
|
261
|
+
if (!isHttpUrl(videoUrl)) throw new Error('videoUrl must be an HTTP URL');
|
|
262
|
+
if (!['image', 'video'].includes(characterOrientation)) {
|
|
263
|
+
throw new Error('characterOrientation must be image or video');
|
|
264
|
+
}
|
|
265
|
+
if (keepOriginalSound !== undefined && !['yes', 'no'].includes(keepOriginalSound)) {
|
|
266
|
+
throw new Error('keepOriginalSound must be yes or no');
|
|
267
|
+
}
|
|
268
|
+
if (callbackUrl && !isHttpUrl(callbackUrl)) throw new Error('callbackUrl must be an HTTP URL');
|
|
90
269
|
const body: Record<string, unknown> = {
|
|
91
270
|
mode,
|
|
92
271
|
image_url: imageUrl,
|
|
93
272
|
video_url: videoUrl,
|
|
94
273
|
character_orientation: characterOrientation,
|
|
95
|
-
...rest,
|
|
96
274
|
};
|
|
97
275
|
if (keepOriginalSound !== undefined) body.keep_original_sound = keepOriginalSound;
|
|
98
276
|
if (prompt !== undefined) body.prompt = prompt;
|
|
99
277
|
if (callbackUrl !== undefined) body.callback_url = callbackUrl;
|
|
278
|
+
if (opts.async !== undefined) body.async = opts.async;
|
|
100
279
|
return this.transport.request('POST', '/kling/motion', { json: body });
|
|
101
280
|
}
|
|
102
281
|
}
|
package/src/runtime/transport.ts
CHANGED
|
@@ -68,6 +68,18 @@ function sleep(ms: number): Promise<void> {
|
|
|
68
68
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function isAbortError(error: unknown): boolean {
|
|
72
|
+
return typeof error === 'object' && error !== null && 'name' in error && error.name === 'AbortError';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function timeoutError(error: unknown): TimeoutError {
|
|
76
|
+
return new TimeoutError({
|
|
77
|
+
message: `Request timed out${error instanceof Error && error.message ? `: ${error.message}` : ''}`,
|
|
78
|
+
statusCode: 0,
|
|
79
|
+
code: 'timeout',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
export interface TransportOptions {
|
|
72
84
|
apiToken?: string;
|
|
73
85
|
baseURL?: string;
|
|
@@ -104,7 +116,7 @@ export class Transport {
|
|
|
104
116
|
code: 'no_token',
|
|
105
117
|
});
|
|
106
118
|
}
|
|
107
|
-
this.baseURL = (opts.baseURL ?? 'https://
|
|
119
|
+
this.baseURL = (opts.baseURL ?? 'https://x402.acedata.cloud').replace(/\/+$/, '');
|
|
108
120
|
this.platformBaseURL = (opts.platformBaseURL ?? 'https://platform.acedata.cloud').replace(/\/+$/, '');
|
|
109
121
|
this.timeout = opts.timeout ?? 300_000;
|
|
110
122
|
this.maxRetries = opts.maxRetries ?? 2;
|
|
@@ -144,7 +156,8 @@ export class Transport {
|
|
|
144
156
|
let lastError: Error | null = null;
|
|
145
157
|
let paymentAttempted = false;
|
|
146
158
|
let extraHeaders: Record<string, string> = {};
|
|
147
|
-
|
|
159
|
+
let attempt = 0;
|
|
160
|
+
while (true) {
|
|
148
161
|
const controller = new AbortController();
|
|
149
162
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
150
163
|
try {
|
|
@@ -158,20 +171,29 @@ export class Transport {
|
|
|
158
171
|
|
|
159
172
|
if (resp.status === 402 && this.paymentHandler && !paymentAttempted) {
|
|
160
173
|
const text = await resp.text();
|
|
161
|
-
let body:
|
|
174
|
+
let body: unknown;
|
|
162
175
|
try {
|
|
163
|
-
body = JSON.parse(text)
|
|
176
|
+
body = JSON.parse(text);
|
|
164
177
|
} catch {
|
|
165
178
|
throw mapError(402, { error: { code: 'invalid_402', message: text } });
|
|
166
179
|
}
|
|
167
|
-
if (
|
|
180
|
+
if (
|
|
181
|
+
!body ||
|
|
182
|
+
typeof body !== 'object' ||
|
|
183
|
+
!Array.isArray((body as PaymentRequiredBody).accepts) ||
|
|
184
|
+
!(body as PaymentRequiredBody).accepts.length ||
|
|
185
|
+
!(body as PaymentRequiredBody).accepts.every(
|
|
186
|
+
(requirement) => requirement !== null && typeof requirement === 'object'
|
|
187
|
+
)
|
|
188
|
+
) {
|
|
168
189
|
throw mapError(402, { error: { code: 'invalid_402', message: 'No payment requirements' } });
|
|
169
190
|
}
|
|
191
|
+
const paymentRequired = body as PaymentRequiredBody;
|
|
170
192
|
const result = await this.paymentHandler({
|
|
171
193
|
url,
|
|
172
194
|
method,
|
|
173
195
|
body: opts.json,
|
|
174
|
-
accepts:
|
|
196
|
+
accepts: paymentRequired.accepts,
|
|
175
197
|
});
|
|
176
198
|
extraHeaders = { ...extraHeaders, ...result.headers };
|
|
177
199
|
paymentAttempted = true;
|
|
@@ -186,8 +208,9 @@ export class Transport {
|
|
|
186
208
|
} catch {
|
|
187
209
|
body = { error: { code: 'unknown', message: text } };
|
|
188
210
|
}
|
|
189
|
-
if (RETRY_STATUS_CODES.has(resp.status) && attempt < this.maxRetries) {
|
|
211
|
+
if (!paymentAttempted && RETRY_STATUS_CODES.has(resp.status) && attempt < this.maxRetries) {
|
|
190
212
|
await sleep(backoffDelay(attempt) * 1000);
|
|
213
|
+
attempt += 1;
|
|
191
214
|
continue;
|
|
192
215
|
}
|
|
193
216
|
throw mapError(resp.status, body);
|
|
@@ -197,14 +220,19 @@ export class Transport {
|
|
|
197
220
|
} catch (err) {
|
|
198
221
|
clearTimeout(timer);
|
|
199
222
|
if (err instanceof APIError) throw err;
|
|
200
|
-
|
|
201
|
-
|
|
223
|
+
if (isAbortError(err)) {
|
|
224
|
+
lastError = timeoutError(err);
|
|
225
|
+
} else {
|
|
226
|
+
lastError = err as Error;
|
|
227
|
+
}
|
|
228
|
+
if (!paymentAttempted && attempt < this.maxRetries) {
|
|
202
229
|
await sleep(backoffDelay(attempt) * 1000);
|
|
230
|
+
attempt += 1;
|
|
203
231
|
continue;
|
|
204
232
|
}
|
|
233
|
+
throw lastError;
|
|
205
234
|
}
|
|
206
235
|
}
|
|
207
|
-
throw lastError ?? new TransportError('Request failed after retries');
|
|
208
236
|
}
|
|
209
237
|
|
|
210
238
|
async *requestStream(
|
|
@@ -214,52 +242,115 @@ export class Transport {
|
|
|
214
242
|
): AsyncGenerator<string, void, unknown> {
|
|
215
243
|
const url = `${this.baseURL}${path}`;
|
|
216
244
|
const headers = { ...this.headers, accept: 'text/event-stream' };
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
const resp = await fetch(url, {
|
|
222
|
-
method,
|
|
223
|
-
headers,
|
|
224
|
-
body: opts.json ? JSON.stringify(opts.json) : undefined,
|
|
225
|
-
signal: controller.signal,
|
|
226
|
-
});
|
|
245
|
+
let paymentAttempted = false;
|
|
246
|
+
let extraHeaders: Record<string, string> = {};
|
|
227
247
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
while (true) {
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const timeoutMs = opts.timeout ?? this.timeout;
|
|
251
|
+
const connectionTimer = setTimeout(() => controller.abort(), timeoutMs);
|
|
252
|
+
try {
|
|
253
|
+
let resp: Response;
|
|
231
254
|
try {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
255
|
+
resp = await fetch(url, {
|
|
256
|
+
method,
|
|
257
|
+
headers: { ...headers, ...extraHeaders },
|
|
258
|
+
body: opts.json ? JSON.stringify(opts.json) : undefined,
|
|
259
|
+
signal: controller.signal,
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (isAbortError(error)) throw timeoutError(error);
|
|
263
|
+
throw error;
|
|
264
|
+
} finally {
|
|
265
|
+
clearTimeout(connectionTimer);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (resp.status === 402 && this.paymentHandler && !paymentAttempted) {
|
|
269
|
+
const text = await resp.text();
|
|
270
|
+
let body: unknown;
|
|
271
|
+
try {
|
|
272
|
+
body = JSON.parse(text);
|
|
273
|
+
} catch {
|
|
274
|
+
throw mapError(402, { error: { code: 'invalid_402', message: text } });
|
|
275
|
+
}
|
|
276
|
+
if (
|
|
277
|
+
!body ||
|
|
278
|
+
typeof body !== 'object' ||
|
|
279
|
+
!Array.isArray((body as PaymentRequiredBody).accepts) ||
|
|
280
|
+
!(body as PaymentRequiredBody).accepts.length ||
|
|
281
|
+
!(body as PaymentRequiredBody).accepts.every(
|
|
282
|
+
(requirement) => requirement !== null && typeof requirement === 'object'
|
|
283
|
+
)
|
|
284
|
+
) {
|
|
285
|
+
throw mapError(402, { error: { code: 'invalid_402', message: 'No payment requirements' } });
|
|
286
|
+
}
|
|
287
|
+
const paymentRequired = body as PaymentRequiredBody;
|
|
288
|
+
const result = await this.paymentHandler({
|
|
289
|
+
url,
|
|
290
|
+
method,
|
|
291
|
+
body: opts.json,
|
|
292
|
+
accepts: paymentRequired.accepts,
|
|
293
|
+
});
|
|
294
|
+
extraHeaders = { ...extraHeaders, ...result.headers };
|
|
295
|
+
paymentAttempted = true;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (resp.status >= 400) {
|
|
300
|
+
const text = await resp.text();
|
|
301
|
+
let body: Record<string, unknown>;
|
|
302
|
+
try {
|
|
303
|
+
body = JSON.parse(text) as Record<string, unknown>;
|
|
304
|
+
} catch {
|
|
305
|
+
body = { error: { code: 'unknown', message: text } };
|
|
306
|
+
}
|
|
307
|
+
throw mapError(resp.status, body);
|
|
235
308
|
}
|
|
236
|
-
throw mapError(resp.status, body);
|
|
237
|
-
}
|
|
238
309
|
|
|
239
|
-
|
|
310
|
+
if (!resp.body) throw new TransportError('No response body for stream');
|
|
240
311
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
312
|
+
const reader = resp.body.getReader();
|
|
313
|
+
const decoder = new TextDecoder();
|
|
314
|
+
let buffer = '';
|
|
244
315
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
316
|
+
try {
|
|
317
|
+
while (true) {
|
|
318
|
+
const idleTimer = setTimeout(() => controller.abort(), timeoutMs);
|
|
319
|
+
let result: Awaited<ReturnType<typeof reader.read>>;
|
|
320
|
+
try {
|
|
321
|
+
result = await reader.read();
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (isAbortError(error)) throw timeoutError(error);
|
|
324
|
+
throw error;
|
|
325
|
+
} finally {
|
|
326
|
+
clearTimeout(idleTimer);
|
|
327
|
+
}
|
|
328
|
+
const { done, value } = result;
|
|
329
|
+
if (done) break;
|
|
330
|
+
buffer += decoder.decode(value, { stream: true });
|
|
249
331
|
|
|
250
|
-
|
|
251
|
-
|
|
332
|
+
const lines = buffer.split('\n');
|
|
333
|
+
buffer = lines.pop() ?? '';
|
|
252
334
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
335
|
+
for (const line of lines) {
|
|
336
|
+
if (line.startsWith('data: ')) {
|
|
337
|
+
const data = line.slice(6);
|
|
338
|
+
if (data === '[DONE]') return;
|
|
339
|
+
yield data;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
try {
|
|
345
|
+
await reader.cancel();
|
|
346
|
+
} catch {
|
|
347
|
+
// Preserve the original stream error, especially SDK TimeoutError.
|
|
258
348
|
}
|
|
259
349
|
}
|
|
350
|
+
return;
|
|
351
|
+
} finally {
|
|
352
|
+
clearTimeout(connectionTimer);
|
|
260
353
|
}
|
|
261
|
-
} finally {
|
|
262
|
-
clearTimeout(timer);
|
|
263
354
|
}
|
|
264
355
|
}
|
|
265
356
|
|
|
@@ -288,12 +379,18 @@ export class Transport {
|
|
|
288
379
|
const controller = new AbortController();
|
|
289
380
|
const timer = setTimeout(() => controller.abort(), opts.timeout ?? this.timeout);
|
|
290
381
|
try {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
382
|
+
let resp: Response;
|
|
383
|
+
try {
|
|
384
|
+
resp = await fetch(url, {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
headers: authHeaders,
|
|
387
|
+
body,
|
|
388
|
+
signal: controller.signal,
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (isAbortError(error)) throw timeoutError(error);
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
297
394
|
clearTimeout(timer);
|
|
298
395
|
|
|
299
396
|
if (resp.status >= 400) {
|