@yetter/client 0.0.12 → 0.0.14
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 +59 -3
- package/dist/api.d.ts +3 -0
- package/dist/api.js +110 -60
- package/dist/client.d.ts +27 -52
- package/dist/client.js +327 -256
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +18 -0
- package/package.json +6 -1
- package/bowow2.jpeg +0 -0
- package/examples/stream.ts +0 -54
- package/examples/submit.ts +0 -80
- package/examples/subscribe.ts +0 -41
- package/examples/upload-and-generate.ts +0 -39
- package/src/api.ts +0 -159
- package/src/client.ts +0 -697
- package/src/index.ts +0 -12
- package/src/types.ts +0 -192
- package/tsconfig.json +0 -13
package/dist/client.js
CHANGED
|
@@ -1,37 +1,127 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
import { YetterImageClient } from "./api.js";
|
|
3
|
-
import { EventSourcePolyfill } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
import { EventSourcePolyfill } from "event-source-polyfill";
|
|
4
|
+
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
5
|
+
const DEFAULT_UPLOAD_TIMEOUT_MS = 5 * 60 * 1000;
|
|
6
|
+
const STREAM_HEARTBEAT_TIMEOUT_MS = 120000;
|
|
7
|
+
const DEFAULT_STREAM_SSE_MAX_CONSECUTIVE_ERRORS = 3;
|
|
8
|
+
const DEFAULT_ENDPOINT = "https://api.yetter.ai";
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
function getEnvApiKey() {
|
|
13
|
+
return process.env.YTR_API_KEY || process.env.REACT_APP_YTR_API_KEY || "";
|
|
14
|
+
}
|
|
15
|
+
export class YetterClient {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.queue = {
|
|
18
|
+
submit: async (model, options) => {
|
|
19
|
+
const client = this.createApiClient();
|
|
20
|
+
return client.generate({
|
|
21
|
+
model,
|
|
22
|
+
...options.input,
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
status: async (model, options) => {
|
|
26
|
+
const client = this.createApiClient();
|
|
27
|
+
const endpoint = client.getApiEndpoint();
|
|
28
|
+
const statusUrl = `${endpoint}/${model}/requests/${options.requestId}/status`;
|
|
29
|
+
const statusData = await client.getStatus({ url: statusUrl });
|
|
30
|
+
return {
|
|
31
|
+
data: statusData,
|
|
32
|
+
requestId: options.requestId,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
result: async (model, options) => {
|
|
36
|
+
const client = this.createApiClient();
|
|
37
|
+
const endpoint = client.getApiEndpoint();
|
|
38
|
+
const responseUrl = `${endpoint}/${model}/requests/${options.requestId}`;
|
|
39
|
+
const responseData = await client.getResponse({ url: responseUrl });
|
|
40
|
+
return {
|
|
41
|
+
data: responseData,
|
|
42
|
+
requestId: options.requestId,
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
this.apiKey = "";
|
|
47
|
+
this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
|
|
48
|
+
const envKey = getEnvApiKey();
|
|
49
|
+
if (envKey) {
|
|
50
|
+
this.apiKey = `Key ${envKey}`;
|
|
51
|
+
}
|
|
52
|
+
if (typeof options.apiKey === "string") {
|
|
53
|
+
this.setApiKey(options.apiKey, options.is_bearer);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
setApiKey(rawApiKey, isBearer) {
|
|
57
|
+
const normalizedApiKey = rawApiKey.trim();
|
|
58
|
+
if (!normalizedApiKey) {
|
|
59
|
+
this.apiKey = "";
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.apiKey = isBearer ? `Bearer ${normalizedApiKey}` : `Key ${normalizedApiKey}`;
|
|
63
|
+
}
|
|
64
|
+
assertApiKeyConfigured() {
|
|
65
|
+
if (!this.apiKey) {
|
|
66
|
+
throw new Error("API key is not configured. Call configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
createApiClient() {
|
|
70
|
+
this.assertApiKeyConfigured();
|
|
71
|
+
return new YetterImageClient({
|
|
72
|
+
apiKey: this.apiKey,
|
|
73
|
+
endpoint: this.endpoint,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
getUploadTimeoutMs(options) {
|
|
77
|
+
if (typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0) {
|
|
78
|
+
return options.timeout;
|
|
79
|
+
}
|
|
80
|
+
return DEFAULT_UPLOAD_TIMEOUT_MS;
|
|
81
|
+
}
|
|
82
|
+
async putWithTimeout(url, body, headers, timeoutMs) {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
85
|
+
try {
|
|
86
|
+
return await fetch(url, {
|
|
87
|
+
method: "PUT",
|
|
88
|
+
headers,
|
|
89
|
+
body,
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if ((error === null || error === void 0 ? void 0 : error.name) === "AbortError") {
|
|
95
|
+
throw new Error(`Upload request timed out after ${timeoutMs}ms`);
|
|
12
96
|
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
configure(options) {
|
|
104
|
+
if (typeof options.apiKey === "string") {
|
|
105
|
+
this.setApiKey(options.apiKey, options.is_bearer);
|
|
13
106
|
}
|
|
14
107
|
if (options.endpoint) {
|
|
15
|
-
|
|
108
|
+
this.endpoint = options.endpoint;
|
|
16
109
|
}
|
|
17
110
|
}
|
|
18
|
-
|
|
111
|
+
async subscribe(model, options) {
|
|
19
112
|
var _b;
|
|
20
|
-
|
|
21
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
22
|
-
}
|
|
23
|
-
const client = new YetterImageClient({
|
|
24
|
-
apiKey: _a.apiKey,
|
|
25
|
-
endpoint: _a.endpoint
|
|
26
|
-
});
|
|
113
|
+
const client = this.createApiClient();
|
|
27
114
|
const generateResponse = await client.generate({
|
|
28
|
-
model
|
|
115
|
+
model,
|
|
29
116
|
...options.input,
|
|
30
117
|
});
|
|
31
118
|
let status = generateResponse.status;
|
|
32
119
|
let lastStatusResponse;
|
|
33
120
|
const startTime = Date.now();
|
|
34
|
-
const timeoutMilliseconds = 30 * 60 * 1000;
|
|
121
|
+
const timeoutMilliseconds = 30 * 60 * 1000;
|
|
122
|
+
const pollIntervalMs = typeof options.pollIntervalMs === "number" && Number.isFinite(options.pollIntervalMs) && options.pollIntervalMs > 0
|
|
123
|
+
? options.pollIntervalMs
|
|
124
|
+
: DEFAULT_POLL_INTERVAL_MS;
|
|
35
125
|
while (status !== "COMPLETED" && status !== "ERROR" && status !== "CANCELLED") {
|
|
36
126
|
if (Date.now() - startTime > timeoutMilliseconds) {
|
|
37
127
|
console.warn(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}. Attempting to cancel.`);
|
|
@@ -53,6 +143,9 @@ export class yetter {
|
|
|
53
143
|
if (options.onQueueUpdate) {
|
|
54
144
|
options.onQueueUpdate(lastStatusResponse);
|
|
55
145
|
}
|
|
146
|
+
if (status !== "COMPLETED" && status !== "ERROR" && status !== "CANCELLED") {
|
|
147
|
+
await sleep(pollIntervalMs);
|
|
148
|
+
}
|
|
56
149
|
}
|
|
57
150
|
catch (error) {
|
|
58
151
|
console.error("Error during status polling:", error);
|
|
@@ -60,35 +153,36 @@ export class yetter {
|
|
|
60
153
|
}
|
|
61
154
|
}
|
|
62
155
|
if (status === "ERROR") {
|
|
63
|
-
const errorMessage = ((_b = lastStatusResponse === null || lastStatusResponse === void 0 ? void 0 : lastStatusResponse.logs) === null || _b === void 0 ? void 0 : _b.map(log => log.message).join("\n")) || "Image generation failed.";
|
|
156
|
+
const errorMessage = ((_b = lastStatusResponse === null || lastStatusResponse === void 0 ? void 0 : lastStatusResponse.logs) === null || _b === void 0 ? void 0 : _b.map((log) => log.message).join("\n")) || "Image generation failed.";
|
|
64
157
|
throw new Error(errorMessage);
|
|
65
158
|
}
|
|
66
|
-
|
|
159
|
+
if (status === "CANCELLED") {
|
|
67
160
|
throw new Error("Image generation was cancelled by user.");
|
|
68
161
|
}
|
|
69
|
-
|
|
162
|
+
return client.getResponse({
|
|
70
163
|
url: generateResponse.response_url,
|
|
71
164
|
});
|
|
72
|
-
return finalResponse;
|
|
73
165
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
77
|
-
}
|
|
78
|
-
const client = new YetterImageClient({
|
|
79
|
-
apiKey: _a.apiKey,
|
|
80
|
-
endpoint: _a.endpoint
|
|
81
|
-
});
|
|
166
|
+
async stream(model, options) {
|
|
167
|
+
const client = this.createApiClient();
|
|
82
168
|
const initialApiResponse = await client.generate({
|
|
83
|
-
model
|
|
169
|
+
model,
|
|
84
170
|
...options.input,
|
|
85
171
|
});
|
|
86
172
|
const requestId = initialApiResponse.request_id;
|
|
87
173
|
const responseUrl = initialApiResponse.response_url;
|
|
174
|
+
const statusUrl = initialApiResponse.status_url;
|
|
88
175
|
const cancelUrl = initialApiResponse.cancel_url;
|
|
89
176
|
const sseStreamUrl = `${client.getApiEndpoint()}/${model}/requests/${requestId}/status/stream`;
|
|
177
|
+
const streamPollIntervalMs = typeof options.pollIntervalMs === "number" && Number.isFinite(options.pollIntervalMs) && options.pollIntervalMs > 0
|
|
178
|
+
? options.pollIntervalMs
|
|
179
|
+
: DEFAULT_POLL_INTERVAL_MS;
|
|
180
|
+
const maxConsecutiveSseErrors = typeof options.sseMaxConsecutiveErrors === "number" &&
|
|
181
|
+
Number.isFinite(options.sseMaxConsecutiveErrors) &&
|
|
182
|
+
options.sseMaxConsecutiveErrors > 0
|
|
183
|
+
? Math.floor(options.sseMaxConsecutiveErrors)
|
|
184
|
+
: DEFAULT_STREAM_SSE_MAX_CONSECUTIVE_ERRORS;
|
|
90
185
|
let eventSource;
|
|
91
|
-
// Setup the promise for the done() method
|
|
92
186
|
let resolveDonePromise;
|
|
93
187
|
let rejectDonePromise;
|
|
94
188
|
const donePromise = new Promise((resolve, reject) => {
|
|
@@ -96,8 +190,6 @@ export class yetter {
|
|
|
96
190
|
rejectDonePromise = reject;
|
|
97
191
|
});
|
|
98
192
|
const controller = {
|
|
99
|
-
// This will be used by the async iterator to pull events
|
|
100
|
-
// It needs a way to buffer events or signal availability
|
|
101
193
|
events: [],
|
|
102
194
|
resolvers: [],
|
|
103
195
|
isClosed: false,
|
|
@@ -124,7 +216,6 @@ export class yetter {
|
|
|
124
216
|
else {
|
|
125
217
|
this.events.push(event);
|
|
126
218
|
}
|
|
127
|
-
// Check for terminal events to resolve/reject the donePromise
|
|
128
219
|
this.currentStatus = event.status;
|
|
129
220
|
},
|
|
130
221
|
error(err) {
|
|
@@ -159,7 +250,7 @@ export class yetter {
|
|
|
159
250
|
if (this.isClosed)
|
|
160
251
|
return;
|
|
161
252
|
this.isClosed = true;
|
|
162
|
-
this.resolvers.forEach(resolve => resolve({ value: undefined, done: true }));
|
|
253
|
+
this.resolvers.forEach((resolve) => resolve({ value: undefined, done: true }));
|
|
163
254
|
this.resolvers = [];
|
|
164
255
|
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
165
256
|
},
|
|
@@ -170,73 +261,158 @@ export class yetter {
|
|
|
170
261
|
if (this.isClosed) {
|
|
171
262
|
return Promise.resolve({ value: undefined, done: true });
|
|
172
263
|
}
|
|
173
|
-
return new Promise(resolve => this.resolvers.push(resolve));
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
eventSource = new EventSourcePolyfill(sseStreamUrl, {
|
|
177
|
-
headers: { 'Authorization': `${_a.apiKey}` },
|
|
178
|
-
});
|
|
179
|
-
eventSource.onopen = (event) => {
|
|
180
|
-
console.log("SSE Connection Opened:", event);
|
|
264
|
+
return new Promise((resolve) => this.resolvers.push(resolve));
|
|
265
|
+
},
|
|
181
266
|
};
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
catch (e) {
|
|
189
|
-
console.error("Error parsing SSE 'data' event:", e, "Raw data:", event.data);
|
|
190
|
-
controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
|
|
267
|
+
let hasStartedFallbackPolling = false;
|
|
268
|
+
const terminalStatuses = new Set(["COMPLETED", "ERROR", "CANCELLED"]);
|
|
269
|
+
const startPollingFallback = async (reason) => {
|
|
270
|
+
if (controller.isClosed || hasStartedFallbackPolling) {
|
|
271
|
+
return;
|
|
191
272
|
}
|
|
192
|
-
|
|
193
|
-
// when 'done' event is received, currentStatus can only be COMPLETED or CANCELLED
|
|
194
|
-
// TODO: remove currentStatus and branch two cases(COMPLETED and CANCELLED) by response status by responseUrl
|
|
195
|
-
// TODO: Determine whether raise error or not when response status is CANCELLED
|
|
196
|
-
// => current code raise error when response status is CANCELLED because resolveDonePromise only get completed response
|
|
197
|
-
// => this mean that user expect only completed response from .done() method
|
|
198
|
-
eventSource.addEventListener('done', async (event) => {
|
|
199
|
-
// console.log("SSE 'done' event received, raw data:", event.data);
|
|
273
|
+
hasStartedFallbackPolling = true;
|
|
200
274
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
275
|
+
console.warn(`Falling back to status polling for ${requestId}: ${reason}`);
|
|
276
|
+
while (!controller.isClosed) {
|
|
277
|
+
const latestStatus = await client.getStatus({ url: statusUrl });
|
|
278
|
+
controller.push(latestStatus);
|
|
279
|
+
if (terminalStatuses.has(latestStatus.status)) {
|
|
280
|
+
if (latestStatus.status === "COMPLETED") {
|
|
281
|
+
const response = await client.getResponse({ url: responseUrl });
|
|
282
|
+
controller.done(response);
|
|
283
|
+
}
|
|
284
|
+
else if (latestStatus.status === "CANCELLED") {
|
|
285
|
+
controller.cancel();
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
controller.error(new Error(`Stream reported ERROR for ${requestId}`));
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
205
291
|
}
|
|
206
|
-
|
|
207
|
-
const response = await client.getResponse({ url: responseUrl });
|
|
208
|
-
controller.done(response);
|
|
209
|
-
}
|
|
210
|
-
else if (controller.currentStatus === "CANCELLED") {
|
|
211
|
-
controller.cancel();
|
|
292
|
+
await sleep(streamPollIntervalMs);
|
|
212
293
|
}
|
|
213
294
|
}
|
|
214
|
-
catch (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
});
|
|
219
|
-
eventSource.addEventListener('error', (event) => {
|
|
220
|
-
// console.log("SSE 'done' event received, raw data:", event.data);
|
|
221
|
-
console.log("SSE 'error' event received, raw data:", event.data);
|
|
222
|
-
controller.error(new Error("Stream reported ERROR for ${requestId}"));
|
|
223
|
-
});
|
|
224
|
-
eventSource.onerror = (err) => {
|
|
225
|
-
var _b;
|
|
226
|
-
const message = ((_b = err === null || err === void 0 ? void 0 : err.error) === null || _b === void 0 ? void 0 : _b.message) || (err === null || err === void 0 ? void 0 : err.message) || '';
|
|
227
|
-
const isIdleTimeout = typeof message === 'string' && message.includes('No activity within') && message.includes('Reconnecting');
|
|
228
|
-
if (isIdleTimeout) {
|
|
229
|
-
console.warn("SSE idle timeout; letting EventSource auto-reconnect.", err);
|
|
230
|
-
return;
|
|
295
|
+
catch (error) {
|
|
296
|
+
controller.error(error instanceof Error
|
|
297
|
+
? error
|
|
298
|
+
: new Error(`Fallback polling failed for ${requestId}: ${String((error === null || error === void 0 ? void 0 : error.message) || error)}`));
|
|
231
299
|
}
|
|
232
|
-
console.warn("SSE Connection Error (onerror) - will allow auto-reconnect:", err);
|
|
233
300
|
};
|
|
234
|
-
// Handle if API immediately returns a terminal status in initialApiResponse (e.g. already completed/failed)
|
|
235
301
|
if (initialApiResponse.status === "COMPLETED") {
|
|
236
302
|
controller.push(initialApiResponse);
|
|
303
|
+
const finalResponse = await client.getResponse({ url: responseUrl });
|
|
304
|
+
controller.done(finalResponse);
|
|
237
305
|
}
|
|
238
306
|
else if (initialApiResponse.status === "ERROR") {
|
|
239
307
|
controller.push(initialApiResponse);
|
|
308
|
+
controller.error(new Error(`Stream reported ERROR for ${requestId}`));
|
|
309
|
+
}
|
|
310
|
+
else if (initialApiResponse.status === "CANCELLED") {
|
|
311
|
+
controller.push(initialApiResponse);
|
|
312
|
+
controller.cancel();
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
if (options.disableSse) {
|
|
316
|
+
void startPollingFallback("SSE disabled by stream options.");
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
let consecutiveSseTransportErrors = 0;
|
|
320
|
+
try {
|
|
321
|
+
eventSource = new EventSourcePolyfill(sseStreamUrl, {
|
|
322
|
+
headers: { Authorization: this.apiKey },
|
|
323
|
+
heartbeatTimeout: STREAM_HEARTBEAT_TIMEOUT_MS,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
console.warn("Failed to initialize SSE transport; switching to polling.", error);
|
|
328
|
+
void startPollingFallback(`SSE initialization failed: ${String((error === null || error === void 0 ? void 0 : error.message) || error)}`);
|
|
329
|
+
}
|
|
330
|
+
if (eventSource) {
|
|
331
|
+
eventSource.onopen = (event) => {
|
|
332
|
+
console.log("SSE Connection Opened:", event);
|
|
333
|
+
};
|
|
334
|
+
eventSource.addEventListener("data", (event) => {
|
|
335
|
+
try {
|
|
336
|
+
consecutiveSseTransportErrors = 0;
|
|
337
|
+
const statusData = JSON.parse(event.data);
|
|
338
|
+
controller.push(statusData);
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
console.error("Error parsing SSE 'data' event:", e, "Raw data:", event.data);
|
|
342
|
+
controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
eventSource.addEventListener("done", async (event) => {
|
|
346
|
+
try {
|
|
347
|
+
try {
|
|
348
|
+
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
349
|
+
}
|
|
350
|
+
catch { }
|
|
351
|
+
if (controller.currentStatus === "COMPLETED") {
|
|
352
|
+
const response = await client.getResponse({ url: responseUrl });
|
|
353
|
+
controller.done(response);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (controller.currentStatus === "CANCELLED") {
|
|
357
|
+
controller.cancel();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (controller.currentStatus === "ERROR") {
|
|
361
|
+
controller.error(new Error(`Stream reported ERROR for ${requestId}`));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const latestStatus = await client.getStatus({ url: statusUrl });
|
|
365
|
+
controller.push(latestStatus);
|
|
366
|
+
if (latestStatus.status === "COMPLETED") {
|
|
367
|
+
const response = await client.getResponse({ url: responseUrl });
|
|
368
|
+
controller.done(response);
|
|
369
|
+
}
|
|
370
|
+
else if (latestStatus.status === "CANCELLED") {
|
|
371
|
+
controller.cancel();
|
|
372
|
+
}
|
|
373
|
+
else if (latestStatus.status === "ERROR") {
|
|
374
|
+
controller.error(new Error(`Stream reported ERROR for ${requestId}`));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
void startPollingFallback("SSE stream ended without terminal status.");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
console.error("Error parsing SSE 'done' event:", e, "Raw data:", event.data);
|
|
382
|
+
controller.error(new Error(`Error parsing SSE 'done' event: ${e.message}`));
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
eventSource.addEventListener("error", (event) => {
|
|
386
|
+
const rawData = event === null || event === void 0 ? void 0 : event.data;
|
|
387
|
+
if (!rawData) {
|
|
388
|
+
console.warn("SSE transport error event; waiting for reconnect.");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
console.log("SSE application 'error' event received, raw data:", rawData);
|
|
392
|
+
controller.error(new Error(`Stream reported ERROR for ${requestId}: ${rawData}`));
|
|
393
|
+
});
|
|
394
|
+
eventSource.onerror = (err) => {
|
|
395
|
+
var _b;
|
|
396
|
+
const message = ((_b = err === null || err === void 0 ? void 0 : err.error) === null || _b === void 0 ? void 0 : _b.message) || (err === null || err === void 0 ? void 0 : err.message) || "";
|
|
397
|
+
const isIdleTimeout = typeof message === "string" &&
|
|
398
|
+
message.includes("No activity within") &&
|
|
399
|
+
message.includes("Reconnecting");
|
|
400
|
+
if (isIdleTimeout) {
|
|
401
|
+
console.warn("SSE idle timeout; letting EventSource auto-reconnect.", err);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
consecutiveSseTransportErrors += 1;
|
|
405
|
+
console.warn("SSE connection issue (onerror); waiting for auto-reconnect.", err);
|
|
406
|
+
if (consecutiveSseTransportErrors >= maxConsecutiveSseErrors) {
|
|
407
|
+
try {
|
|
408
|
+
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
409
|
+
}
|
|
410
|
+
catch { }
|
|
411
|
+
void startPollingFallback(`SSE transport failed ${consecutiveSseTransportErrors} times.`);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
240
416
|
}
|
|
241
417
|
return {
|
|
242
418
|
async *[Symbol.asyncIterator]() {
|
|
@@ -260,56 +436,19 @@ export class yetter {
|
|
|
260
436
|
getRequestId: () => requestId,
|
|
261
437
|
};
|
|
262
438
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
* @param options Upload configuration options
|
|
268
|
-
* @returns Promise resolving to upload result with public URL
|
|
269
|
-
*
|
|
270
|
-
* @example
|
|
271
|
-
* ```typescript
|
|
272
|
-
* // Node.js
|
|
273
|
-
* const result = await yetter.uploadFile("/path/to/image.jpg", {
|
|
274
|
-
* onProgress: (pct) => console.log(`Upload: ${pct}%`)
|
|
275
|
-
* });
|
|
276
|
-
*
|
|
277
|
-
* // Browser
|
|
278
|
-
* const fileInput = document.querySelector('input[type="file"]');
|
|
279
|
-
* const file = fileInput.files[0];
|
|
280
|
-
* const result = await yetter.uploadFile(file, {
|
|
281
|
-
* onProgress: (pct) => updateProgressBar(pct)
|
|
282
|
-
* });
|
|
283
|
-
* ```
|
|
284
|
-
*/
|
|
285
|
-
static async uploadFile(fileOrPath, options = {}) {
|
|
286
|
-
if (!_a.apiKey) {
|
|
287
|
-
throw new Error("API key is not configured. Call yetter.configure()");
|
|
288
|
-
}
|
|
289
|
-
if (typeof fileOrPath === 'string') {
|
|
290
|
-
// Node.js path
|
|
291
|
-
return _a._uploadFromPath(fileOrPath, options);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
// Browser File/Blob
|
|
295
|
-
return _a._uploadFromBlob(fileOrPath, options);
|
|
439
|
+
async uploadFile(fileOrPath, options = {}) {
|
|
440
|
+
this.assertApiKeyConfigured();
|
|
441
|
+
if (typeof fileOrPath === "string") {
|
|
442
|
+
return this._uploadFromPath(fileOrPath, options);
|
|
296
443
|
}
|
|
444
|
+
return this._uploadFromBlob(fileOrPath, options);
|
|
297
445
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
* This is an alias for uploadFile for better clarity in browser contexts
|
|
301
|
-
*/
|
|
302
|
-
static async uploadBlob(file, options = {}) {
|
|
303
|
-
return _a.uploadFile(file, options);
|
|
446
|
+
async uploadBlob(file, options = {}) {
|
|
447
|
+
return this.uploadFile(file, options);
|
|
304
448
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
static async _uploadFromPath(filePath, options) {
|
|
309
|
-
// Dynamic import for Node.js modules
|
|
310
|
-
const fs = await import('fs');
|
|
311
|
-
const mime = await import('mime-types');
|
|
312
|
-
// Validate file exists
|
|
449
|
+
async _uploadFromPath(filePath, options) {
|
|
450
|
+
const fs = await import("fs");
|
|
451
|
+
const mime = await import("mime-types");
|
|
313
452
|
if (!fs.existsSync(filePath)) {
|
|
314
453
|
throw new Error(`File not found: ${filePath}`);
|
|
315
454
|
}
|
|
@@ -317,24 +456,19 @@ export class yetter {
|
|
|
317
456
|
const fileSize = stats.size;
|
|
318
457
|
const fileName = filePath.split(/[\\/]/).pop() || "upload";
|
|
319
458
|
const mimeType = mime.default.lookup(filePath) || "application/octet-stream";
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
endpoint: _a.endpoint,
|
|
323
|
-
});
|
|
324
|
-
// Step 1: Request presigned URL(s)
|
|
459
|
+
const uploadTimeoutMs = this.getUploadTimeoutMs(options);
|
|
460
|
+
const client = this.createApiClient();
|
|
325
461
|
const uploadUrlResponse = await client.getUploadUrl({
|
|
326
462
|
file_name: fileName,
|
|
327
463
|
content_type: mimeType,
|
|
328
464
|
size: fileSize,
|
|
329
465
|
});
|
|
330
|
-
// Step 2: Upload file content
|
|
331
466
|
if (uploadUrlResponse.mode === "single") {
|
|
332
|
-
await
|
|
467
|
+
await this._uploadFileSingle(filePath, uploadUrlResponse.put_url, mimeType, fileSize, options, fs, uploadTimeoutMs);
|
|
333
468
|
}
|
|
334
469
|
else {
|
|
335
|
-
await
|
|
470
|
+
await this._uploadFileMultipart(filePath, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, fs, uploadTimeoutMs);
|
|
336
471
|
}
|
|
337
|
-
// Step 3: Notify completion
|
|
338
472
|
const result = await client.uploadComplete({
|
|
339
473
|
key: uploadUrlResponse.key,
|
|
340
474
|
});
|
|
@@ -343,32 +477,23 @@ export class yetter {
|
|
|
343
477
|
}
|
|
344
478
|
return result;
|
|
345
479
|
}
|
|
346
|
-
|
|
347
|
-
* Upload file from Blob/File object (browser)
|
|
348
|
-
*/
|
|
349
|
-
static async _uploadFromBlob(file, options) {
|
|
480
|
+
async _uploadFromBlob(file, options) {
|
|
350
481
|
const fileSize = file.size;
|
|
351
|
-
const fileName = options.filename ||
|
|
352
|
-
(file instanceof File ? file.name : "blob-upload");
|
|
482
|
+
const fileName = options.filename || (file instanceof File ? file.name : "blob-upload");
|
|
353
483
|
const mimeType = file.type || "application/octet-stream";
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
endpoint: _a.endpoint,
|
|
357
|
-
});
|
|
358
|
-
// Step 1: Request presigned URL(s)
|
|
484
|
+
const uploadTimeoutMs = this.getUploadTimeoutMs(options);
|
|
485
|
+
const client = this.createApiClient();
|
|
359
486
|
const uploadUrlResponse = await client.getUploadUrl({
|
|
360
487
|
file_name: fileName,
|
|
361
488
|
content_type: mimeType,
|
|
362
489
|
size: fileSize,
|
|
363
490
|
});
|
|
364
|
-
// Step 2: Upload file content
|
|
365
491
|
if (uploadUrlResponse.mode === "single") {
|
|
366
|
-
await
|
|
492
|
+
await this._uploadBlobSingle(file, uploadUrlResponse.put_url, mimeType, fileSize, options, uploadTimeoutMs);
|
|
367
493
|
}
|
|
368
494
|
else {
|
|
369
|
-
await
|
|
495
|
+
await this._uploadBlobMultipart(file, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, uploadTimeoutMs);
|
|
370
496
|
}
|
|
371
|
-
// Step 3: Notify completion
|
|
372
497
|
const result = await client.uploadComplete({
|
|
373
498
|
key: uploadUrlResponse.key,
|
|
374
499
|
});
|
|
@@ -377,19 +502,12 @@ export class yetter {
|
|
|
377
502
|
}
|
|
378
503
|
return result;
|
|
379
504
|
}
|
|
380
|
-
|
|
381
|
-
* Upload file using single PUT request (Node.js, private helper)
|
|
382
|
-
*/
|
|
383
|
-
static async _uploadFileSingle(filePath, presignedUrl, contentType, totalSize, options, fs) {
|
|
505
|
+
async _uploadFileSingle(filePath, presignedUrl, contentType, totalSize, options, fs, timeoutMs) {
|
|
384
506
|
const fileBuffer = fs.readFileSync(filePath);
|
|
385
|
-
const response = await
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
"Content-Length": String(totalSize),
|
|
390
|
-
},
|
|
391
|
-
body: fileBuffer,
|
|
392
|
-
});
|
|
507
|
+
const response = await this.putWithTimeout(presignedUrl, fileBuffer, {
|
|
508
|
+
"Content-Type": contentType,
|
|
509
|
+
"Content-Length": String(totalSize),
|
|
510
|
+
}, timeoutMs);
|
|
393
511
|
if (!response.ok) {
|
|
394
512
|
const errorText = await response.text();
|
|
395
513
|
throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
|
|
@@ -398,10 +516,7 @@ export class yetter {
|
|
|
398
516
|
options.onProgress(90);
|
|
399
517
|
}
|
|
400
518
|
}
|
|
401
|
-
|
|
402
|
-
* Upload file using multipart upload (Node.js, private helper)
|
|
403
|
-
*/
|
|
404
|
-
static async _uploadFileMultipart(filePath, partUrls, partSize, totalSize, options, fs) {
|
|
519
|
+
async _uploadFileMultipart(filePath, partUrls, partSize, totalSize, options, fs, timeoutMs) {
|
|
405
520
|
const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
|
|
406
521
|
const fileHandle = fs.openSync(filePath, "r");
|
|
407
522
|
try {
|
|
@@ -414,17 +529,12 @@ export class yetter {
|
|
|
414
529
|
if (chunk.length === 0) {
|
|
415
530
|
break;
|
|
416
531
|
}
|
|
417
|
-
const response = await
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"Content-Length": String(chunk.length),
|
|
421
|
-
},
|
|
422
|
-
body: chunk,
|
|
423
|
-
});
|
|
532
|
+
const response = await this.putWithTimeout(part.url, chunk, {
|
|
533
|
+
"Content-Length": String(chunk.length),
|
|
534
|
+
}, timeoutMs);
|
|
424
535
|
if (!response.ok) {
|
|
425
536
|
const errorText = await response.text();
|
|
426
|
-
throw new Error(`Multipart upload failed at part ${part.part_number} `
|
|
427
|
-
`(${response.status}): ${errorText}`);
|
|
537
|
+
throw new Error(`Multipart upload failed at part ${part.part_number} (${response.status}): ${errorText}`);
|
|
428
538
|
}
|
|
429
539
|
if (options.onProgress) {
|
|
430
540
|
const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
|
|
@@ -436,18 +546,10 @@ export class yetter {
|
|
|
436
546
|
fs.closeSync(fileHandle);
|
|
437
547
|
}
|
|
438
548
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const response = await fetch(presignedUrl, {
|
|
444
|
-
method: "PUT",
|
|
445
|
-
headers: {
|
|
446
|
-
"Content-Type": contentType,
|
|
447
|
-
"Content-Length": String(totalSize),
|
|
448
|
-
},
|
|
449
|
-
body: blob,
|
|
450
|
-
});
|
|
549
|
+
async _uploadBlobSingle(blob, presignedUrl, contentType, _totalSize, options, timeoutMs) {
|
|
550
|
+
const response = await this.putWithTimeout(presignedUrl, blob, {
|
|
551
|
+
"Content-Type": contentType,
|
|
552
|
+
}, timeoutMs);
|
|
451
553
|
if (!response.ok) {
|
|
452
554
|
const errorText = await response.text();
|
|
453
555
|
throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
|
|
@@ -456,10 +558,7 @@ export class yetter {
|
|
|
456
558
|
options.onProgress(90);
|
|
457
559
|
}
|
|
458
560
|
}
|
|
459
|
-
|
|
460
|
-
* Upload blob using multipart upload (browser, private helper)
|
|
461
|
-
*/
|
|
462
|
-
static async _uploadBlobMultipart(blob, partUrls, partSize, totalSize, options) {
|
|
561
|
+
async _uploadBlobMultipart(blob, partUrls, partSize, totalSize, options, timeoutMs) {
|
|
463
562
|
const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
|
|
464
563
|
for (let i = 0; i < sortedParts.length; i++) {
|
|
465
564
|
const part = sortedParts[i];
|
|
@@ -469,17 +568,10 @@ export class yetter {
|
|
|
469
568
|
if (chunk.size === 0) {
|
|
470
569
|
break;
|
|
471
570
|
}
|
|
472
|
-
const response = await
|
|
473
|
-
method: "PUT",
|
|
474
|
-
headers: {
|
|
475
|
-
"Content-Length": String(chunk.size),
|
|
476
|
-
},
|
|
477
|
-
body: chunk,
|
|
478
|
-
});
|
|
571
|
+
const response = await this.putWithTimeout(part.url, chunk, {}, timeoutMs);
|
|
479
572
|
if (!response.ok) {
|
|
480
573
|
const errorText = await response.text();
|
|
481
|
-
throw new Error(`Multipart upload failed at part ${part.part_number} `
|
|
482
|
-
`(${response.status}): ${errorText}`);
|
|
574
|
+
throw new Error(`Multipart upload failed at part ${part.part_number} (${response.status}): ${errorText}`);
|
|
483
575
|
}
|
|
484
576
|
if (options.onProgress) {
|
|
485
577
|
const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
|
|
@@ -488,54 +580,33 @@ export class yetter {
|
|
|
488
580
|
}
|
|
489
581
|
}
|
|
490
582
|
}
|
|
583
|
+
export class yetter {
|
|
584
|
+
static configure(options) {
|
|
585
|
+
this.client.configure(options);
|
|
586
|
+
}
|
|
587
|
+
static subscribe(model, options) {
|
|
588
|
+
return this.client.subscribe(model, options);
|
|
589
|
+
}
|
|
590
|
+
static stream(model, options) {
|
|
591
|
+
return this.client.stream(model, options);
|
|
592
|
+
}
|
|
593
|
+
static uploadFile(fileOrPath, options = {}) {
|
|
594
|
+
return this.client.uploadFile(fileOrPath, options);
|
|
595
|
+
}
|
|
596
|
+
static uploadBlob(file, options = {}) {
|
|
597
|
+
return this.client.uploadBlob(file, options);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
491
600
|
_a = yetter;
|
|
492
|
-
yetter.
|
|
493
|
-
yetter.endpoint = "https://api.yetter.ai";
|
|
601
|
+
yetter.client = new YetterClient();
|
|
494
602
|
yetter.queue = {
|
|
495
603
|
submit: async (model, options) => {
|
|
496
|
-
|
|
497
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
498
|
-
}
|
|
499
|
-
const client = new YetterImageClient({
|
|
500
|
-
apiKey: _a.apiKey,
|
|
501
|
-
endpoint: _a.endpoint
|
|
502
|
-
});
|
|
503
|
-
const generateResponse = await client.generate({
|
|
504
|
-
model: model,
|
|
505
|
-
...options.input,
|
|
506
|
-
});
|
|
507
|
-
return generateResponse;
|
|
604
|
+
return _a.client.queue.submit(model, options);
|
|
508
605
|
},
|
|
509
606
|
status: async (model, options) => {
|
|
510
|
-
|
|
511
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
512
|
-
}
|
|
513
|
-
const client = new YetterImageClient({
|
|
514
|
-
apiKey: _a.apiKey,
|
|
515
|
-
endpoint: _a.endpoint
|
|
516
|
-
});
|
|
517
|
-
const endpoint = client.getApiEndpoint();
|
|
518
|
-
const statusUrl = `${endpoint}/${model}/requests/${options.requestId}/status`;
|
|
519
|
-
const statusData = await client.getStatus({ url: statusUrl });
|
|
520
|
-
return {
|
|
521
|
-
data: statusData,
|
|
522
|
-
requestId: options.requestId,
|
|
523
|
-
};
|
|
607
|
+
return _a.client.queue.status(model, options);
|
|
524
608
|
},
|
|
525
609
|
result: async (model, options) => {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
const client = new YetterImageClient({
|
|
530
|
-
apiKey: _a.apiKey,
|
|
531
|
-
endpoint: _a.endpoint
|
|
532
|
-
});
|
|
533
|
-
const endpoint = client.getApiEndpoint();
|
|
534
|
-
const responseUrl = `${endpoint}/${model}/requests/${options.requestId}`;
|
|
535
|
-
const responseData = await client.getResponse({ url: responseUrl });
|
|
536
|
-
return {
|
|
537
|
-
data: responseData,
|
|
538
|
-
requestId: options.requestId,
|
|
539
|
-
};
|
|
540
|
-
}
|
|
610
|
+
return _a.client.queue.result(model, options);
|
|
611
|
+
},
|
|
541
612
|
};
|